我想在插入或更新“间隔”时保持不变性。使用SQLAlchemy的多对多关系。 目标是确保表中没有重叠的间隔表示同一对象的间隔(A)。
例如:
class A(Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
intervals = relationship('Interval', backref='a', cascade='all, delete-orphan')
class B(Base):
__tablename__ = 'b'
id = Column(Integer, primary_key=True)
intervals = relationship('Interval', backref='b', cascade='all, delete-orphan')
class Interval(Base):
__tablename__ = 'interval'
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey('a.id', ondelete='cascade'), nullable=False)
b_id = Column(Integer, ForeignKey('b.id', ondelete='cascade'), nullable=False)
start = Column(DateTime, nullable=False)
end = Column(DateTime, nullable=False)
根据范的说明更新(谢谢):
所以我想在插入和更新之前/期间确保“'间隔”中没有记录。表格
((interval.start >= start and interval.start <= end) or
(interval.end >= start and interval.end <= end) or
(start >= interval.start and start <= interval.end) or
(end >= interval.start and end <= interval.end)) and
a_id = interval.a_id
所以问题是使用MySQL后端使用SQLAlchemy实现这一目标的最佳方法。 我想确保这个检查尽可能原子化,并且不可能通过并发操作来违反它。 我希望这个问题很清楚。感谢您的帮助。
更新1:
回答 van 的问题:
验证失败时您的工作流程是什么?
对于未通过验证且未添加到?
的间隔,您将如何处理?
请注意,它们仍将提交给DB,因为它们属于b1但是a_id = NULL(在当前模型中允许使用NULL)
最终用户应该看到什么? 交易是否应该全部承诺? 你是否乐意在try / except?
中包装任何a.intervals.add(...)
这就是我想象的添加间隔:
1。)表单验证发生,我们知道其他字段已经验证失败(间隔不会添加到会话中),我们想检查间隔是否也未通过验证:
### Start request
# Prevalidation
Interval.check_valid(a, start, end)
...
#### End request
2.。)所有表单字段都通过验证,我们想检查如果提交间隔是否有效(可能不需要),然后提交间隔:
# Start request
# Basic validation at the time of addition
try:
interval = Interval(a, b, start, end)
except SomeException:
return("This interval overlaps another, please fix this!")
...
# Main validation when the interval relation is committed to database
try:
session.flush() # commit the session to the database
except AnotherException: # maybe IntegrityError or something similar
return("This interval overlaps another, please fix this!")
### End request
我不确定是否真的需要基本验证。 需要预验证,因为如果某人POST表单并且我们看到其他字段有一些错误,我们希望获得响应中列出的所有无效字段。 这样做的原因是我不希望用户多次发布表单,并且一次又一次地回到他身边并且出现更新的错误,但是我想立刻列出所有错误,我想知道这些错误。能够检查。
查看解决方案 van ,前提是它确实进行了基本验证,但可能存在一些可能导致问题的边缘情况。 这是一个Web应用程序,因此两个不同的用户可能正在使用不同的会话,因此当执行i1 = Interval(a1,b1,start1,end1)时,i1不会出现在其他会话中,直到从数据库刷新注册表为止会话。 (至少这是我认为它是如何工作的。)
这里列出的情况是可能还是我误解了什么?
我还在考虑将UPDATE和INSERT触发器添加到&#39; interval&#39;表做这个检查。 我不确定这是否是正确的方法,如果这提供了原子性,将确保上面列出的并发尝试不会导致问题。 如果这是正确的方法,我的问题将是在调用Base.metedata.create_all()时创建这些触发器的正确方法。 这是执行此操作的唯一方法,还是有可能以某种方式将此附加到模型并让create_all()创建它: DELIMITER / Creating a trigger in SQLAlchemy
更新2:
检查间隔是否发生碰撞的正确算法似乎是:
interval.start <= end and start <= interval.end
我发现确保适当原子性的正确方法是在检查重叠的方法中使用select for update。 这至少适用于MySQL。
答案 0 :(得分:1)
首先:您的检查不足以验证重叠,因为它不包括另一个间隔完全包含的情况。请参阅以下验证码中的检查。
然后:这不是对sqlalchemy方面做的直截了当的检查。首先,请查看Simple Validators文档。您的实现可能如下所示:
class A(Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
@validates('intervals', include_backrefs=True, include_removes=False)
def validate_overlap(self, key, interval):
assert key == 'intervals'
# if already in a collection, ski the validation
# this might happen if same Interval was added multiple times
if interval in self.intervals:
return interval
# assert that no other interval overlaps
overlaps = [i for i in self.intervals
if ((i.start >= interval.start and i.start <= interval.end) or
(i.end >= interval.start and i.end <= interval.end) or
(i.start <= interval.start and i.end >= interval.start) or
(i.start <= interval.end and i.end >= interval.end)
)
]
assert not(overlaps), "Interval overlaps with: {}".format(overlaps)
return interval
现在这样的示例代码应该可以工作,其中“work”我的意思是“当添加重叠的间隔时,验证代码运行并抛出异常”:
session.expunge_all()
a1, b1 = _query_session_somehow(...) # assume that a1 has no intervals yet
i1 = Interval(b=b1, start=date(2013, 1, 1), end=date(2013, 1, 31))
a1.intervals.append(i1)
i2 = Interval(b=b1, start=date(2013, 2, 1), end=date(2013, 2, 28))
a1.intervals.append(i2)
i3 = Interval(b=b1, start=date(2013, 2, 8), end=date(2013, 2, 19))
try:
a1.intervals.append(i3)
except Exception as exc:
print "ERROR", exc
session.commit()
assert 2 == len(a1.intervals)
您应该注意的是同一文档页面中的以下注释。 逐字:
请注意,集合的验证程序无法发出该集合的负载 验证例程中的集合 - 这种用法提出了一个 断言以避免递归溢出。这是一个可重入的条件 这是不受支持的。
因此,如果我们稍微修改一下使用代码并首先添加关系的另一面,那么您应该期望sqlalchemy异常AssertionError: Collection was loaded during event handling.
,并且由于此限制,代码将无效:
session.expunge_all()
a1, b1 = _query_session_somehow(...)
# a1.intervals # @note: uncomment to make sure the collection is loaded
i1 = Interval(a=a1, b=b1, start=date(2013, 1, 1), end=date(2013, 1, 31))
我们在这里做的是我们首先添加了关系的另一面,但是我们的验证器需要加载intervals
集合,这会导致在a1.intervals
不是a.intervals
时触发限制首先加载。
通过确保始终加载Interval
,可以解决这个问题。取消注释上面代码段中的注释行应该可以使它工作。
由于这个实现有一些棘手的问题,你可以改为考虑Session Events。
但鉴于您已通过验证:
a
的{{1}},您会怎么做?
b1
但具有a_id = NULL
(您在当前模型中允许使用NULL)a.intervals.add(...)
包裹在try/except
?