SQLAlchemy: basamaklı silme


116

SQLAlchemy'nin kademeli seçeneklerinde önemsiz bir şeyi kaçırıyor olmalıyım çünkü doğru çalışması için basit bir kademeli silme alamıyorum - eğer bir ana öğe silinirse, çocuklar nullyabancı anahtarlarla devam eder .

Buraya kısa bir test senaryosu koydum:

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key = True)

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key = True)
    parentid = Column(Integer, ForeignKey(Parent.id))
    parent = relationship(Parent, cascade = "all,delete", backref = "children")

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

session = Session()

parent = Parent()
parent.children.append(Child())
parent.children.append(Child())
parent.children.append(Child())

session.add(parent)
session.commit()

print "Before delete, children = {0}".format(session.query(Child).count())
print "Before delete, parent = {0}".format(session.query(Parent).count())

session.delete(parent)
session.commit()

print "After delete, children = {0}".format(session.query(Child).count())
print "After delete parent = {0}".format(session.query(Parent).count())

session.close()

Çıktı:

Before delete, children = 3
Before delete, parent = 1
After delete, children = 3
After delete parent = 0

Ebeveyn ve Çocuk arasında basit, bire çok bir ilişki vardır. Komut dosyası bir ebeveyn oluşturur, 3 çocuk ekler ve ardından yürütür. Ardından, ebeveyni siler, ancak çocuklar ısrar eder. Neden? Çocukların kademeli silinmesini nasıl sağlarım?


Dokümanlardaki bu bölüm (en azından şimdi, orijinal gönderiden
Soferio

Yanıtlar:


185

Sorun, sqlalchemy'nin Childebeveyn olarak görmesidir , çünkü ilişkinizi tanımladığınız yer burasıdır (elbette ona "Çocuk" demeniz umursamaz).

Parentİlişkiyi bunun yerine sınıfta tanımlarsanız, işe yarayacaktır:

children = relationship("Child", cascade="all,delete", backref="parent")

( "Child"dizge olarak not edin : bildirime dayalı stil kullanılırken buna izin verilir, böylece henüz tanımlanmamış bir sınıfa başvurabilirsiniz)

Siz de eklemek isteyebilirsiniz delete-orphan( deleteebeveyn silindiğinde çocukların silinmesine neden olur, delete-orphanayrıca ebeveyn silinmemiş olsa bile ebeveynden "kaldırılan" çocukları da siler)

DÜZENLEME: az önce öğrendim: sınıfta ilişkiyi gerçekten tanımlamak istiyorsanız Child, bunu yapabilirsiniz, ancak arka referans üzerinde kademeli tanımlamanız gerekecektir (geri referansı açıkça oluşturarak), aşağıdaki gibi:

parent = relationship(Parent, backref=backref("children", cascade="all,delete"))

(ima eden from sqlalchemy.orm import backref)


6
Aha, işte bu. Keşke bu konuda dokümantasyon daha açık olsaydı!
carl

15
Evet. Çok yararlı. SQLAlchemy'nin belgelerinde her zaman sorunlar yaşadım.
ayaz


1
@Lyman Zerga: OP örneğinde: Bir Childnesneyi kaldırırsanız parent.children, bu nesne veritabanından silinmeli mi, yoksa yalnızca üst öğeye referansı mı kaldırılmalıdır (yani parentid, satırı silmek yerine sütunu boş olarak ayarlayın )
Steven

1
Bekle, relationshipebeveyn-çocuk kurulumunu dikte etmez. ForeignKeyMasada kullanmak , onu çocuk olarak kuran şeydir. relationshipEbeveyn veya çocukta olması önemli değil .
d512

110

@ Steven'ın asnwer, session.delete()benim durumumda asla gerçekleşmeyen , silerken iyidir . Çoğu zaman sildiğimi fark ettim session.query().filter().delete()(bu, öğeleri belleğe koymaz ve doğrudan db'den siler). Bu yöntemi kullanmak sqlalchemy's cascade='all, delete'çalışmıyor. Yine de bir çözüm var: ON DELETE CASCADEdb aracılığıyla (not: tüm veritabanları bunu desteklemez).

class Child(Base):
    __tablename__ = "children"

    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parents.id", ondelete='CASCADE'))

class Parent(Base):
    __tablename__ = "parents"

    id = Column(Integer, primary_key=True)
    child = relationship(Child, backref="parent", passive_deletes=True)

3
Bu farkı açıkladığınız için teşekkürlersession.query().filter().delete()
Kullanmaya

4
passive_deletes='all'Ebeveyn silindiğinde alt veritabanı basamakları tarafından silinecek çocukları almak için ayarlamam gerekiyordu. İle passive_deletes=True, üst öğe silinmeden önce alt nesnelerin ilişkisi kesiliyordu (üst öğe NULL olarak ayarlandı), bu nedenle veritabanı kademeli hiçbir şey yapmıyordu.
Milorad Pop-Tosic

@ MiloradPop-Tosic SQLAlchemy'yi 3 yıldır kullanmıyorum ama dokümanı okumak passive_deletes gibi görünüyor = True hala doğru şey.
Alex Okrushko

2
passive_deletes=TrueBu senaryoda doğru çalıştığını onaylayabilirim .
d512

Silme üzerine kademeli geçiş içeren alembic otomatik oluşturma revizyonlarında sorun yaşıyordum - cevap buydu.
JNW

105

Oldukça eski bir gönderi, ancak bunun için bir veya iki saat geçirdim, bu yüzden bulduğumu paylaşmak istedim, özellikle de listelenen diğer yorumlardan bazıları pek doğru olmadığı için.

TL; DR

Alt tabloya bir yabancı verin veya mevcut olanı değiştirerek ondelete='CASCADE'şunları ekleyin :

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))

Ve biri şu ilişkilerinin:

a) Bu üst tablodaki:

children = db.relationship('Child', backref='parent', passive_deletes=True)

b) Veya alt masada şu:

parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

ayrıntılar

Öncelikle, kabul edilen cevabın söylediğine rağmen, ebeveyn / çocuk ilişkisi kullanılarak relationshipkurulmaz, kullanılarak kurulur ForeignKey. relationshipÜst veya alt masalara koyabilirsiniz ve iyi çalışacaktır. Görünüşe göre, alt masalarda,backref anahtar kelime bağımsız değişkenine ek olarak işlevi de .

1. Seçenek (tercih edilir)

İkincisi, SqlAlchemy iki farklı basamaklandırmayı destekler. İlki ve önerdiğim, veritabanınızda yerleşiktir ve genellikle yabancı anahtar bildiriminde bir kısıtlama biçimini alır. PostgreSQL'de şöyle görünür:

CONSTRAINT child_parent_id_fkey FOREIGN KEY (parent_id)
REFERENCES parent_table(id) MATCH SIMPLE
ON DELETE CASCADE

Bu, içinden bir kaydı sildiğinizde parent_table, içindeki tüm ilgili satırların child_tableveritabanı tarafından sizin için silineceği anlamına gelir . Hızlı ve güvenilirdir ve muhtemelen en iyi seçeneğinizdir. Bunu SqlAlchemy'de şu şekilde ayarlıyorsunuz ForeignKey(alt tablo tanımının bir parçası):

parent_id = db.Column(db.Integer, db.ForeignKey('parent.id', ondelete='CASCADE'))
parent = db.relationship('Parent', backref=backref('children', passive_deletes=True))

ondelete='CASCADE'Yaratan parçasıdır ON DELETE CASCADEmasaya.

Anladım!

Burada önemli bir uyarı var. Bir relationshipile nasıl belirtildiğime dikkat edin passive_deletes=True? Buna sahip değilseniz, her şey işe yaramayacak. Bunun nedeni, bir ana kaydı sildiğinizde SqlAlchemy'nin varsayılan olarak gerçekten tuhaf bir şey yapmasıdır. Tüm alt satırların yabancı anahtarlarını olarak ayarlar NULL. Eğer bir satırı silerseniz parent_tablenerede id= 5, o zaman temelde çalıştırır

UPDATE child_table SET parent_id = NULL WHERE parent_id = 5

Neden bunu istiyorsun, hiçbir fikrim yok. Birçok veritabanı motoru NULL, bir yetim oluşturarak geçerli bir yabancı anahtar belirlemenize izin verse bile şaşırırdım. Kötü bir fikir gibi görünüyor, ama belki bir kullanım durumu vardır. Her neyse, SqlAlchemy'nin bunu yapmasına izin verirseniz, veritabanının kurduğunuz programı kullanarak çocukları temizlemesini engellersiniz ON DELETE CASCADE. Bunun nedeni, hangi alt satırların silineceğini bilmek için bu yabancı anahtarlara güvenmesidir. SqlAlchemy hepsini olarak ayarladıktan NULLsonra veritabanı bunları silemez. Ayarlamak passive_deletes=TrueSqlAlchemy'nin NULLyabancı anahtarları dışarı çıkarmasını engeller .

SqlAlchemy belgelerinde pasif silmeler hakkında daha fazla bilgi edinebilirsiniz .

seçenek 2

Bunu yapmanın diğer yolu, SqlAlchemy'nin sizin için yapmasına izin vermektir. Bu kullanılarak kurulur cascadeve argüman relationship. İlişki ana tabloda tanımlanmışsa, şöyle görünür:

children = relationship('Child', cascade='all,delete', backref='parent')

İlişki çocuk üzerindeyse, bunu şöyle yaparsın:

parent = relationship('Parent', backref=backref('children', cascade='all,delete'))

Yine, bu çocuk, bu yüzden adlı bir yöntemi çağırmalısınız backref ve basamaklı verileri oraya koymalısınız.

Bunun yerine, bir üst satırı sildiğinizde, SqlAlchemy aslında alt satırları temizlemeniz için silme ifadelerini çalıştıracaktır. Bu, muhtemelen bu veritabanının sizin için işlemesine izin vermek kadar verimli olmayacaktır, bu yüzden bunu önermiyorum.

Desteklediği basamaklı özelliklerle ilgili SqlAlchemy belgeleri .


Açıklama için teşekkür ederim. Şimdi mantıklı.
Odin

1
Neden Columnalt tablodaki a'yı da çalışmıyor olarak bildirmek ForeignKey('parent.id', ondelete='cascade', onupdate='cascade')işe yarıyor? Üst tablo satırı da silindiğinde çocukların da silinmesini bekliyordum. Bunun yerine, SQLA çocukları ya a olarak ayarlar ya parent.id=NULLda onları "olduğu gibi" bırakır, ancak silme işlemi yapılmaz. Bu, başlangıçta relationshipebeveynde children = relationship('Parent', backref='parent')veya olarak tanımlandıktan sonra relationship('Parent', backref=backref('parent', passive_deletes=True)); DB, cascadeDDL'deki (SQLite3 tabanlı kavram kanıtı) kuralları gösterir . Düşünceler?
code_dredd

1
Ayrıca, kullandığım zaman backref=backref('parent', passive_deletes=True)şu uyarıyı aldığımı not etmeliyim : bu (bariz) bire-çok ebeveyn-çocuk ilişkisinde herhangi bir nedenle SAWarning: On Parent.children, 'passive_deletes' is normally configured on one-to-many, one-to-one, many-to-many relationships only. "relationships only." % selfkullanılmasını sevmediğini öne passive_deletes=Truesürmek.
code_dredd

Harika açıklama. Bir soru - deletegereksiz cascade='all,delete'mi?
zaggi

1
@zaggi deleteiçinde gereksiz IS cascade='all,delete'göre beri, sqlalchemy dokümanlarının , alla eşanlamlı içindirsave-update, merge, refresh-expire, expunge, delete
pmsoltani

7

Steven, geri referansı açıkça oluşturmanız gerektiği konusunda haklıdır, bu, kademenin ebeveyn üzerinde uygulanmasına neden olur (test senaryosunda olduğu gibi çocuğa uygulanmasının aksine).

Bununla birlikte, Çocuk üzerindeki ilişkiyi tanımlamak, sqlalchemy'yi Çocuğu ebeveyn olarak kabul etmemektedir. İlişkinin nerede tanımlandığı (alt veya üst), hangisinin üst, hangisinin alt öğe olduğunu belirleyen iki tabloyu birbirine bağlayan yabancı anahtardır.

Yine de bir sözleşmeye bağlı kalmak mantıklı ve Steven'ın cevabına dayanarak, tüm çocuk ilişkilerimi ebeveyn üzerinde tanımlıyorum.


6

Belgelerle de uğraştım, ancak belgelerin kendilerinin kılavuzdan daha kolay olma eğiliminde olduğunu gördüm. Örneğin, sqlalchemy.orm'dan ilişki içe aktarırsanız ve yardım (ilişki) yaparsanız, size basamaklama için belirleyebileceğiniz tüm seçenekleri verecektir. Kurşun delete-orphandiyor ki:

alt türünde ebeveyn olmayan bir öğe tespit edilirse, onu silmek için işaretleyin.
Bu seçeneğin, alt sınıfın bekleyen bir öğesinin bir ebeveyn olmadan kalıcı olmasını engellediğini unutmayın.

Sorununuzun daha çok ebeveyn-çocuk ilişkilerini tanımlayan belgelerle ilgili olduğunu anladım. Ancak, basamaklı seçeneklerle ilgili bir sorun yaşıyor olabilirsiniz, çünkü "all"içerir "delete". "delete-orphan"dahil olmayan tek seçenektir "all".


Nesneler help(..)üzerinde kullanmak sqlalchemyçok yardımcı olur! Teşekkürler :-))) ! PyCharm bağlam yuvalarında hiçbir şey göstermez ve açıkça help. Çok teşekkür ederim!
dmitry_romanov

5

Steven'ın cevabı sağlam. Ek bir çıkarıma işaret etmek istiyorum.

Kullanarak relationship , uygulama katmanını (Flask) bilgi tutarlılığından sorumlu yaparsınız. Bu, veritabanına Flask aracılığıyla değil, veritabanı yardımcı programı veya veritabanına doğrudan bağlanan bir kişi gibi erişen diğer işlemlerin bu kısıtlamaları yaşamayacağı ve verilerinizi tasarlamak için çok çalıştığınız mantıksal veri modelini bozacak şekilde değiştirebileceği anlamına gelir. .

Mümkün olduğunda, ForeignKeyd512 ve Alex tarafından açıklanan yaklaşımı kullanın . DB motoru, kısıtlamaları gerçekten uygulamakta (kaçınılmaz bir şekilde) çok iyidir, bu nedenle bu, veri bütünlüğünü korumak için açık ara en iyi stratejidir. Veri bütünlüğünü işlemek için bir uygulamaya güvenmeniz gereken tek zaman, veritabanının bunları işleyemediği zamandır, örneğin, yabancı anahtarları desteklemeyen SQLite sürümleri.

Üst-alt nesne ilişkilerinde gezinme gibi uygulama davranışlarını etkinleştirmek için varlıklar arasında daha fazla bağlantı oluşturmanız gerekiyorsa backref, ile birlikte kullanın ForeignKey.


2

Stevan'ın cevabı mükemmel. Ama hala hatayı alıyorsanız. Bunun üzerine başka bir olası deneme şudur:

http://vincentaudebert.github.io/python/sql/2015/10/09/cascade-delete-sqlalchemy/

Bağlantıdan kopyalandı-

Modellerinizde kademeli silme belirtmiş olsanız bile yabancı anahtar bağımlılığıyla ilgili sorun yaşarsanız hızlı ipucu.

SQLAlchemy kullanarak, cascade='all, delete'üst tablonuzda olması gereken basamaklı bir silme belirtmek için . Tamam ama sonra şöyle bir şey çalıştırdığınızda:

session.query(models.yourmodule.YourParentTable).filter(conditions).delete()

Aslında, çocuk tablolarınızda kullanılan bir yabancı anahtarla ilgili bir hatayı tetikler.

Nesneyi sorgulamak ve ardından silmek için kullandığım çözüm:

session = models.DBSession()
your_db_object = session.query(models.yourmodule.YourParentTable).filter(conditions).first()
if your_db_object is not None:
    session.delete(your_db_object)

Bu, ebeveyn kaydınızı VE onunla ilişkili tüm çocukları silmeli.


1
Arama .first()gerekli mi? Hangi filtre koşulları bir nesne listesi döndürür ve her şeyin silinmesi gerekir? .first()Çağırmak sadece ilk nesneyi almaz mı ? @Prashant
Kavin Raju S

2

Alex Okrushko cevabı neredeyse benim için en iyisi oldu. Ondelete = 'CASCADE' ve passive_deletes = True kombine kullanıldı. Ama sqlite için çalışmasını sağlamak için fazladan bir şey yapmam gerekiyordu.

Base = declarative_base()
ROOM_TABLE = "roomdata"
FURNITURE_TABLE = "furnituredata"

class DBFurniture(Base):
    __tablename__ = FURNITURE_TABLE
    id = Column(Integer, primary_key=True)
    room_id = Column(Integer, ForeignKey('roomdata.id', ondelete='CASCADE'))


class DBRoom(Base):
    __tablename__ = ROOM_TABLE
    id = Column(Integer, primary_key=True)
    furniture = relationship("DBFurniture", backref="room", passive_deletes=True)

Sqlite için çalıştığından emin olmak için bu kodu eklediğinizden emin olun.

from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlite3 import Connection as SQLite3Connection

@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
    if isinstance(dbapi_connection, SQLite3Connection):
        cursor = dbapi_connection.cursor()
        cursor.execute("PRAGMA foreign_keys=ON;")
        cursor.close()

Buradan çalındı: SQLAlchemy ifade dili ve SQLite's on delete cascade


0

TLDR: Yukarıdaki çözümler işe yaramazsa, sütununuza nullable = False eklemeyi deneyin.

Kademeli işlevi mevcut çözümlerle çalışmak için kullanamayan bazı insanlar için buraya küçük bir nokta eklemek isterim (harika). Çalışmamla örnek arasındaki temel fark, automap kullanmamdı. Bunun kaskadların kurulumuna nasıl müdahale edebileceğini tam olarak bilmiyorum, ancak bunu kullandığımı not etmek istiyorum. Ayrıca bir SQLite veritabanı ile çalışıyorum.

Burada açıklanan her çözümü denedim, ancak alt tablomdaki satırların yabancı anahtarları üst satır silindiğinde boş olarak ayarlanmaya devam etti. Buradaki tüm çözümleri boşuna denedim. Ancak, alt sütunu yabancı anahtarla nullable = False olarak ayarladığımda basamaklı çalıştı.

Alt masada şunu ekledim:

Column('parent_id', Integer(), ForeignKey('parent.id', ondelete="CASCADE"), nullable=False)
Child.parent = relationship("parent", backref=backref("children", passive_deletes=True)

Bu kurulumla, kaskad beklendiği gibi çalıştı.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.