【问题标题】:How to build backref with both associatition object and secondaryjoin?如何使用关联对象和辅助连接构建反向引用?
【发布时间】:2013-03-08 17:59:56
【问题描述】:

我需要一些模型,例如:

  • 工作 - 例如。文学作品。
  • 工人 - 例如作曲家、翻译或类似的人对工作有贡献。

因此,需要'type' 字段来按分工区分工人。作为 SQLAlchemy 的文档,这种情况可以从 association object 中受益,如下所示:

class Work(base):
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    description = Column(Text)

class Worker(base):
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    description = Column(Text)

class Assignment(base):
    work_id = Column(Integer, Foreignkey('work.id'), primary_key=True)
    worker_id = Column(Integer, Foreignkey('worker.id'), primary_key=True)
    type = Column(SmallInteger, nullable=True)

但是,如何利用backrefalternatvie join condition立即建立关系,实现每个Work对象可以检索和修改对应的Worker( s) 通过不同的属性进行区分。例如:

work = session.query(Work).get(1)
work.name
>>> 'A Dream of The Red Mansions'
work.composers
>>> [<Worker('Xueqin Cao')>]
work.translators
>>> [<Worker('Xianyi Yang')>, <Worker('Naidie Dai')>]

反之亦然:

worker = session.query(Worker).get(1)
worker.name
>>> 'Xueqin Cao'
worker.composed
>>> [<Work('A Dream of The Red Mansions')>]
worker.translated
>>> []

在不指定secondary 的情况下直接添加secondaryjoin 似乎不可行,此外,SQLAlchemy 的文档指出:

当使用关联对象模式时,建议不要将关联映射表用作其他地方的 relationship() 的辅助参数,除非该 relationship() 包含选项 viewonly=True。如果在相关属性以及相关对象上检测到类似状态,SQLAlchemy 可能会尝试在同一个表上发出冗余的 INSERT 和 DELETE 语句。

那么,有没有什么方法可以优雅而轻松地建立这些关系呢?

【问题讨论】:

    标签: sqlalchemy


    【解决方案1】:

    这里有三种一般的方法。

    一个是,做一个“香草”设置,你有“工作”/“工人”设置而不区分“类型” - 然后,使用 relationship() 为“作曲家”,“作曲家”,“翻译者”,通过使用“辅助”到Assignment.__table__ 以及自定义连接条件以及viewonly=True 来“翻译”。因此,您只能通过 vanilla 属性进行写入。这里的一个缺点是“vanilla”和“specific”集合之间没有立即同步。

    另一个是,与“vanilla”设置相同,但只是使用普通的 Python 描述符在内存中给出“composer”、“composed”、“translator”、“translated”视图,即[obj.worker for obj in self.workers if obj.type == 'composer']。这是最简单的方法。无论您放入“vanilla”集合中的什么内容都会直接显示在“filtered”集合中,SQL 很简单,并且执行的 SELECT 语句更少(每个 Worker/Work 一个,而不是每个 Worker/Work N 个)。

    最后,使用主连接和反向引用最接近您所要求的方法,但请注意关联对象,反向引用位于 Work/Assignment 和 Assignment/Worker 之间,而不是直接在 Work/Worker 之间。这种方法可能最终会使用更多的 SQL 来获得结果,但它是最完整的,并且还具有自动编写“类型”的漂亮特性。我们还使用了“单向反向引用”,因为 Assignment 没有一种简单的向外关联的方法(有办法做到这一点,但会很乏味)。使用 Python 函数自动创建关系可以减少样板文件,注意这里我使用字符串作为“类型”,如果您向系统添加更多参数,这可以是一个整数:

    from sqlalchemy import *
    from sqlalchemy.orm import *
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.ext.associationproxy import association_proxy
    
    Base = declarative_base()
    
    def _work_assignment(name):
        assign_ = relationship("Assignment",
                        primaryjoin="and_(Assignment.work_id==Work.id, "
                                        "Assignment.type=='%s')" % name,
                        back_populates="work", cascade="all, delete-orphan")
        assoc = association_proxy("%s_assign" % name, "worker",
                        creator=lambda worker: Assignment(worker=worker, type=name))
        return assoc, assign_
    
    def _worker_assignment(name):
        assign_ = relationship("Assignment",
                        primaryjoin="and_(Assignment.worker_id==Worker.id, "
                                        "Assignment.type=='%s')" % name,
                        back_populates="worker", cascade="all, delete-orphan")
        assoc = association_proxy("%s_assign" % name, "work",
                        creator=lambda work: Assignment(work=work, type=name))
        return assoc, assign_
    
    class Work(Base):
        __tablename__ = 'work'
        id = Column(Integer, primary_key=True)
        name = Column(String(50))
        description = Column(Text)
    
        composers, composer_assign = _work_assignment("composer")
        translators, translator_assign = _work_assignment("translator")
    
    class Worker(Base):
        __tablename__ = 'worker'
        id = Column(Integer, primary_key=True)
        name = Column(String(50))
        description = Column(Text)
    
        composed, composer_assign = _worker_assignment("composer")
        translated, translator_assign = _worker_assignment("translator")
    
    class Assignment(Base):
        __tablename__ = 'assignment'
        work_id = Column(Integer, ForeignKey('work.id'), primary_key=True)
        worker_id = Column(Integer, ForeignKey('worker.id'), primary_key=True)
        type = Column(String, nullable=False)
    
        worker = relationship("Worker")
        work = relationship("Work")
    
    e = create_engine("sqlite://", echo=True)
    Base.metadata.create_all(e)
    
    
    session = Session(e)
    
    ww1, ww2, ww3 = Worker(name='Xueqin Cao'), Worker(name='Xianyi Yang'), Worker(name='Naidie Dai')
    
    w1 = Work(name='A Dream of The Red Mansions')
    w1.composers.append(ww1)
    w1.translators.extend([ww2, ww3])
    
    session.add(w1)
    session.commit()
    
    work = session.query(Work).get(1)
    assert work.name == 'A Dream of The Red Mansions'
    assert work.composers == [ww1]
    assert work.translators == [ww2, ww3]
    
    worker = session.query(Worker).get(ww1.id)
    assert worker.name == 'Xueqin Cao'
    assert worker.composed == [work]
    assert worker.translated == []
    
    worker.composed[:] = []
    
    # either do this...
    session.expire(work, ['composer_assign'])
    
    # or this....basically need composer_assign to reload
    # session.commit()
    
    assert work.composers == []
    

    【讨论】:

    • 非常感谢。此外,它似乎正在失去惰性加载程序的好处,并且无法通过关联代理从一个Worker 的分配中删除Work。如何克服或改善这种情况?
    • ive 对其进行了更新,以支持通过删除项目来删除作业。但它仍然需要另一方过期/刷新才能显示新数据。
    猜你喜欢
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 2012-07-11
    • 2016-12-10
    • 2021-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多