Свяжите «внешнюю» модель класса с flask sqlalchemy

Мы используем модель центрального класса для самых разных модулей Python. Эта модель определяется с помощью SQLAlchemy. Все классы наследуются от declarative_base.

Например, определения нашей модели выглядят примерно так:

Base = declarative_base()

class Post(Base):
    __tablename__ = 'Posts'
    id = Column(INT, primary_key=True, autoincrement=True)
    body = Column(TEXT)
    timestamp = Column(TIMESTAMP)
    user_id = Column(INT, ForeignKey('Users.uid'))

Мы создаем фляжное веб-приложение, в котором используем ту же модель. Мы обнаружили сложную проблему, заключающуюся в том, что flask-sqlalchemy разработана таким образом, что она ожидает, что все классы, используемые в ее модели, будут определены путем передачи активного экземпляра сеанса. Вот пример «правильного» определения модели класса flask-sqalchemy:

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)

Обратите внимание, что приведенный выше пример для flask-sqlalchemy требует уже созданного экземпляра сеанса sql. Это привело нас в ужас, потому что мы совершенно не знаем, как интегрировать нашу модель SqlAlchemy в flask. Мы действительно хотим использовать именно пакет flask-security.

Эта проблема уже поднималась на SO. Вот, например: Как использовать flask- sqlalchemy с существующей моделью sqlalchemy?

Наши требования отличаются от требований тех, кто принял там ответ. В ответе указано, что теряется возможность использовать User.query, но это как раз одна из вещей, которую мы должны сохранить.

Невозможно отказаться от нашего красивого, элегантного определения модели центрального класса в пользу того, что, по-видимому, требует flask-sqlalchemy. Есть ли способ связать нашу модель с объектом SQLAlchemy()? Бонусные баллы за то, что мы получили метод .query() для наших классов, который, по-видимому, требуется flask-security.


person melchoir55    schedule 01.03.2015    source источник


Ответы (1)


Решение:

На сегодняшний день лучше всего это сделать следующим образом:

Реализовать или импортировать sqlalchemy base

from sqlalchemy.ext.declarative import declarative_base

base = declarative_base()


class Base(base):

    __abstract__ = True
    uid = Column(Integer, primary_key=True, autoincrement=True)

Зарегистрируйте внешнюю базу:

from flask_sqlalchemy import SQLAlchemy
from model.base import Base

app = Flask(__name__)
db = SQLAlchemy(app, model_class=Base)

Заархивировано для потомков:

Я потратил много времени на поиск ответа на этот вопрос. Сегодня это сделать намного проще, чем когда я изначально задавал этот вопрос, но все же это не совсем просто.

Всем, кто решит заняться безопасностью самостоятельно, я рекомендую следующее превосходное изложение общих шаблонов проектирования, использующих flask, но избегающих использования ненужных зависимостей, таких как flask-security: https://exploreflask.com/users.html

ОБНОВЛЕНИЕ: Для всех, кто заинтересован, патч, связанный с этим, находится в разработке в течение некоторого времени. На данный момент он еще не выпущен, но вы можете проверить его ход здесь: https://github.com/mitsuhiko/flask-sqlalchemy/pull/250#issuecomment-77504337

ОБНОВЛЕНИЕ: я взял код из вышеупомянутого патча и создал локальное переопределение для объекта SQLAlchemy, которое позволяет зарегистрировать внешнюю базу. Я думаю, что это лучший доступный вариант до тех пор, пока FSA не добавит это официально. Вот код из этого класса для всех, кто заинтересован. Протестирована работа с Flask-SqlAlchemy 2.2.

Исправление в register_external_base:

import flask_sqlalchemy
'''Created by Isaac Martin 2017. Licensed insofar as it can be according to the standard terms of the MIT license: https://en.wikipedia.org/wiki/MIT_License. The author accepts no liability for consequences resulting from the use of this software. '''
class SQLAlchemy(flask_sqlalchemy.SQLAlchemy):
    def __init__(self, app=None, use_native_unicode=True, session_options=None,
                 metadata=None, query_class=flask_sqlalchemy.BaseQuery, model_class=flask_sqlalchemy.Model):

        self.use_native_unicode = use_native_unicode
        self.Query = query_class
        self.session = self.create_scoped_session(session_options)
        self.Model = self.make_declarative_base(model_class, metadata)
        self._engine_lock = flask_sqlalchemy.Lock()
        self.app = app
        flask_sqlalchemy._include_sqlalchemy(self, query_class)
        self.external_bases = []

        if app is not None:
            self.init_app(app)

    def get_tables_for_bind(self, bind=None):
        """Returns a list of all tables relevant for a bind."""
        result = []
        for Base in self.bases:
            for table in flask_sqlalchemy.itervalues(Base.metadata.tables):
                if table.info.get('bind_key') == bind:
                    result.append(table)

        return result

    def get_binds(self, app=None):
        """Returns a dictionary with a table->engine mapping.
        This is suitable for use of sessionmaker(binds=db.get_binds(app)).
        """
        app = self.get_app(app)
        binds = [None] + list(app.config.get('SQLALCHEMY_BINDS') or ())
        retval = {}
        for bind in binds:
            engine = self.get_engine(app, bind)
            tables = self.get_tables_for_bind(bind)
            retval.update(dict((table, engine) for table in tables))
        return retval

    @property
    def bases(self):
        return [self.Model] + self.external_bases

    def register_base(self, Base):
        """Register an external raw SQLAlchemy declarative base.
        Allows usage of the base with our session management and
        adds convenience query property using self.Query by default."""

        self.external_bases.append(Base)
        for c in Base._decl_class_registry.values():
            if isinstance(c, type):
                if not hasattr(c, 'query') and not hasattr(c, 'query_class'):
                    c.query_class = self.Query
                if not hasattr(c, 'query'):
                    c.query = flask_sqlalchemy._QueryProperty(self)

                    # for name in dir(c):
                    #     attr = getattr(c, name)
                    #     if type(attr) == orm.attributes.InstrumentedAttribute:
                    #         if hasattr(attr.prop, 'query_class'):
                    #             attr.prop.query_class = self.Query

                    # if hasattr(c , 'rel_dynamic'):
                    #     c.rel_dynamic.prop.query_class = self.Query

Для использования следующим образом:

app = Flask(__name__)
db = SQLAlchemy(app)
db.register_base(base)
person melchoir55    schedule 03.03.2015
comment
Это фантастика! Не могли бы вы поместить заголовок лицензии в код, чтобы мы могли его использовать. - person rachekalmir; 17.05.2017
comment
Вы можете использовать код, предоставленный на SO, без явной лицензии. Тем не менее, я добавил для вас строку документации, если это вам поможет. - person melchoir55; 17.05.2017
comment
Сумасшедший, после довольно продолжительного поиска по этому вопросу оказалось, что до сих пор не было PR для устранения этого недостатка. Выражаю огромную благодарность merchoir55! Еще одна проблема, с которой я столкнулся, заключается в том, что для текущих версий требуется закрытый атрибут _engine_options. Как только я добавил self._engine_options = engine_options or {} в конструктор вышеуказанного переопределения, он заработал как шарм! Я тоже в восторге, я годами отскакивал от этой проблемы, никогда не решаясь ее обойти, потому что она обычно живет для меня в библиотечных проблемах;) - person kmjb; 24.06.2020
comment
Переопределения больше не требуются. Обратите внимание, что переопределения находятся в разделе, который «заархивирован для потомков». Лучший способ сделать это сейчас — передать базу в конструктор, как подробно описано в разделе в верхней части моего ответа. - person melchoir55; 25.06.2020
comment
Есть ли что-то, что следует учитывать при применении вашего примера, например. мне нужен столбец __abstract__ = True или этот столбец uid? Или было бы достаточно использовать base = declarative_base() и поместить его как db = SQLAlchemy(app, model_class=base) и убедиться, что все мои модели также наследуются от base? - person Joe; 15.10.2020
comment
Ваши модели просто должны наследоваться от base. - person melchoir55; 15.10.2020