Sqlalchemy: actualización de la relación secundaria

Tengo dos tablas, digamos A y B. Ambas tienen una identificación de clave principal. Tienen una relación de muchos a muchos, SEC.

SEC = Table('sec', Base.metadata, Column('a_id', Integer, ForeignKey('A.id'), primary_key=True, nullable=False), Column('b_id', Integer, ForeignKey('B.id'), primary_key=True, nullable=False) ) class A(): ... id = Column(Integer, primary_key=True) ... rels = relationship(B, secondary=SEC) class B(): ... id = Column(Integer, primary_key=True) ... 

Consideremos este pedazo de código.

 a = A() b1 = B() b2 = B() a.rels = [b1, b2] ... #some place later b3 = B() a.rels = [b1, b3] # errors sometimes 

A veces, me sale un error en la última línea diciendo

 duplicate key value violates unique constraint a_b_pkey 

En mi entendimiento, creo que intenta agregar (a.id, b.id) a la tabla ‘sec’ nuevamente, lo que resulta en un error de restricción único. ¿Eso es lo que es? Si es así, ¿cómo puedo evitar esto? Si no, ¿por qué tengo este error?

El problema es que desea asegurarse de que las instancias que crea son únicas. Podemos crear un constructor alternativo que verifique un caché de instancias no comprometidas existentes o consulte la base de datos para una instancia comprometida existente antes de devolver una instancia nueva.

Aquí hay una demostración de tal método:

 from sqlalchemy import Column, Integer, String, ForeignKey, Table from sqlalchemy.engine import create_engine from sqlalchemy.ext.declarative.api import declarative_base from sqlalchemy.orm import sessionmaker, relationship engine = create_engine('sqlite:///:memory:', echo=True) Session = sessionmaker(engine) Base = declarative_base(engine) session = Session() class Role(Base): __tablename__ = 'role' id = Column(Integer, primary_key=True) name = Column(String, nullable=False, unique=True) @classmethod def get_unique(cls, name): # get the session cache, creating it if necessary cache = session._unique_cache = getattr(session, '_unique_cache', {}) # create a key for memoizing key = (cls, name) # check the cache first o = cache.get(key) if o is None: # check the database if it's not in the cache o = session.query(cls).filter_by(name=name).first() if o is None: # create a new one if it's not in the database o = cls(name=name) session.add(o) # update the cache cache[key] = o return o Base.metadata.create_all() # demonstrate cache check r1 = Role.get_unique('admin') # this is new r2 = Role.get_unique('admin') # from cache session.commit() # doesn't fail # demonstrate database check r1 = Role.get_unique('mod') # this is new session.commit() session._unique_cache.clear() # empty cache r2 = Role.get_unique('mod') # from database session.commit() # nop # show final state print session.query(Role).all() # two unique instances from four create calls 

El método create_unique se inspiró en el ejemplo de la wiki de SQLAlchemy . Esta versión es mucho menos complicada, lo que favorece la simplicidad sobre la flexibilidad. Lo he usado en sistemas de producción sin problemas.

Obviamente hay mejoras que se pueden agregar; Esto es solo un ejemplo simple. El método get_unique podría heredarse de un UniqueMixin , para ser utilizado para cualquier número de modelos. Se podrían implementar memorias más flexibles de los argumentos. Esto también deja de lado el problema de los múltiples subprocesos que insertan datos en conflicto mencionados por Ants Aasma; El manejo es más complejo pero debería ser una extensión obvia. Te lo dejo a tí.

El error que mencionas es de hecho al insertar un valor en conflicto en la tabla de segundos. Para asegurarse de que proviene de la operación que cree que es, no de algún cambio anterior, active el registro de SQL y verifique qué valores intenta insertar antes de cometer errores.

Cuando se sobrescribe un valor de colección de muchos a muchos, SQLAlchemy compara los nuevos contenidos de la colección con el estado en la base de datos y, en consecuencia, emite declaraciones de eliminación e inserción. A menos que esté hurgando en el interior de SQLAlchemy, debería haber dos formas de encontrar este error.

La primera es la modificación concurrente: el Proceso 1 recupera el valor a.rels y advierte que está vacío, mientras que el Proceso 2 también recupera a.rels, lo establece en [b1, b2] y confirma el vaciado del (a, b1), (a, b2) Tuplas, el Proceso 1 establece a.rels en [b1, b3] notando que el contenido anterior estaba vacío y cuando intenta vaciar la tupla secundaria (a, b1), se produce un error de clave duplicada. La acción correcta en tales casos suele ser volver a intentar la transacción desde la parte superior. Puede utilizar el aislamiento de transacción serializable para obtener en su lugar un error de serialización en este caso que es distinto de un error de lógica de negocios que causa un error de clave duplicada.

El segundo caso ocurre cuando ha logrado convencer a SQLAlchemy de que no necesita conocer el estado de la base de datos configurando la estrategia de carga del atributo rels en noload . Esto se puede hacer al definir la relación agregando el parámetro lazy='noload' , o al realizar consultas, llamando a .options(noload(A.rels)) en la consulta. SQLAlchemy asumirá que la tabla sec no tiene filas coincidentes para los objetos cargados con esta estrategia vigente.