2011-08-18 8 views
12

Ho un modello/tabella Test e un modello/tabella TestAuditLog, utilizzando SQLAlchemy e SQL Server 2008. La relazione tra i due è Test.id == TestAuditLog.entityId, con un test con molti audit log. TestAuditLog è progettato per conservare una cronologia delle modifiche alle righe nella tabella Test. Voglio tenere traccia quando viene eliminato anche uno Test, ma ho problemi con questo. In SQL Server Management Studio, ho impostato la proprietà "Enforce Foreign Key Constraint" della relazione FK_TEST_AUDIT_LOG_TEST su "No", pensando che consentirebbe a una riga TestAuditLog di esistere con uno che non si connette più a qualsiasi Test.id perché il Test è stato eliminato. Tuttavia, quando cerco di creare un TestAuditLog con SQLAlchemy e quindi eliminare il Test, ottengo un errore:SQLAlchemy: non applicare il vincolo di chiave esterna a una relazione

(IntegrityError) ('23000', "[23000] [Microsoft][ODBC SQL Server Driver][SQL Server]Cannot insert the value NULL into column 'AL_TEST_ID', table 'TEST_AUDIT_LOG'; column does not allow nulls. UPDATE fails. (515) (SQLExecDirectW); [01000] [Microsoft][ODBC SQL Server Driver][SQL Server]The statement has been terminated. (3621)") u'UPDATE [TEST_AUDIT_LOG] SET [AL_TEST_ID]=? WHERE [TEST_AUDIT_LOG].[AL_ID] = ?' (None, 8)

penso a causa del rapporto di chiave esterna tra Test e TestAuditLog, dopo aver eliminare la riga Test, SQLAlchemy sta tentando di aggiornare tutti i registri di controllo di quel test per avere un . Non voglio che lo faccia; Voglio che SQLAlchemy lasci da solo i registri di controllo. Come posso dire a SQLAlchemy di consentire l'esistenza di registri di controllo di cui entityId non si connette con qualsiasi Test.id?

ho provato solo la rimozione della ForeignKey dai miei tavoli, ma mi piacerebbe essere ancora in grado di dire myTest.audits e ottenere tutti i registri di controllo di un test, e SQLAlchemy lamentavano di non sapere come partecipare Test e TestAuditLog. Quando ho quindi specificato uno primaryjoin su relationship, si è lamentato di non avere uno ForeignKey o ForeignKeyConstraint con le colonne.

Qui sono i miei modelli:

class TestAuditLog(Base, Common): 
    __tablename__ = u'TEST_AUDIT_LOG' 
    entityId = Column(u'AL_TEST_ID', INTEGER(), ForeignKey(u'TEST.TS_TEST_ID'), 
     nullable=False) 
    ... 

class Test(Base, Common): 
    __tablename__ = u'TEST' 
    id = Column(u'TS_TEST_ID', INTEGER(), primary_key=True, nullable=False) 
    audits = relationship(TestAuditLog, backref="test") 
    ... 

Ed ecco come sto cercando di eliminare un test, mantenendo i suoi registri di controllo, la loro entityId intatte:

test = Session.query(Test).first() 
    Session.begin() 
    try: 
     Session.add(TestAuditLog(entityId=test.id)) 
     Session.flush() 
     Session.delete(test) 
     Session.commit() 
    except: 
     Session.rollback() 
     raise 

risposta

11

È possibile risolvere questo problema:

  • PUNTO-1: non avendo un ForeignKey né sul piano RDBMS né sul piano SA
  • PUNTO-2: esplicitamente specificare unirsi condizioni per il rapporto
  • PUNTO-3: relazione marchio cascate di fare affidamento su passive_deletes bandiera

Snippet di codice completamente funzionante di seguito dovrebbe darti un'idea (punti sono evidenziati nel code):

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey 
from sqlalchemy.orm import scoped_session, sessionmaker, relationship 
from sqlalchemy.ext.declarative import declarative_base 

Base = declarative_base() 
engine = create_engine('sqlite:///:memory:', echo=False) 

Session = sessionmaker(bind=engine) 

class TestAuditLog(Base): 
    __tablename__ = 'TEST_AUDIT_LOG' 
    id = Column(Integer, primary_key=True) 
    comment = Column(String) 

    entityId = Column('TEST_AUDIT_LOG', Integer, nullable=False, 
        # POINT-1 
        #ForeignKey('TEST.TS_TEST_ID', ondelete="CASCADE"), 
        ) 

    def __init__(self, comment): 
     self.comment = comment 

    def __repr__(self): 
     return "<TestAuditLog(id=%s entityId=%s, comment=%s)>" % (self.id, self.entityId, self.comment) 

class Test(Base): 
    __tablename__ = 'TEST' 
    id = Column('TS_TEST_ID', Integer, primary_key=True) 
    name = Column(String) 

    audits = relationship(TestAuditLog, backref='test', 
       # POINT-2 
       primaryjoin="Test.id==TestAuditLog.entityId", 
       foreign_keys=[TestAuditLog.__table__.c.TEST_AUDIT_LOG], 
       # POINT-3 
       passive_deletes='all', 
      ) 

    def __init__(self, name): 
     self.name = name 

    def __repr__(self): 
     return "<Test(id=%s, name=%s)>" % (self.id, self.name) 


Base.metadata.create_all(engine) 

################### 
## tests 
session = Session() 

# create test data 
tests = [Test("test-" + str(i)) for i in range(3)] 
_cnt = 0 
for _t in tests: 
    for __ in range(2): 
     _t.audits.append(TestAuditLog("comment-" + str(_cnt))) 
     _cnt += 1 
session.add_all(tests) 
session.commit() 
session.expunge_all() 
print '-'*80 

# check test data, delete one Test 
t1 = session.query(Test).get(1) 
print "t: ", t1 
print "t.a: ", t1.audits 
session.delete(t1) 
session.commit() 
session.expunge_all() 
print '-'*80 

# check that audits are still in the DB for deleted Test 
t1 = session.query(Test).get(1) 
assert t1 is None 
_q = session.query(TestAuditLog).filter(TestAuditLog.entityId == 1) 
_r = _q.all() 
assert len(_r) == 2 
for _a in _r: 
    print _a 

Un'altra opzione sarebbe quella di duplicare la colonna utilizzata nell'FK e rendere nullable la colonna FK con l'opzione ON CASCADE SET NULL. In questo modo puoi ancora controllare la traccia di controllo degli oggetti cancellati usando questa colonna.

+0

Il 'passive_deletes = 'all'' sulla' relazione' l'ha fatto!In questo modo sono riuscito a mantenere le relazioni e SQLAlchemy non è tornato indietro e ho cercato di cancellare "entityId' in" Test ". Grazie! –

+0

Solo per il riferimento - sarebbe necessario anche impostare 'lazy =" dynamic "' sul lato genitore della relazione in modo che sqlalchemy non recuperi tutti i child quando non ne hai bisogno (cioè quando aggiorna solo un campo non pertinente nella tabella padre). – Greg0ry

+0

@ Greg0ry: No, non è necessario. Come documentato in [Uso delle strategie di caricamento: Caricamento lento, Caricamento in carico] (http://docs.sqlalchemy.org/en/rel_1_0/orm/loading_relationships.html#using-loader-strategies-lazy-loading-eager-loading): * Per impostazione predefinita, tutte le relazioni tra oggetti sono lazy loading ... *. Quindi, a meno che tu non faccia altrimenti, il genitore non dovrebbe caricare i bambini a meno che tu non li acceda. – van

Problemi correlati