Flask + SQLAlchemy - 自定义元类来修改列设置器(动态混合属性)

MDB*_*MDB 3 python sqlalchemy metaclass flask flask-sqlalchemy

我有一个现有的使用 SQLAlchemy 的 Flask 应用程序。这个应用程序中的几个模型/表都有存储原始 HTML 的列,我想在列的 setter 上注入一个函数,以便传入的原始 html 被“清理”。我想在模型中执行此操作,因此我不必在整个表单或路由代码中添加“清理此数据”。

我目前已经可以这样做:

from application import db, clean_the_data
from sqlalchemy.ext.hybrid import hybrid_property
class Example(db.Model):
  __tablename__ = 'example'

  normal_column = db.Column(db.Integer,
                            primary_key=True,
                            autoincrement=True)

  _html_column = db.Column('html_column', db.Text,
                           nullable=False)

  @hybrid_property
  def html_column(self):
    return self._html_column

  @html_column.setter
  def html_column(self, value):
    self._html_column = clean_the_data(value)
Run Code Online (Sandbox Code Playgroud)

这就像一个魅力 - 除了模型定义之外,_html_column 名称从未出现过,调用了更清洁的函数,并使用了清洁后的数据。万岁。

我当然可以停在那里,只吃列的丑陋处理,但是当您可以弄乱元类时为什么要这样做呢?

注意:以下都假设“application”是主要的 Flask 模块,并且它包含两个子项:“db”——SQLAlchemy 句柄和“clean_the_data”——清理传入 HTML 的函数。

因此,我开始尝试创建一个新的基本 Model 类,该类在创建类时发现需要清理的列,并自动处理各种事情,因此您可以执行以下操作,而不是上面的代码:

from application import db
class Example(db.Model):
  __tablename__ = 'example'
  __html_columns__ = ['html_column'] # Our oh-so-subtle hint

  normal_column = db.Column(db.Integer,
                            primary_key=True,
                            autoincrement=True)

  html_column = db.Column(db.Text,
                          nullable=False)
Run Code Online (Sandbox Code Playgroud)

当然,SQLAlchemy 和 Flask 在幕后进行的诡计与元类的结合使得这不太直接(这也是为什么几乎匹配的问题“在 SQLAlchemy 中创建混合属性的自定义元类”并没有多大帮助 -烧瓶也妨碍了)。我几乎已经在 application/models/__init__.py 中使用以下内容:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
# Yes, I'm importing _X stuff...I tried other ways to avoid this
# but to no avail
from flask_sqlalchemy import (Model as BaseModel,
                              _BoundDeclarativeMeta,
                              _QueryProperty)
from application import db, clean_the_data

class _HTMLBoundDeclarativeMeta(_BoundDeclarativeMeta):
  def __new__(cls, name, bases, d):
    # Move any fields named in __html_columns__ to a
    # _field/field pair with a hybrid_property
    if '__html_columns__' in d:
      for field in d['__html_columns__']:
        if field not in d:
          continue
        hidden = '_' + field
        fget = lambda self: getattr(self, hidden)
        fset = lambda self, value: setattr(self, hidden,
                                           clean_the_data(value))
        d[hidden] = d[field] # clobber...
        d[hidden].name = field # So we don't have to explicitly
                               # name the column. Should probably
                               # force a quote on the name too
        d[field] = hybrid_property(fget, fset)
      del d['__html_columns__'] # Not needed any more
    return _BoundDeclarativeMeta.__new__(cls, name, bases, d)

# The following copied from how flask_sqlalchemy creates it's Model
Model = declarative_base(cls=BaseModel, name='Model',
                         metaclass=_HTMLBoundDeclarativeMeta)
Model.query = _QueryProperty(db)

# Need to replace the original Model in flask_sqlalchemy, otherwise it
# uses the old one, while you use the new one, and tables aren't
# shared between them
db.Model = Model
Run Code Online (Sandbox Code Playgroud)

设置好后,您的模型类可能如下所示:

from application import db
from application.models import Model

class Example(Model): # Or db.Model really, since it's been replaced
  __tablename__ = 'example'
  __html_columns__ = ['html_column'] # Our oh-so-subtle hint

  normal_column = db.Column(db.Integer,
                            primary_key=True,
                            autoincrement=True)

  html_column = db.Column(db.Text,
                          nullable=False)
Run Code Online (Sandbox Code Playgroud)

几乎有效,因为没有错误,数据被正确读取和保存等。除了永远不会调用 hybrid_property 的设置器。getter 是(我已经确认了两者中的打印语句),但是 setter 被完全忽略,因此从不调用更清洁的函数。数据设置 - 对未清理的数据进行了非常愉快的更改。

显然,我还没有完全模拟动态版本中代码的静态版本,但老实说,我不知道问题出在哪里。据我所知, hybrid_property应该像注册 getter 一样注册 setter,但事实并非如此。在静态版本中,setter 被注册并使用得很好。

关于如何使最后一步起作用的任何想法?

Tim*_*nin 5

也许使用自定义类型?

from sqlalchemy import TypeDecorator, Text

class CleanedHtml(TypeDecorator):
    impl = Text

    def process_bind_param(self, value, dialect):
        return clean_the_data(value)
Run Code Online (Sandbox Code Playgroud)

然后你可以这样写你的模型:

class Example(db.Model):
    __tablename__ = 'example'
    normal_column = db.Column(db.Integer, primary_key=True, autoincrement=True)
    html_column = db.Column(CleanedHtml)
Run Code Online (Sandbox Code Playgroud)

此处的文档中有更多解释:http : //docs.sqlalchemy.org/en/latest/core/custom_types.html#augmenting-existing-types