SQLAlchemy 多态进阶(__mapper_args__ )、多对多标签

 nadia     2020-03-26     1500     0   

欢迎来到银盒子的世界~

转自博客:https://blog.csdn.net/u012206617/article/details/95364152


熟悉SQLAlchemy的人往往知道polymorphic(多态)的模型定义,如果你恰好不熟悉SQLAlchemy,这里简单的举一个例子:

class Employee(Base):
  __tablename__ = 'employee'
  id = Column(Integer, primary_key=True)
  name = Column(String(50))
  type = Column(String(50))
  __mapper_args__ = {
      'polymorphic_identity':'employee',
      'polymorphic_on':type
   }

这里定义了雇员Employee 模型,指定type字段为多态所在字段,并且对于这个模型,当type字段为'employee'时,即为一个雇员

再看下面这两个继承之后的模型

class Engineer(Employee):
  __tablename__ = 'engineer'
  id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
  engineer_name = Column(String(30))
  __mapper_args__ = {
      'polymorphic_identity':'engineer',
   }
class Manager(Employee):
  __tablename__ = 'manager'
  id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
  manager_name = Column(String(30))
  __mapper_args__ = {
      'polymorphic_identity':'manager',
   }

这里又定义了两个模型,Engineer,Manager,

并对应了两张表,这两张表的结构除了有少许不同,类似的,polymorphic_identity指定了这两种模型对应的type字段值,

在上面的基础上,可以提出的问题:

  1. 可不可以完全在一张表上实现这样的多态?

  2. 这样的模型可以用作M2M关系吗?

两者的答案显然是肯定的。 对于第一个问题,只需要使得后两者的__tablename__ = None,并且不指定额外的字段即可。

第二个问题,即这几天我的重构的探索

如何设置多对多模型

对于一个多对多的关系表,按照 SQLAlchemy 文档:

association_table = Table('association', Base.metadata,
    Column('left_id', Integer, ForeignKey('left.id')),
    Column('right_id', Integer, ForeignKey('right.id')))
    
class Parent(Base):
  __tablename__ = 'left'
  id = Column(Integer, primary_key=True)
  children = relationship("Child",secondary=association_table)
  
class Child(Base):
  __tablename__ = 'right'
  id = Column(Integer, primary_key=True)

(虽然我们的基本不会按照SQLAlchemy那样定义ForeignKey了,万恶的ForeignKey。。)

关键在于应当有第三张表,存放M2M的关系。上面的association,就是这样的一张M2M表,有两个字段left_id和right_id

而且显然的,我们可以轻松地想象出取出M2M关系的SQL:

select left.id,right.id from left join association on left.id=association.left_id
join right on association.right_id=right.id

是借助association实现两个表的JOIN关系

SQLAlchemy 的对应操作这里就不赘述了,大家看文档吧咩哈哈

M2M和多态

此次重构遇到的问题就是:如果我们的M2M的关系,如果是在多态上进行的,例如上面的Child,如果我不仅仅有Child,还分Boy和Girl,如何在这一张association_table进行控制呢? 上面代码先稍作修改:

class Association(Base):
  left_id = Column('left_id', Integer, ForeignKey('left.id')),
  right_id = Column('right_id', Integer, ForeignKey('right.id'))
  gender = Column('gender', Boolean)
  __mapper_args__ = {"polymorphic_on": gender}

增加了gender字段,并且增加了多态声明__mapper_args__ 我们先假设一下这样的SQL该怎么写吧,实际上是很简单的哈:

select left.id,right.id from left join association on (left.id=association.left_id and association.gender)
join right on association.right_id=right.id

join的时候额外加一个字段即可。

如何让SQLalchemy可以生成出这样的SQL,并且还自动进行例如增删查改的SQL声明呢?

SQLAlchemy同样给出了对应的 样例

我基于这个样例做了一定的修改:

  1. 完全不用ForeignKey声明。这一点很容易,from sqlalchemy.orm import foreign,用foreign函数包一下对应的字段,就可以当成外键来用

  2. 样例中的Address声明了association = relationship("AddressAssociation", backref="addresses"),这样使得AddressAssociation有了一个addresses的反向引用(backref),在实际的M2M模型设计中,考虑到是跨模块的模型映射,为了方便修改和维护,没有修改M2M左边的这个M,因此在AddressAssociation中动态声明了一个addresses

  3. 增加relationship cascade属性,以进行删除操作

对应的diff如下(稍微修改了字段名),稍后有完整代码:

  1. --- origin.py    2016-10-13 11:28:57.000000000 +0800  
  2. +++ target.py    2016-10-13 11:29:44.000000000 +0800  
  3. @@ -1,80 +1,84 @@  
  4. from sqlalchemy.ext.declarative import as_declarative, declared_attr
  5. from sqlalchemy import create_engine, Integer, Column, \
  6. -    String, ForeignKey
  7. -from sqlalchemy.orm import Session, relationship, backref
  8. +    String, and_
  9. +from sqlalchemy.orm import Session, foreign, relationship, backref
  10. from sqlalchemy.ext.associationproxy import association_proxy
  11. class AddressAssociation(Base):
  12. """Associates a collection of Address objects
  13.     with a particular parent.

  14.     """
  15. __tablename__ = "address_association"
  16. -
  17. -    discriminator = Column(String)
  18. +    addr_id = Column(Integer,
  19. +                     primary_key=True,
  20. +                     )
  21. +    order_id = Column(Integer,
  22. +                      primary_key=True,
  23. +                      )
  24. +    discriminator = Column(String, primary_key=True)
  25. """Refers to the type of parent."""
  26. __mapper_args__ = {"polymorphic_on": discriminator}
  27. class Address(Base):
  28. """The Address class.

  29.     This represents all address records in a
  30.     single table.

  31.     """
  32. -    association_id = Column(Integer, ForeignKey("address_association.id"))
  33. +    id = Column(Integer, primary_key=True)
  34. street = Column(String)
  35. city = Column(String)
  36. zip = Column(String)
  37. -    association = relationship("AddressAssociation", backref="addresses")
  38. -
  39. -    parent = association_proxy("association", "parent")
  40. def __repr__(self):
  41. return "%s(street=%r, city=%r, zip=%r)" % \
  42. (self.__class__.__name__, self.street,
  43. self.city, self.zip)
  44. class HasAddresses(object):
  45. """HasAddresses mixin, creates a relationship to
  46.     the address_association table for each parent.

  47.     """
  48.     @declared_attr
  49. -    def address_association_id(cls):  
  50. -        return Column(Integer, ForeignKey("address_association.id"))
  51. -
  52. -    @declared_attr
  53. def address_association(cls):
  54. name = cls.__name__
  55. discriminator = name.lower()
  56. assoc_cls = type(
  57. "%sAddressAssociation" % name,
  58. (AddressAssociation, ),
  59. dict(
  60. __tablename__=None,
  61. __mapper_args__={
  62. "polymorphic_identity": discriminator
  63. -                }
  64. -            )
  65. +                },
  66. +                addresses=relationship(
  67. +                    Address,
  68. +                    primaryjoin="Address.idforeign({assoc_cls_name}.addr_id)".format(assoc_cls_name=assoc_cls_name))
  69. +                        )
  70. )
  71. cls.addresses = association_proxy(
  72. "address_association", "addresses",
  73. creator=lambda addresses: assoc_cls(addresses=addresses)
  74. )
  75. return relationship(assoc_cls,
  76. -                            backref=backref("parent", uselist=False))
  77. +                            primaryjoin=and_(foreign(assoc_cls.addr_id)  Address.id,
  78. +                                             foreign(assoc_cls.order_id) == cls.id),
  79. +                            cascade="save-update, merge, delete, delete-orphan",
  80. +                            )
  81. class Customer(HasAddresses, Base):
  82. name = Column(String)
  83. class Supplier(HasAddresses, Base):
  84. company_name = Column(String)

此后就可以通过Customer.addresses.append等操作M2M了

 


发表评论