python - quick - sqlalchemy relationship not null
Manejo de claves primarias duplicadas al insertar en SQLAlchemy(estilo declarativo) (4)
Debe manejar cada IntegrityError
la misma manera: deshaga la transacción y, opcionalmente, intente nuevamente. Algunas bases de datos ni siquiera le permiten hacer nada más que eso después de IntegrityError
. También puede adquirir un bloqueo en la mesa, o un bloqueo de grano más fino si la base de datos lo permite, al comienzo de las dos transacciones en conflicto.
Usando la declaración with
para comenzar explícitamente una transacción, y confirmar automáticamente (o deshacer 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)
Mi aplicación está utilizando una sesión con ámbito y el estilo declarativo de SQLALchemy. Es una aplicación web y muchas de las inserciones de base de datos las ejecuta Celery
, un programador de tareas.
Generalmente, cuando decido insertar un objeto, mi código puede hacer algo en 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 gran parte de esto lo realizan trabajadores asíncronos, es posible que uno que esté trabajando esté a mitad de camino insertando una 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 mi vida, encontrar una buena manera de lidiar con este problema, aparte de cambiar Session.commit()
por:
''''''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 luego, en todas partes donde tengo Session.commit
, ahora tengo schema.commit(ignore=True)
donde no me importa que la fila no se vuelva a insertar.
Para mí, esto parece muy frágil debido a la comprobación de la cadena. Al igual que un FYI, cuando se IntegrityError
un IntegrityError
se ve así:
(IntegrityError) (1062, "Duplicate entry ''123'' for key ''PRIMARY''")
Así que, por supuesto, la clave principal que estaba insertando era algo así como la Duplicate entry is a cool thing
entonces supongo que podría pasar por alto el de IntegrityError
, que no fue en realidad debido a claves primarias duplicadas.
¿Hay mejores enfoques que mantengan el enfoque limpio de SQLAlchemy que estoy usando (en lugar de comenzar a escribir declaraciones en cadenas, etc.)?
Db es MySQL (aunque para las pruebas unitarias me gusta usar SQLite, y no querría obstaculizar esa capacidad con nuevos enfoques).
¡Aclamaciones!
En lugar de session.add(obj)
necesita usar los códigos mencionados a continuación, esto será mucho más limpio y no necesitará usar la función de confirmación personalizada como mencionó. Esto ignorará el conflicto, sin embargo, no solo para la clave duplicada sino también para otros.
mysql:
self.session.execute(insert(self.table, values=values, prefixes=[''IGNORE'']))
sqlite
self.session.execute(insert(self.table, values=values, prefixes=[''OR IGNORE'']))
Si utiliza session.merge(bike)
lugar de session.add(bike)
, no generará errores de clave principal. La bike
será recuperada y actualizada o creada según sea necesario.
Supongo que sus claves principales aquí son naturales de alguna manera, por lo que no puede confiar en las técnicas normales de autoincremento. Entonces, digamos que el problema es realmente una de las columnas únicas que necesita insertar, que es más común.
Si desea "intentar insertar, revertir parcialmente si falla", use SAVEPOINT, que con SQLAlchemy está begin_nested (). el siguiente rollback () o commit () solo actúa sobre ese SAVEPOINT, no el mayor lapso de cosas que están sucediendo.
Sin embargo, en general, el patrón aquí es solo uno que realmente debe evitarse. Lo que realmente quieres hacer aquí es una de tres cosas. 1. No ejecute trabajos simultáneos que traten con las mismas claves que deben insertarse. 2. sincronice los trabajos de alguna manera en las teclas concurrentes con las que se trabaja y 3. use algún servicio común para generar nuevos registros de este tipo en particular, compartidos por los trabajos (o asegúrese de que estén configurados antes de ejecutar los trabajos).
Si lo piensas, el # 2 tiene lugar en cualquier caso con un alto grado de aislamiento. Iniciar 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 la sesión 2 bloques, ya que la fila con PK # 1 está bloqueada. 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 bien sin bloqueo.
El punto es que si estás haciendo este tipo de disputa PK / UQ, tus tareas de apio se van a serializar de todos modos , o al menos, deberían serlo.