標籤:

SQLAlchemy 多態進階

手機里隨便找了一張照片做題圖( less 怎麼能is more呢...less is better then more...

最近在做模型設計的重構,之前的M2O的設計有些不符合需求了,平添了很多工作量,產品最初的設計和後來改來改去的需求明顯產生了矛盾,重構成M2M的過程中,學到了很多SQLAlchemy的知識,因此在這裡記錄一下

什麼是polymorphic

熟悉SQLAlchemy的人往往知道polymorphic(多態)的模型定義,如果你恰好不熟悉SQLAlchemy,這裡簡單的舉一個例子:

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

這裡定義了僱員Employee 模型,指定type欄位為多態所在欄位,並且對於這個模型,當type欄位為employee時,即為一個僱員

再看下面這兩個繼承之後的模型

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

這裡又定義了兩個模型,Engineer,Manager,

並對應了兩張表,這兩張表的結構除了有少許不同,類似的,polymorphic_identity指定了這兩種模型對應的type欄位值,

在上面的基礎上,可以提出的問題:

  1. 可不可以完全在一張表上實現這樣的多態?
  2. 這樣的模型可以用作M2M關係嗎?

兩者的答案顯然是肯定的。 對於第一個問題,只需要使得後兩者的__tablename__ = None,並且不指定額外的欄位即可。

第二個問題,即這幾天我的重構的探索

如何設置多對多模型

對於一個多對多的關係表,按照SQLAlchemy文檔:

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

(雖然我們的基本不會按照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 njoin right on association.right_id=right.id n

是藉助association實現兩個表的JOIN關係

SQLAlchemy 的對應操作這裡就不贅述了,大家看文檔吧咩哈哈

M2M和多態

此次重構遇到的問題就是:如果我們的M2M的關係,如果是在多態上進行的,例如上面的Child,如果我不僅僅有Child,還分Boy和Girl,如何在這一張association_table進行控制呢? 上面代碼先稍作修改:

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

增加了gender欄位,並且增加了多態聲明__mapper_args__ 我們先假設一下這樣的SQL該怎麼寫吧,實際上是很簡單的哈:

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

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如下(稍微修改了欄位名),稍後有完整代碼:

--- origin.py 2016-10-13 11:28:57.000000000 +0800 n+++ target.py 2016-10-13 11:29:44.000000000 +0800 n@@ -1,80 +1,84 @@ n from sqlalchemy.ext.declarative import as_declarative, declared_attrn from sqlalchemy import create_engine, Integer, Column, n- String, ForeignKey n-from sqlalchemy.orm import Session, relationship, backref n+ String, and_ n+from sqlalchemy.orm import Session, foreign, relationship, backref n from sqlalchemy.ext.associationproxy import association_proxynnn class AddressAssociation(Base):n """Associates a collection of Address objectsn with a particular parent.nn """n __tablename__ = "address_association"n- n- discriminator = Column(String) n+ addr_id = Column(Integer, n+ primary_key=True, n+ ) n+ order_id = Column(Integer, n+ primary_key=True, n+ ) n+ discriminator = Column(String, primary_key=True) n """Refers to the type of parent."""nn __mapper_args__ = {"polymorphic_on": discriminator}nnn class Address(Base):n """The Address class.nn This represents all address records in an single table.nn """n- association_id = Column(Integer, ForeignKey("address_association.id")) n+ id = Column(Integer, primary_key=True) n street = Column(String)n city = Column(String)n zip = Column(String)n- association = relationship("AddressAssociation", backref="addresses") n- n- parent = association_proxy("association", "parent")nn def __repr__(self):n return "%s(street=%r, city=%r, zip=%r)" % n (self.__class__.__name__, self.street,n self.city, self.zip)nnn class HasAddresses(object):n """HasAddresses mixin, creates a relationship ton the address_association table for each parent.nn """n @declared_attrn- def address_association_id(cls): n- return Column(Integer, ForeignKey("address_association.id")) n- n- @declared_attr n def address_association(cls):n name = cls.__name__n discriminator = name.lower()nn assoc_cls = type(n "%sAddressAssociation" % name,n (AddressAssociation, ),n dict(n __tablename__=None,n __mapper_args__={n "polymorphic_identity": discriminatorn- } n- ) n+ }, n+ addresses=relationship( n+ Address, n+ primaryjoin="Address.idforeign({assoc_cls_name}.addr_id)".format(assoc_cls_name=assoc_cls_name)) n+ ) n )nn cls.addresses = association_proxy(n "address_association", "addresses",n creator=lambda addresses: assoc_cls(addresses=addresses)n )n return relationship(assoc_cls,n- backref=backref("parent", uselist=False)) n+ primaryjoin=and_(foreign(assoc_cls.addr_id) Address.id, n+ foreign(assoc_cls.order_id) == cls.id), n+ cascade="save-update, merge, delete, delete-orphan", n+ )nnn class Customer(HasAddresses, Base):n name = Column(String)nnn class Supplier(HasAddresses, Base):n company_name = Column(String)n

此後就可以通過Customer.addresses.append等操作M2M了

(哦...知乎不支持diff的code?

最後的感覺就是:在對M2M進行操作的時候, 很符合直覺,但是實際上的模型定義,晦澀難懂....

可以掀桌了好把


推薦閱讀:

TAG:Python | SQLAlchemy |