代码之家  ›  专栏  ›  技术社区  ›  MDB

Flask+SQLAlchemy-用于修改列设置器的自定义元类(动态hybrid_property)

  •  3
  • MDB  · 技术社区  · 9 年前

    我有一个使用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)
    

    这就像一个魅力——除了模型定义之外,从未看到_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)
    

    当然,SQLAlchemy和Flask幕后进行的诡计与元类的结合使这一点变得不那么直接(这也是为什么“自定义元类以在SQLAlcheme中创建混合属性”这一几乎匹配的问题没有太大帮助-Flask也会碍手碍脚)。在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
    

    设置好后,模型类可以如下所示:

    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)
    

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

    显然,我还没有完全模拟动态版本中的静态版本的代码,但我真的不知道问题出在哪里 应该 注册setter就像它有getter一样,但它不是。在静态版本中,setter被注册并使用得很好。

    关于如何完成最后一步,有什么想法吗?

    1 回复  |  直到 9 年前
        1
  •  5
  •   Timothée Jeannin    9 年前

    可能使用自定义类型?

    from sqlalchemy import TypeDecorator, Text
    
    class CleanedHtml(TypeDecorator):
        impl = Text
    
        def process_bind_param(self, value, dialect):
            return clean_the_data(value)
    

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

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

    更多说明可在以下文档中找到: http://docs.sqlalchemy.org/en/latest/core/custom_types.html#augmenting-existing-types