SQLAlchemy 多对多关系 Association Proxy

上一篇:Python ORM - SQLAlchemy (Draft) - //www.greatytc.com/p/adb09ff73a2e

在定义多对多(Many to Many)模型时,上一篇文章里说,中间表只用能db.Table定义。
这会导致很多Session Model的操作不可用,而且中间表中添加和查询额外字段也很麻烦。
其实,较新的SQLAlchemy版本,已经支持中间表格用模型定义了,这就是:Association Proxy

image.png

举例:

某个网站系统,其中两张表

  • 一张表是用户信息User,每个用户可以有多种标签Keywords
  • 另一张表是标签信息Keyword,每类标签,可以标注给多个用户
    那很显然是Many to Many,需要另外定义一张中间表。

如果我们想在中间表上添加额外字段,那就可以定义:Association Objects

模型定义

引入sqlalchemy模块

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, backref

from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func

from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=False)
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
ss = Session()
Base = declarative_base()

定义User Model
注意关联Keyword Model,使用association_proxy

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String(64))

    # association proxy of "user_keywords" collection
    # to "keyword" attribute
    keywords = association_proxy('user_keywords', 'keyword') # 中间表和Many2Many表的名字

    def __init__(self, name):
        self.name = name

第二张表:Keyword,注意,其中某个字段,要加跟表名一样的label!比如,keyword字段

class Keyword(Base):
    __tablename__ = 'keyword'
    id = Column(Integer, primary_key=True)
    keyword = Column('keyword', String(64)) # 要加跟表名一样的标签

    def __init__(self, keyword):
        self.keyword = keyword

    def __repr__(self):
        return 'Keyword(%s)' % repr(self.keyword)

中间表,用来关联User/Keyword两张表:

  • 两个字段,是ForeignKey类型
  • user字段,使用backref,方便直接调用:user1.user_keywords
  • 你可以添加任意多的额外字段,比如special_key
class UserKeyword(Base):
    __tablename__ = 'user_keyword'
    user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
    keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
    special_key = Column(String(50))

    # bidirectional attribute/collection of "user"/"user_keywords"
    user = relationship(User,
                backref=backref("user_keywords",
                                cascade="all, delete-orphan")
            )

    # reference to the "Keyword" object
    keyword = relationship("Keyword")

    def __init__(self, keyword=None, user=None, special_key=None):
        self.user = user
        self.keyword = keyword
        self.special_key = special_key
    def __repr__(self):
        return f'User({self.user_id})-Keyword({self.keyword_id })_self.special_key '

好了,定义完成,生成表格吧

Base.metadata.create_all(engine)   

session操作

UserKeyword这个中间表,可以使用session的任意命令来操作

  1. 新建几个User和Keyword
ss.add_all([
    Keyword('shadow'), Keyword('LOR1'), Keyword('WWII'),
    User('Kevin'), User('Olivia'),
])
ss.add_all([
    UserKeyword(user=ss.query(User).get(1), keyword=ss.query(Keyword).get(3), special_key ='abc'),
    UserOb(user=ss.query(User).get(2), keyword=ss.query(Keyword).get(3), special_key ='efg'),
    UserOb(user=ss.query(User).get(2), keyword=ss.query(Keyword).get(2), special_key ='cde'),
])

检查一下是否创建成功:

ss.query(UserKeyword).all()
# [User(1)-Keyword(3)_abc, User(2)-Keyword(3)_efg, User(2)-Keyword(2)_efg]

试一下强大的Session Query!
比如,我们想查询一下,各类Keyword的使用次数:

stmt = ss.query(UserKeyword.keyword_id, func.count('*').label('kw_count')).\
    group_by(UserKeyword.keyword_id).subquery()
for kw, count in ss.query(Keyword, stmt.c.kw_count).\
     outerjoin(stmt, Keyword.id==stmt.c.keyword_id).order_by(Keyword.id):
     print(kw, 'kw_count:', count)

#Keyword(shadow) kw_count: None
#Keyword(LOR1) kw_count: 1
#Keyword(WWII) kw_count: 2

比如,想根据额外字段special_key是否为abc来查询:

stmt = ss.query(UserKeyword.keyword_id, func.count(UserKeyword.special_key).label('status_count')).\
    filter(UserKeyword.special_key=='abc').group_by(UserKeyword.ob_id).subquery()
for kw, count in ss.query(Keyword, stmt.c.status_count).\
     outerjoin(stmt, Keyword.id==stmt.c.keyword_id).order_by(Keyword.id):
     print(kw, 'status_count:', count)
print('Filtered:')
for kw, count in ss.query(Keyword, stmt.c.status_count).\
     filter(stmt.c.status_count>=1).outerjoin(stmt, Keyword.id==stmt.c.keyword_id).order_by(Keyword.id):
     print(kw, 'status_count:', count)

Reference: https://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容