How to inherit a custom constructor for all Flask-SQLAlchemy models

Viewed 472

My code:

# db.py
class NewModel(Model):
    user = sa.Column(sa.String(256), nullable = False, default = 'User')
    uploaded = sa.Column(sa.DateTime, nullable = False, default = datetime.utcnow)

    def __init__(self, **kwargs):
        kwargs['user'] = session['username']

        super().__init__(**kwargs)

    def save(self):
        db.session.add(self)
        db.session.commit()

db = SQLAlchemy(model_class = NewModel)
# upload_file.py
class UploadFile(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    filename = db.Column(db.String(256), nullable = False)
    path = db.Column(db.String(256), nullable = False, unique = True)

    def __init__(self, file):
        self.file = file

        kwargs = {
            'filename': file.filename,
            'path': secure_filename(file.filename)
        }

        super().__init__(**kwargs)

    def valid(self):
        return self.file is not None and \
            self.file.filename.strip() != ''

When I try to make an instance of UploadFile it works, but it doesn't call NewModel's constructor. Why?

I've found this issue, but it didn't solve my problem.
And I tried to use an orm.reconstructor decorator but it didn't work either.

1 Answers

The approach in the question doesn't work because SQLAlchemy patches its own __init__ method onto the model superclass. The default implementation assigns instance attribute values based on keyword arguments, which is why the code "works" even though the defined __init__ method is not called.

This is configurable via the declarative_base function, but unfortunately Flask-SQLAlchemy doesn't expose this directly.

It's possible to get the desired results by creating an abstract model class, and having the application's models inherit from it.

db = SQLAlchemy(app)


class NewModel(db.Model):

    __abstract__ = True

    user = sa.Column(sa.String(256), nullable=False, default="User")
    uploaded = sa.Column(sa.DateTime, nullable=False, default=datetime.utcnow)

    def __init__(self, **kwargs):
        kwargs["user"] = session["username"]
        super().__init__(**kwargs)

    def save(self):
        db.session.add(self)
        db.session.commit()


# upload_file.py
class UploadFile(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    filename = db.Column(db.String(256), nullable=False)
    path = db.Column(db.String(256), nullable=False, unique=True)

    def __init__(self, file):
        kwargs = {
            'filename': file.filename,
            'path': secure_filename(file.filename)
        }

        super().__init__(**kwargs)

To get a working __init__ implementation on the model superclass (NewModel), we need to make a custom constructor function and use it, Flask's DefaultMeta metaclass and the NewModel class to create a new declarative base class that can be passed to the SQLAlchemy constructor.

import sqlalchemy as sa
from sqlalchemy import orm
from flask import Flask
from flask_sqlalchemy import SQLAlchemy, Model, DefaultMeta

class NewModel(Model):

    user = sa.Column(sa.String(256), nullable=False, default="User")
    uploaded = sa.Column(sa.DateTime, nullable=False, default=datetime.utcnow)

    def save(self):
        db.session.add(self)
        db.session.commit()

def my_declarative_constructor(self, **kwargs):
    """This will be the `__init__` method for all models."""
    # Set attrs from kwargs using SQLALchemy's default constructor function.
    orm.decl_api._declarative_constructor(self, **kwargs)
    # Do custom attribute assignments.
    self.user = session['username']

# Create a new declarative base class that combines our constructor function,
# Flask's default metaclass and our NewModel class.
Base = orm.declarative_base(
    constructor=my_declarative_constructor, metaclass=DefaultMeta, cls=NewModel
)

# Use our declarative base class to create the db integration.
db = SQLAlchemy(app, model_class=Base)


# upload_file.py
class UploadFile(db.Model):
    ...
Related