SQLAlchemy FK ondelete не ОГРАНИЧИВАЕТ

У меня установлены самореферентные отношения. У человека может быть один родитель-одиночка (или Нет), а у человека может быть много детей (или Нет).

Таким образом, NULL допускается в качестве FK:

class Person(db.Model):
    id        = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer, db.ForeignKey('person.id', ondelete='RESTRICT'))
    parent    = db.relationship('Person', remote_side=[id], back_populates='children')
    children  = db.relationship('Person', back_populates='parent')

Однако я хочу запретить удаление человека, если он является родителем. Поэтому я включил пункт ondelete='RESTRICT', но он не действует. Столбец parent_id по-прежнему имеет значение NULL, когда родитель удаляется.

(обратите внимание, что мое соединение с SQLite переключило ограничения внешнего ключа pragma на ON)

Почему база данных не выдает ошибку при удалении родителя и, следовательно, дочерний столбец с ним в качестве внешнего ключа ограничивает это?


person Attack68    schedule 03.05.2019    source источник


Ответы (2)


Ваша настройка ограничения внешнего ключа выглядит правильно, но ваши отношения ORM не имеют явной каскадной конфигурации, поэтому они используют значения по умолчанию save-update и объединить. В этой конфигурации по умолчанию отношение children отменяет связь осиротевших дочерних элементов при удалении родителя путем установки для их внешнего ключа значения NULL. Я думаю, вам следует использовать passive_deletes='all' (см. примечание к delete каскады) в этом случае, чтобы отключить любые каскады уровня ORM при удалении родителя, чтобы база данных могла предотвратить удаление при ее сбросе:

class Person(db.Model):
    id        = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer, db.ForeignKey('person.id', ondelete='RESTRICT'))
    parent    = db.relationship('Person', remote_side=[id], back_populates='children')
    children  = db.relationship('Person', back_populates='parent', passive_deletes='all')
person Ilja Everilä    schedule 03.05.2019
comment
Итак, если столбец внешнего ключа может иметь значение NULL, и вы хотите, чтобы ondelete=RESTRICT вы сказали, что это должно быть установлено в 'all', как правило? - person SuperShoot; 03.05.2019
comment
Спасибо, за это, если я установлю отношение viewonly=True, то это тоже сработает. Есть ли неотъемлемое преимущество использования вашего метода по сравнению с подходом viewonly? - person Attack68; 03.05.2019
comment
Из того, что я могу сказать, если вы явно внесете изменения в отношения с подходом passive_delete, эти изменения все равно будут сброшены. С подходом viewonly даже ваши явные изменения в коллекции отношений не будут сброшены. Так что я полагаю, это зависит от того, как вы собираетесь использовать коллекцию. - person SuperShoot; 03.05.2019
comment
@supershoot Не знаю о правилах, но, с другой стороны, было бы немного странно иметь RESTRICT в базе данных, но совершенно другое поведение в ORM, то есть отменять связь или удалять. Из документов: Кроме того, установка флага в строковое значение «все» отключит «обнуление» дочерних внешних ключей, когда родительский объект удален и каскад удаления или удаления-сироты не включен. Обычно это используется, когда на стороне базы данных имеется сценарий запуска или возникновения ошибки. Наличие RESTRICT похоже на сценарий возникновения ошибки. - person Ilja Everilä; 03.05.2019
comment
До сих пор я не рассматривал взаимодействие между действиями db ondelete и ORM cascades до такой степени, поэтому я благодарен за ваш вклад и вопрос ОП здесь, поскольку теперь мне ясно, что когда это важно, это очень важно . Ваше здоровье. - person SuperShoot; 04.05.2019

Sqlalchemy обнуляет дочерние строки до того, как база данных сможет оценить ограничение внешнего ключа. Если вы добавите passive_deletes=True к связи, sqlalchemy не будет пытаться управлять удалением связанных сущностей, а просто позволит базе данных делать это в зависимости от того, как вы ее настроили. не будет сначала выдавать выберите для заполнения отношения перед удалением родителя. Установка значения True по-прежнему приведет к тому, что дочерние объекты уже находятся в сеансе, а для их столбца FK установлено значение NULL.

Эта конфигурация:

class Person(db.Model):
    id        = db.Column(db.Integer, primary_key=True)
    parent_id = db.Column(db.Integer, db.ForeignKey('person.id', ondelete='RESTRICT'))
    parent    = db.relationship('Person', remote_side=[id], back_populates='children')
    children  = db.relationship('Person', back_populates='parent', passive_deletes=True)


if __name__ == '__main__':
    with app.app_context():
        db.drop_all()
        db.create_all()
        parent = Person()
        db.session.add(parent)
        child = Person(parent=parent)
        db.session.commit()
        db.session.delete(parent)
        db.session.commit()

Поднимает:

sqlalchemy.exc.IntegrityError: (mysql.connector.errors.IntegrityError) 1451 (23000): не удается удалить или обновить родительскую строку: сбой ограничения внешнего ключа (test.person, CONSTRAINT person_ibfk_1 FOREIGN KEY (parent_id) REFERENCES person (id) )

if __name__ == '__main__':
    with app.app_context():
        db.drop_all()
        db.create_all()
        parent = Person()
        db.session.add(parent)
        child = Person(parent=parent)
        db.session.commit()
        db.session.query(Person).all()  # reload the people into the session before deleting parent
        db.session.delete(parent)
        db.session.commit()

... по-прежнему обнуляет поле parent_id дочернего элемента, даже с passive_deletes=True. Так что passive_deletes='all' - это путь.

person SuperShoot    schedule 03.05.2019
comment
Однако True не всегда достаточно; каскады по-прежнему будут применяться, если были загружены дочерние элементы. "all" следует использовать в случае RESTRICT. True хорошо, когда каскад на стороне БД тоже CASCADE :D - person Ilja Everilä; 03.05.2019
comment
...или SET NULL (источник). - person Ilja Everilä; 03.05.2019
comment
Спасибо за это, я только когда-либо читал раздел учебника документации, касающийся passive_deletes, в котором не упоминается это как альтернатива (docs.sqlalchemy.org/en/13/orm/collections.html#passive-deletes). Мне нужно начать больше изучать документацию по API, так как это полезно знать! - person SuperShoot; 03.05.2019