2011-08-18 14 views
12

Tengo un modelo/tabla Test y un modelo/tabla TestAuditLog, usando SQLAlchemy y SQL Server 2008. La relación entre los dos es Test.id == TestAuditLog.entityId, con una prueba que tiene muchas auditorías registros TestAuditLog está destinado a mantener un historial de cambios en las filas en la tabla Test. Quiero rastrear cuando se borra un Test, también, pero estoy teniendo problemas con esto. En SQL Server Management Studio, establecí la propiedad FK_TEST_AUDIT_LOG_TEST de la relación "Enforce Foreign Key Constraint" en "No", pensando que permitiría una fila TestAuditLog existir con un entityId que ya no se conecta con ningún Test.id porque se ha eliminado el Test. Sin embargo, cuando intento crear un TestAuditLog con SQLAlchemy y elimine la Test, me sale un error:SQLAlchemy: no aplicar la restricción de clave externa en una relación

(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)

Creo que debido a la relación de clave externa entre Test y TestAuditLog, después de eliminar la fila Test, SQLAlchemy intenta actualizar todos los registros de auditoría de la prueba para tener un NULLentityId. No quiero que haga esto; Quiero que SQLAlchemy deje los registros de auditoría solo. ¿Cómo puedo decirle a SQLAlchemy que permita que existan registros de auditoría cuyo entityId no se conecte con ningún Test.id?

me trataron sólo la eliminación de la ForeignKey de mis cuadros, pero me gustaría seguir siendo capaz de decir myTest.audits y obtener todos los registros de auditoría de una prueba, y SQLAlchemy se quejaron de no saber cómo unirse Test y TestAuditLog. Cuando especifiqué un primaryjoin en el relationship, se quejaba de no tener un ForeignKey o ForeignKeyConstraint con las columnas.

Éstos son mis modelos:

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") 
    ... 

Y es como yo estoy tratando de eliminar una prueba, manteniendo sus registros de auditoría aquí, su entityId intactos:

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 

Respuesta

11

Puede resolver esto mediante:

  • POINT-1: no tener un ForeignKey ni en el nivel RDBMS ni en el nivel SA
  • PUNTO 2: especificar explícitamente unirse a las condiciones de la relación
  • PUNTO 3: cascadas relación marca a confiar en passive_deletes bandera

totalmente fragmento de código de trabajo a continuación debe darle una idea (puntos se destacan en el 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 

Otra opción sería duplicar la columna utilizada en el FK, y hacer que la columna FK sea nulable con la opción ON CASCADE SET NULL. De esta forma, aún puede verificar la pista de auditoría de los objetos eliminados utilizando esta columna.

+0

¡El 'pasivo_delete = 'all'' en la' relación' lo hizo!De esa forma pude mantener las relaciones y SQLAlchemy no retrocedió e intentó eliminar el 'entityId' en la eliminación de' Test'. ¡Gracias! –

+0

Solo como referencia: también se requiere establecer 'lazy =" dynamic "' en el lado padre de la relación para que sqlalchemy no busque todos los elementos secundarios cuando no lo necesite (es decir, al actualizar un campo irrelevante en la tabla padre)) – Greg0ry

+0

@ Greg0ry: No, no es necesario. Como se documenta en [Uso de estrategias de cargador: carga lenta, carga lenta] (http://docs.sqlalchemy.org/en/rel_1_0/orm/loading_relationships.html#using-loader-strategies-lazy-loading-eager-loading): * Por defecto, todas las relaciones entre objetos son carga lenta ... *. A menos que lo haga de otra manera, el padre no debe cargar hijos a menos que usted tenga acceso a ellos. – van

Cuestiones relacionadas