在 SQLAlchemy 中处理两个 rows/objects 之间的多重关系

Handle multiple relations between two rows/objects in SQLAlchemy

我在使用 Python3 和 SQLAlchemy 连接的 sqlite 数据库的应用程序中发现了这个问题。我从面向对象开发人员的角度设计了数据结构。我认为这是我的问题之一。 ;)

简单说明: 实体 a(来自 table/class A)可以多次引用实体 b(来自 table/class B)。

样本:

CREATE TABLE "B" (
        oid INTEGER NOT NULL,
        val INTEGER,
        PRIMARY KEY (oid)
);
CREATE TABLE "A" (
        oid INTEGER NOT NULL,
        PRIMARY KEY (oid)
);
CREATE TABLE a_b_relation (
        a_oid INTEGER,
        b_oid INTEGER,
        FOREIGN KEY(a_oid) REFERENCES "A" (oid),
        FOREIGN KEY(b_oid) REFERENCES "B" (oid)

sqlite> SELECT oid FROM A;
1
sqlite> SELECT oid, val FROM B;
1|0
2|1
sqlite> SELECT a_oid, b_oid FROM a_b_relation;
1|1
1|1

你在这里看到 a 有两个对同一个 entity/row b 的引用。 在我的数据结构中,这是有道理的。但也许它会破坏 SQL-/RDBMS-rule?

当我在 Python3 中对对象执行此操作时,它会导致错误,因为 SQLAlchemy 尝试为此执行 DELETE 语句。

a._bbb.clear()

错误

DELETE FROM a_b_relation WHERE a_b_relation.a_oid = ? AND a_b_relation.b_oid = ?
(1, 1)
ROLLBACK
sqlalchemy.orm.exc.StaleDataError: DELETE statement on table 'a_b_relation' expected to delete 1 row(s); Only 2 were matched.

在那种情况下,这个错误对我来说是有意义的。但我不知道该如何处理。

在我看来,它看起来像 SQLAlchemy 中的 "bug",因为构造的 DELETE 语句不处理我的用例。但我当然知道,尤其是 SQLA 级开发人员会考虑他们所做的事情,而且这种行为有很好的设计理由。

下面是创建和填充示例数据库以呈现此问题的代码:

#!/usr/bin/env python3

import os.path
import os
import sqlalchemy as sa 
import sqlalchemy.orm as sao
import sqlalchemy.ext.declarative as sad
from sqlalchemy_utils import create_database

_Base = sad.declarative_base()
session = None

a_b_relation= sa.Table('a_b_relation', _Base.metadata,
    sa.Column('a_oid', sa.Integer, sa.ForeignKey('A.oid')),
    sa.Column('b_oid', sa.Integer, sa.ForeignKey('B.oid'))
)


class A(_Base):
    __tablename__ = 'A'

    _oid = sa.Column('oid', sa.Integer, primary_key=True)
    _bbb = sao.relationship('B', secondary=a_b_relation)

    def __str__(self):
        s = '{}.{} oid={}'.format(type(self), id(self), self._oid)
        for b in self._bbb:
            s += '\n\t{}'.format(b)
        return s


class B(_Base):
    __tablename__ = 'B'

    _oid = sa.Column('oid', sa.Integer, primary_key=True)
    _val = sa.Column('val', sa.Integer)

    def __str__(self):
        return '{}.{} oid={} val={}'.format(type(self), id(self), self._oid, self._val)


dbfile = 'set.db'

def _create_database():
    if os.path.exists(dbfile):
        os.remove(dbfile)

    engine = sa.create_engine('sqlite:///{}'.format(dbfile), echo=True)
    create_database(engine.url)
    _Base.metadata.create_all(engine)
    return sao.sessionmaker(bind=engine)()

def _fill_database():
    a = A()
    session.add(a)

    for v in range(2):
        b = B()
        b._val = v
        session.add(b)
        if v == 0:
            # THIS CAUSE THE PROBLEM I THINK
            a._bbb += [b]
            a._bbb += [b]

    session.commit()


if __name__ == '__main__':
    session = _create_database()
    _fill_database()

    a = session.query(A).first()
    a._bbb.clear()
    session.commit()

也许它有点过头了,但我也会描述原始数据结构。这是关于对健身房进行使用统计。 :)

您走到机器前并举起几公斤几次 - 这称为 TrainingUnit(相当于示例中的 A)。 对于热身,您重复 12 次,每次重复 35 公斤 - 这称为 SetSet(相当于示例中的 B;请记住,Set 是预留的SQL-关键字)。 然后你休息一分钟,用 40 公斤重复 12 次——第二组。然后你再次做第三次(!),重复 12 次,再次 40 公斤。 所以 TrainingUnit instance/row 需要三个关系到 SetSet 的 instances/rows。因为第二组和第三组具有相同的 values/settings,所以我将在此处使用与 SetSet 相同的 instance/row。

大部分时间,运动员每天都以相同的配置进行 TrainingUnits - 这意味着每天的训练集都具有相同的值。这就是为什么我想重用 SetSet 的 instances/rows。 每个 TrainingUnit 的组数不固定 - 这取决于运动员 he/she 会做多少组。

我的解决方案取决于该资源。感谢您的帮助!

我使用示例代码来描述解决方案。

SQL中的架构...

CREATE TABLE "A" (
        oid INTEGER NOT NULL,
        PRIMARY KEY (oid)
);
CREATE TABLE "B_Val" (
        oid INTEGER NOT NULL,
        val INTEGER,
        PRIMARY KEY (oid)
);
CREATE TABLE "B" (
        oid INTEGER NOT NULL,
        b_val_fk INTEGER,
        PRIMARY KEY (oid),
        FOREIGN KEY(b_val_fk) REFERENCES "B_Val" (oid)
);
CREATE TABLE a_b_relation (
        a_oid INTEGER,
        b_oid INTEGER,
        FOREIGN KEY(a_oid) REFERENCES "A" (oid),
        FOREIGN KEY(b_oid) REFERENCES "B" (oid)
);

...和SQL炼金术

a_b_relation= sa.Table('a_b_relation', _Base.metadata,
    sa.Column('a_oid', sa.Integer, sa.ForeignKey('A.oid')),
    sa.Column('b_oid', sa.Integer, sa.ForeignKey('B.oid'))
)


class A(_Base):
    __tablename__ = 'A'

    _oid = sa.Column('oid', sa.Integer, primary_key=True)
    _bbb = sao.relationship('B', secondary=a_b_relation)


class B_Val(_Base):
    __tablename__ = 'B_Val'
    _oid = sa.Column('oid', sa.Integer, primary_key=True)
    _val = sa.Column('val', sa.Integer)

    def __init__(self, val):
        self._val = val


class B(_Base):
    __tablename__ = 'B'

    _oid = sa.Column('oid', sa.Integer, primary_key=True)
    _b_val_fk = sa.Column('b_val_fk', sa.Integer, sa.ForeignKey('B_Val.oid'))
    _b_val = sao.relationship('B_Val')

    def __init__(self, b_val):
        self._b_val = b_val