2012-04-25 13 views
28

Mi aplicación está utilizando una sesión de ámbito y el estilo declarativo de SQLALchemy. Es una aplicación web y muchas de las inserciones de DB se ejecutan en Celery, un programador de tareas.Tratar con claves primarias duplicadas en la inserción en SQLAlchemy (estilo declarativo)

Por lo general, cuando se decide insertar un objeto, el código podría hacer algo a lo largo de las siguientes líneas:

from schema import Session 
from schema.models import Bike 

pk = 123 # primary key 
bike = Session.query(Bike).filter_by(bike_id=pk).first() 
if not bike: # no bike in DB 
    new_bike = Bike(pk, "shiny", "bike") 
    Session.add(new_bike) 
    Session.commit() 

El problema aquí es que debido a que mucho de esto se lleva a cabo por los trabajadores asíncronos, es posible que una trabajando para estar a la mitad, insertando un Bike con id=123, mientras que otro está comprobando su existencia. En este caso, el segundo trabajador intentará insertar una fila con la misma clave principal, y SQLAlchemy generará un IntegrityError.

no puedo por la vida de mí encontrar una buena manera de hacer frente a este problema, aparte de el canje de Session.commit() para:

'''schema/__init__.py''' 
from sqlalchemy.orm import scoped_session, sessionmaker 
Session = scoped_session(sessionmaker()) 

def commit(ignore=False): 
    try: 
     Session.commit() 
    except IntegrityError as e: 
     reason = e.message 
     logger.warning(reason) 

     if not ignore: 
      raise e 

     if "Duplicate entry" in reason: 
      logger.info("%s already in table." % e.params[0]) 
      Session.rollback() 

Y entonces todas partes tengo Session.commit ahora tengo schema.commit(ignore=True) donde I don' Me importa que la fila no se inserte de nuevo.

Para mí esto parece muy frágil debido a la comprobación de cadena. Así como un FYI, cuando se produce un IntegrityError se ve así:

(IntegrityError) (1062, "Duplicate entry '123' for key 'PRIMARY'") 

Así que por supuesto es la clave principal que estaba insertando fue algo así como Duplicate entry is a cool thing entonces yo supongo que podría perderse IntegrityError 's que no eran en realidad debido a las claves primarias duplicadas.

¿Hay enfoques mejores, que mantienen el enfoque SQLAlchemy limpia que estoy usando (en lugar de empezar a escribir declaraciones en las cadenas etc...)

Db es MySQL (aunque para las pruebas unitarias me gusta para usar SQLite, y no quisiera obstaculizar esa capacidad con nuevos enfoques).

¡Salud!

+3

¿por qué no se tiene en cuenta el uso de incremento automático para la generación de las claves principales que? entonces no tienes que preocuparte por este problema. ¿O hay alguna razón específica para no hacer eso? – mata

+0

Hay una razón específica (lo siento, el ejemplo es un poco trivial). – Edwardr

Respuesta

6

Debe manejar cada IntegrityError de la misma manera: deshace la transacción y, opcionalmente, vuelve a intentarlo. Algunas bases de datos ni siquiera le permitirán hacer nada más que eso después de un IntegrityError. También puede adquirir un candado en la mesa, o un candado más fino si la base de datos lo permite, al comienzo de las dos transacciones en conflicto.

Utilización de la sentencia with para comenzar una transacción explícita, y automáticamente comprometerse (o deshacer en cualquier excepción):

from schema import Session 
from schema.models import Bike 

session = Session() 
with session.begin(): 
    pk = 123 # primary key 
    bike = session.query(Bike).filter_by(bike_id=pk).first() 
    if not bike: # no bike in DB 
     new_bike = Bike(pk, "shiny", "bike") 
     session.add(new_bike) 
+0

Hola. No estoy programando insertar y verificar al mismo tiempo a propósito. El problema es que el objeto pasa a ser creado por dos procesos separados de una manera ad-hoc. No hay nada de malo en eso, es solo la forma en que la aplicación es (en realidad, los objetos no son bicicletas, son * veces *). Sin embargo, tienes razón sobre ejecutar un solo trabajador. Estoy buscando cómo especificar un solo trabajador que maneje todas las tareas relacionadas con DB, lo que proporcionará la sincronía que requiero. Hacer la inserción desde la aplicación no es una opción. La base de datos está en una máquina remota y necesito respuestas de aplicaciones web inferiores a 100ms. – Edwardr

+0

El diseño es casi siempre el culpable de este tipo de problemas SQL. Por ejemplo, ¿está seguro de que no puede hacer que la clave primaria de la base de datos aumente automáticamente y trate el resultado ocasional de "dos filas para lo que antes era la columna de clave principal"? – joeforker

+0

[Lo siento, debería agregar, hay una buena razón por la cual el PK no es un auto-incremento] No estoy seguro de estar de acuerdo. El DB es compartido por muchas otras aplicaciones, incluido el uso de la tabla en cuestión. ¿Por qué es un mal diseño que una base de datos tenga una fila insertando mi otro proceso/aplicación/ser humano después de haber hecho una diligencia debida para verificar? El punto es que debes lidiar con eso en tu aplicación. Mi pregunta es simplemente que la única forma en que puedo ver para tratar con eso en SQLAlchemy es a través de la comprobación de cadenas, y no parece particularmente sólido. – Edwardr

3

Estoy asumiendo que sus claves primarias aquí son naturales, de alguna manera, por lo que no puedes confiar en las técnicas normales de autoincrement. Entonces, digamos que el problema es realmente el de una columna única que necesita insertar, que es más común.

si desea "intentar insertar, deshacer parcialmente en caso de error", utiliza un SAVEPOINT, que con SQLAlchemy es begin_nested(). el siguiente rollback() o commit() solo actúa sobre ese SAVEPOINT, no el mayor lapso de tiempo que está pasando.

Sin embargo, en general, el patrón aquí es uno que realmente debería evitarse. Lo que realmente quieres hacer aquí es una de tres cosas. 1.No ejecute trabajos simultáneos que tengan que ver con las mismas claves que deben insertarse. 2. sincronice los trabajos de alguna manera con las claves simultáneas que se están trabajando y 3. use algún servicio común para generar nuevos registros de este tipo en particular, compartidos por trabajos (o asegúrese de que estén todos configurados antes de que se ejecuten los trabajos).

Si lo piensas bien, el n. ° 2 tiene lugar en cualquier caso con un alto grado de aislamiento. Comience dos sesiones de postgres. Sesión 1:

test=> create table foo(id integer primary key); 
NOTICE: CREATE TABLE/PRIMARY KEY will create implicit index "foo_pkey" for table "foo" 
CREATE TABLE 
test=> begin; 
BEGIN 
test=> insert into foo (id) values (1); 

sesión 2:

test=> begin; 
BEGIN 
test=> insert into foo(id) values(1); 

lo que verá es, sesión 2 bloques, como la fila con PK # 1 está bloqueado. No estoy seguro de si MySQL es lo suficientemente inteligente como para hacer esto, pero ese es el comportamiento correcto. Si OTOH intenta insertar un PK diferente:

^CCancel request sent 
ERROR: canceling statement due to user request 
test=> rollback; 
ROLLBACK 
test=> begin; 
BEGIN 
test=> insert into foo(id) values(2); 
INSERT 0 1 
test=> \q 

procede sin bloqueos.

El punto es que si usted está haciendo este tipo de contención de PK/UQ, sus tareas de apio se serializarán de todos modos, o al menos, deberían serlo.

23

Si usa session.merge(bike) en lugar de session.add(bike), no generará errores de clave primaria. El bike se recuperará y actualizará o creará según sea necesario.

+6

Si usa fusionar, puede obtener errores de integridad si realiza dos fusiones en sesiones diferentes simultáneamente. – Sjoerd

+0

Esta respuesta es buena cuando la sesión se ajusta a la memoria, pero no tan buena para consultas más grandes. Entonces, si desea agregar más datos de los que cabe en la memoria, no puede simplemente crear un conjunto de sesiones y fusionarlas, ¿verdad? – elplatt

2

En lugar de session.add(obj) necesita utilizar los códigos mencionados a continuación, esto será mucho más limpio y no tendría que usar la función de confirmación personalizada como lo ha mencionado. Sin embargo, esto ignorará los conflictos, no solo para la clave duplicada sino también para los demás.

mysql:

self.session.execute(insert(self.table, values=values, prefixes=['IGNORE'])) 

sqlite

self.session.execute(insert(self.table, values=values, prefixes=['OR IGNORE'])) 
Cuestiones relacionadas