SQLAlchemy插入,更新前置条件(一致性检查)

时间:2014-01-17 17:50:46

标签: python mysql sqlalchemy

我想在插入或更新“间隔”时保持不变性。使用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)

  • 这些不应该被提交,并且不应该是a_id或b_id的NULL值。我更新了模型以反映这一点。
  • 那可能是NULL但是这对我来说似乎没有意义,因为a_id = 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不会出现在其他会话中,直到从数据库刷新注册表为止会话。 (至少这是我认为它是如何工作的。)

  1. 用户尝试添加不与数据库中任何间隔重叠的间隔(i1)。间隔(a1,b1,start1,end1)
  2. 用户尝试添加一个间隔(i2),该间隔不与数据库中的任何间隔重叠,但与i1重叠。间隔(a1,b2,start2,end2)
  3. 两个验证都可能成功,因为两个用户可能正在使用不同的会话。
  4. 会话i1属于flush() - ed并且没有错误地提交给数据库。
  5. 会话i2属于flush() - ed并且没有错误地提交给数据库。
  6. 这里列出的情况是可能还是我误解了什么?

    我还在考虑将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。

1 个答案:

答案 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