2014-01-17 2 views
1

Я хотел бы сохранить инвариант при вставке или обновлении отношения «многие-ко-многим» с использованием SQLAlchemy. Цель состоит в том, чтобы в таблице не было перекрывающихся интервалов, которые представляют интервалы для одного и того же объекта (A).Вставка SQLAlchemy, предварительное условие обновления (проверка согласованности)

Например:

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 

Так что вопрос будет то, что лучший способ добиться этого с помощью SQLAlchemy бэкэнд MySQL. Я хочу, чтобы эта проверка была как можно более атомной, и нет возможности нарушить ее с одновременными операциями. Надеюсь, вопрос ясен. Спасибо за помощь заранее.

Update 1:

Ответы на вопросы ван «s:

Что ваш рабочий процесс в случае сбоя проверки?

  • Пользователь должен получить сообщение об ошибке, что интервал, который пользователь попытался добавить перекрывается уже существующий интервал.
  • Пользователь может также получить список интервалов, которые сталкиваются с тем, который пользователь пытался добавить.

Что вы будете делать с теми интервалами, которые прошли проверку и не были добавлены к?

  • См. Ответ на вопрос рабочего процесса.
  • Так что, если проверка не выполняется, если кто-то пытается добавить перекрывающий интервал, должно произойти исключение, и пользователь должен получить сообщение об ошибке, что это недопустимо.
  • Интервалы, которые не были добавлены, не должны быть привязаны к базе данных.

отмечает, что они будут по-прежнему стремятся к БД, потому что они принадлежат к b1, но a_id = NULL (вы разрешите NULLs в текущей модели)

  • Они не должны быть и не должно быть значений NULL для a_id или b_id. Я обновил модель, чтобы отразить это.
  • То есть это может быть NULL, но для меня это не имеет смысла, поскольку записи с a_id = NULL должны были быть очищены каким-то образом.

Что должен видеть конечный пользователь? Должна ли быть совершена сделка и т. Д.? Вы счастливы обернуть любой a.intervals.add (...) в try/except?

  • Пользователь должен видеть сообщение об ошибке, когда это ограничение нарушается.
  • Сделку ни в коем случае нельзя совершать.
  • Попробуйте, кроме упаковки, хорошо.

Это, как я полагаю, добавив интервал:

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 

Я не уверен, действительно ли требуется базовая проверка. Предварительное подтверждение необходимо, потому что, если кто-то выставляет форму, и мы видим некоторые ошибки для других полей, мы хотели бы получить все недопустимые поля, перечисленные в ответе. Причина этого заключается в том, что я не хочу, чтобы пользователь несколько раз публиковал форму, возвращаясь к нему снова и снова с более новыми ошибками, но я хотел бы сразу указать все ошибки, которые я могу проверить ,

Глядя на решение van при условии, что он выполняет основную проверку, но могут быть случаи с краем, где это может вызвать проблемы. Это веб-приложение, поэтому возможно, что два разных пользователя используют разные сеансы, поэтому при выполнении i1 = Interval (a1, b1, start1, end1) i1 не отображается в других сеансах, пока реестр не будет обновлен из базы данных в этой сессии. (По крайней мере, это то, что я думаю, как это работает.)

  1. Пользователи пытается добавить интервал (i1), что это не пересекается с другими интервалами в базе данных. Интервал (a1, b1, start1, end1)
  2. Пользователи пытаются добавить интервал (i2), который не перекрывает любые интервалы в базе данных, но перекрывается с i1. Интервал (a1, b2, start2, end2)
  3. Обе проверки могут быть успешными, поскольку оба пользователя могут использовать разные сеансы.
  4. Session i1 принадлежит к flush() - ed и привязан к базе данных без ошибок.
  5. Session i2 принадлежит к flush() - ed и привязан к базе данных без ошибок.

Возможно ли, что здесь ситуация, или я что-то не понимаю?

Я также думал о добавлении триггеров UPDATE и INSERT в таблицу «interval», чтобы выполнить эту проверку. Я не уверен, что это правильный путь, и если это обеспечивает атомарность, которая гарантирует, что параллельные попытки, перечисленные выше, не вызовут проблемы. Если это правильный способ, мой вопрос будет правильным способом для создания этих триггеров при вызове Base.metedata.create_all(). Это единственный способ сделать это, или есть возможность как-то прикрепить это к модели, и пусть create_all() создайте его: DELIMITER/Creating a trigger in SQLAlchemy

Update 2:

Правильный алгоритм для проверки, если интервалы сталкиваются, кажется:

interval.start <= end and start <= interval.end 

Я обнаружил, что правильный подход для обеспечения надлежащей атомарности является просто использовать select for update в способе, в котором проверяется перекрытие. Это работает с MySQL как минимум.

ответ

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 

Теперь пример кода, как это должно работать, где под «работой» я имею в виду «прогонов кода проверки и утверждают, исключение, когда интервал с перекрытиями добавляется»:

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) 

О чем вы должны знать, это следующий комментарий на той же странице документации. дословные:

Обратите внимание, что валидатор для сбора не может вопрос груз этой коллекции в рамках процедуры проверки - это использование поднимает утверждения, чтобы избежать рекурсий переполнения. Это условие возврата , которое не поддерживается.

Так что, если мы должны были изменить код Usage немного и добавить другую сторону отношений первых, вы должны ожидать 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. Разоблачение прокомментированной строки в фрагменте кода выше должно заставить ее работать.

Как есть несколько сложных точек с этой реализацией, вы можете взглянуть на Session Events.

Но учитывая, что у вас есть подтверждение выяснили:

  • Что ваш рабочий процесс в случае сбоя проверки?
  • Что вы будете делать с теми Interval s, которые не прошли проверку и не были добавлены в a?
    • отмечают, что они будут по-прежнему стремится к БД, потому что они принадлежат к b1 но имеют a_id = NULL (вы позволить NULLs в текущей модели)
  • Что должен видеть конечный пользователь?
  • Должна ли быть совершена сделка и т. Д.?
  • Вы счастливы обернуть любой a.intervals.add(...) в try/except?
+0

Спасибо за исчерпывающий ответ. Я ответил на ваши вопросы в исходном вопросе. – ZergRush

+0

Привет, ZergRush. Я рад, что мой ответ о помощи, и я хочу, чтобы я мог помочь вам больше - у меня просто нет времени, так как объем вашего вопроса внезапно увеличивается в 10 раз. Я поставил этот вопрос в конце своего ответа, чтобы вы думаете о своем рабочем процессе, а не как дополнительный вход для меня, чтобы дать вам «окончательное» решение. Пожалуйста, поделитесь своими решениями окончательного решения, так как у вас есть интересная проблема. Приветствия. – van

Смежные вопросы