2012-10-08 54 views
8

Tengo dos tablas, por ejemplo, A y B. Ambas tienen una identificación de clave principal. Tienen una relación de muchos a muchos, SEC.Sqlalchemy: actualización secundaria de relaciones

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 fragmento 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 que dice

duplicate key value violates unique constraint a_b_pkey 

A mi entender, creo que intenta añadir (a.id, b.id) en la tabla 's' de nuevo 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?

Respuesta

3

El error que menciona es, de hecho, al insertar un valor conflictivo en la tabla seg. 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 compruebe qué valores está intentando insertar antes de extraer el error.

Al sobrescribir un valor de colección de varios a varios, SQLAlchemy compara los nuevos contenidos de la colección con el estado en la base de datos y, en consecuencia, emite instrucciones de eliminar e insertar. A menos que esté hurgando en las partes internas de SQLAlchemy, debe haber dos formas de encontrar este error.

La primera es una modificación simultánea: el Proceso 1 recupera el valor a.rels y nota que está vacío, mientras que el Proceso 2 también recupera a.rels, lo establece en [b1, b2] y confirma el vaciado (a, b1) , (a, b2) tuplas, Proceso 1 establece a.rels a [b1, b3] notando que el contenido anterior estaba vacío y cuando intenta vaciar el segundo tuplo (a, b1) obtiene un error de clave duplicado. La acción correcta en tales casos suele ser reintentar la transacción desde la parte superior. Puede usar serializable transaction isolation para obtener 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 estableciendo 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 una consulta, llamando al .options(noload(A.rels)) en la consulta. SQLAlchemy supondrá que la tabla sec no tiene filas coincidentes para los objetos cargados con esta estrategia en vigor.

+0

No estoy muy seguro de por qué.Necesito probarlo correctamente y se lo haré saber. Gracias por la ayuda. – Sri

8

El problema es que quiere asegurarse de que las instancias que cree sean únicas. Podemos crear un constructor alternativo que verifique un caché de instancias sin compromiso existentes o consulte la base de datos para la instancia comprometida existente antes de devolver una nueva instancia.

Aquí es una demostración de un procedimiento de este tipo:

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 la example from the SQLAlchemy wiki. Esta versión es mucho menos intrincada, lo que favorece la simplicidad sobre la flexibilidad. Lo he usado en sistemas de producción sin problemas.

Obviamente, se pueden agregar mejoras; esto es solo un simple ejemplo. El método get_unique podría heredarse de un UniqueMixin, para ser utilizado con cualquier cantidad de modelos. Se podría implementar una memoria más flexible de los argumentos. Esto también deja de lado el problema de múltiples hilos insertando datos conflictivos mencionados por Ants Aasma; manejo que es más complejo pero debe ser una extensión obvia. Te lo dejo a tí.

Cuestiones relacionadas