在 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 会做多少组。
我的解决方案取决于该资源。感谢您的帮助!
- Database normalization(维基百科)
- Handle multiple relations between two rows/objects(sqlalchemy-邮件列表)
- How do I map a table that has no primary key? (sqlalchemy-docu)
我使用示例代码来描述解决方案。
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
我在使用 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 会做多少组。
我的解决方案取决于该资源。感谢您的帮助!
- Database normalization(维基百科)
- Handle multiple relations between two rows/objects(sqlalchemy-邮件列表)
- How do I map a table that has no primary key? (sqlalchemy-docu)
我使用示例代码来描述解决方案。
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