web-dev-qa-db-fra.com

Traitement des clés primaires en double lors de l'insertion dans SQLAlchemy (style déclaratif)

Mon application utilise une session délimitée et le style déclaratif de SQLALchemy. Il s'agit d'une application Web et de nombreuses insertions de bases de données sont exécutées par Celery, un planificateur de tâches.

En règle générale, lors de la décision d'insérer un objet, mon code peut faire quelque chose dans le sens suivant:

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

Le problème ici est que, car une grande partie de cela est effectué par des travailleurs asynchrones, il est possible qu'un travail soit à mi-chemin en insérant un Bike avec id=123, Tandis qu'un autre vérifie son existence. Dans ce cas, le deuxième travailleur essaiera d'insérer une ligne avec la même clé primaire et SQLAlchemy lèvera un IntegrityError.

Je ne peux pas pour la vie de trouver une bonne façon de résoudre ce problème en dehors de permuter Session.commit() pour:

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

Et puis partout où j'ai Session.commit J'ai maintenant schema.commit(ignore=True) où cela ne me dérange pas que la ligne ne soit pas insérée à nouveau.

Cela me semble très fragile à cause de la vérification des chaînes. Tout comme un FYI, lorsqu'un IntegrityError est levé, il ressemble à ceci:

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

Donc, bien sûr, la clé primaire que j'insérais était quelque chose comme Duplicate entry is a cool thing, Alors je suppose que je pourrais manquer les IntegrityError qui n'étaient pas réellement à cause des clés primaires en double.

Existe-t-il de meilleures approches qui maintiennent l'approche SQLAlchemy propre que j'utilise (au lieu de commencer à écrire des instructions dans des chaînes, etc.)

Db est MySQL (bien que pour les tests unitaires, j'aime utiliser SQLite, et je ne voudrais pas entraver cette capacité avec de nouvelles approches).

À votre santé!

36
Edwardr

Si vous utilisez session.merge(bike) au lieu de session.add(bike), vous ne générerez pas d'erreurs de clé primaire. bike sera récupéré et mis à jour ou créé selon les besoins.

28
sirdodger

Vous devez gérer chaque IntegrityError de la même manière: annulez la transaction et essayez à nouveau éventuellement. Certaines bases de données ne vous permettent même pas de faire plus que cela après un IntegrityError. Vous pouvez également acquérir un verrou sur la table, ou un verrou plus fin si la base de données le permet, au début des deux transactions en conflit.

Utilisation de l'instruction with pour démarrer explicitement une transaction et valider automatiquement (ou annuler toute exception):

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)
8
joeforker

Je suppose que vos clés primaires ici sont naturelles d'une certaine manière, c'est pourquoi vous ne pouvez pas vous fier aux techniques d'auto-incrémentation normales. Supposons donc que le problème soit vraiment l'une des colonnes uniques que vous devez insérer, ce qui est plus courant.

si vous voulez "essayer d'insérer, restaurer partiellement en cas d'échec", vous utilisez un SAVEPOINT, qui avec SQLAlchemy est begin_nested (). le rollback () ou commit () suivant n'agit que sur ce SAVEPOINT, pas la plus grande étendue de choses en cours.

Cependant, dans l'ensemble, le modèle ici est juste celui qui devrait vraiment être évité. Ce que vous voulez vraiment faire ici, c'est l'une des trois choses. 1. N'exécutez pas de travaux simultanés traitant des mêmes clés qui doivent être insérées. 2. synchronisez les travaux d'une manière ou d'une autre sur les clés simultanées utilisées et 3. utilisez un service commun pour générer de nouveaux enregistrements de ce type particulier, partagés par les travaux (ou assurez-vous qu'ils sont tous configurés avant l'exécution des travaux).

Si vous y réfléchissez, le n ° 2 a lieu dans tous les cas avec un haut degré d'isolement. Démarrez deux sessions postgres. Session 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);

session 2:

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

vous verrez des blocs de session 2, car la ligne avec PK # 1 est verrouillée. Je ne sais pas si MySQL est assez intelligent pour le faire, mais c'est le bon comportement. Si OTOH vous essayez d'insérer un PK différent:

^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

il se passe très bien sans bloquer.

Le fait est que si vous faites ce genre de conflit PK/UQ, vos tâches de céleri vont se sérialiser de toute façon, ou du moins, elles devraient l'être.

4
zzzeek

Au lieu de session.add(obj) vous devez utiliser les codes mentionnés ci-dessous, ce sera beaucoup plus propre et vous n'aurez pas besoin d'utiliser la fonction de validation personnalisée comme vous l'avez mentionné. Cela ignorera le conflit, cependant, non seulement pour la clé en double mais aussi pour les autres.

mysql:

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

sqlite

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