SQLAlchemy 多态进阶(__mapper_args__ )、多对多标签
什么是polymorphic
熟悉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字段值,
在上面的基础上,可以提出的问题:
- 可不可以完全在一张表上实现这样的多态?
- 这样的模型可以用作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同样给出了对应的 样例
我基于这个样例做了一定的修改:
- 完全不用ForeignKey声明。这一点很容易,from sqlalchemy.orm import foreign,用foreign函数包一下对应的字段,就可以当成外键来用
- 样例中的Address声明了association = relationship("AddressAssociation", backref="addresses"),这样使得AddressAssociation有了一个addresses的反向引用(backref),在实际的M2M模型设计中,考虑到是跨模块的模型映射,为了方便修改和维护,没有修改M2M左边的这个M,因此在AddressAssociation中动态声明了一个addresses
- 增加relationship cascade属性,以进行删除操作
对应的diff如下(稍微修改了字段名),稍后有完整代码:
--- origin.py 2016-10-13 11:28:57.000000000 +0800
+++ target.py 2016-10-13 11:29:44.000000000 +0800
@@ -1,80 +1,84 @@
from sqlalchemy.ext.declarative import as_declarative, declared_attr
from sqlalchemy import create_engine, Integer, Column, \
- String, ForeignKey
-from sqlalchemy.orm import Session, relationship, backref
+ String, and_
+from sqlalchemy.orm import Session, foreign, relationship, backref
from sqlalchemy.ext.associationproxy import association_proxy
class AddressAssociation(Base):
"""Associates a collection of Address objects
with a particular parent.
"""
__tablename__ = "address_association"
-
- discriminator = Column(String)
+ addr_id = Column(Integer,
+ primary_key=True,
+ )
+ order_id = Column(Integer,
+ primary_key=True,
+ )
+ discriminator = Column(String, primary_key=True)
"""Refers to the type of parent."""
__mapper_args__ = {"polymorphic_on": discriminator}
class Address(Base):
"""The Address class.
This represents all address records in a
single table.
"""
- association_id = Column(Integer, ForeignKey("address_association.id"))
+ id = Column(Integer, primary_key=True)
street = Column(String)
city = Column(String)
zip = Column(String)
- association = relationship("AddressAssociation", backref="addresses")
-
- parent = association_proxy("association", "parent")
def __repr__(self):
return "%s(street=%r, city=%r, zip=%r)" % \
(self.__class__.__name__, self.street,
self.city, self.zip)
class HasAddresses(object):
"""HasAddresses mixin, creates a relationship to
the address_association table for each parent.
"""
@declared_attr
- def address_association_id(cls):
- return Column(Integer, ForeignKey("address_association.id"))
-
- @declared_attr
def address_association(cls):
name = cls.__name__
discriminator = name.lower()
assoc_cls = type(
"%sAddressAssociation" % name,
(AddressAssociation, ),
dict(
__tablename__=None,
__mapper_args__={
"polymorphic_identity": discriminator
- }
- )
+ },
+ addresses=relationship(
+ Address,
+ primaryjoin="Address.idforeign({assoc_cls_name}.addr_id)".format(assoc_cls_name=assoc_cls_name))
+ )
)
cls.addresses = association_proxy(
"address_association", "addresses",
creator=lambda addresses: assoc_cls(addresses=addresses)
)
return relationship(assoc_cls,
- backref=backref("parent", uselist=False))
+ primaryjoin=and_(foreign(assoc_cls.addr_id) Address.id,
+ foreign(assoc_cls.order_id) == cls.id),
+ cascade="save-update, merge, delete, delete-orphan",
+ )
class Customer(HasAddresses, Base):
name = Column(String)
class Supplier(HasAddresses, Base):
company_name = Column(String)
此后就可以通过Customer.addresses.append等操作M2M了
相关文章
- Docker网络配置进阶
- 11-机器学习进阶-元学习(上)MAML(5-9)
- Java进阶 | IO流核心模块与基本原理
- 【CSS进阶】伪元素的妙用--单标签之美
- 【CSS3进阶】酷炫的3D旋转透视
- 数仓工具—Hive进阶之索引(1)
- 课时 25:Kubernetes 网络模型进阶(叶磊)
- JS高级--函数进阶(原型、调用方法、this、bind、严格模式、闭包、垃圾回收、递归深拷贝、匿名函数、回调函数、立即执行函数)
- Java进阶面试题大集合-offer不再是问题
- 推荐 | Vue 入门&进阶路线
- 《Android开发进阶:从小工到专家》——第1章,第1.4节ContentProvider(外共享数据)
- 《Android开发进阶:从小工到专家》——第2章,第2.3节Scroller的使用
- 《Hadoop MapReduce实战手册》一第2章 HDFS进阶
- 【Unity3D进阶4-12】Unity3D 对话系统
- Unity 进阶 之 简单虚拟摇杆joystick的实现,并对应简单控制物体移动和旋转的整理(简单适配到Vuforia 识别放置场景使用)
- Unity AssetBundle 之 (进阶)简单的实现不同平台给 AB 标签的资源打包 Asset Bundle 包(分平台)的方法