210 votes

SQLAlchemy a-t-il un équivalent de get_or_create de Django ?

Je souhaite récupérer un objet dans la base de données s'il existe déjà (en fonction des paramètres fournis) ou le créer s'il n'existe pas.

L'approche de Django get_or_create (ou source ). Existe-t-il un raccourci équivalent dans SQLAlchemy ?

Je suis en train de l'écrire explicitement comme ceci :

def get_or_create_instrument(session, serial_number):
    instrument = session.query(Instrument).filter_by(serial_number=serial_number).first()
    if instrument:
        return instrument
    else:
        instrument = Instrument(serial_number)
        session.add(instrument)
        return instrument

9 votes

Pour ceux qui souhaitent simplement ajouter un objet s'il n'existe pas encore, voir session.merge : stackoverflow.com/questions/12297156/

145voto

Kevin. Points 486

En suivant la solution de @WoLpH, voici le code qui a fonctionné pour moi (version simple) :

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance

Ainsi, je peux obtenir ou créer n'importe quel objet de mon modèle.

Supposons que mon objet modèle soit :

class Country(Base):
    __tablename__ = 'countries'
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)

Pour obtenir ou créer mon objet, j'écris :

myCountry = get_or_create(session, Country, name=countryName)

4 votes

Pour ceux qui cherchent comme moi, c'est la bonne solution pour créer une ligne si elle n'existe pas déjà.

4 votes

Ne faut-il pas ajouter la nouvelle instance à la session ? Sinon, si vous lancez un session.commit() dans le code appelant, rien ne se passera car la nouvelle instance n'est pas ajoutée à la session.

1 votes

Je vous en remercie. Je l'ai trouvé si utile que j'en ai fait un résumé pour un usage ultérieur. gist.github.com/jangeador/e7221fc3b5ebeeac9a08

140voto

Wolph Points 28062

C'est en gros la façon de procéder, il n'y a pas de raccourci disponible AFAIK.

On peut bien sûr généraliser :

def get_or_create(session, model, defaults=None, **kwargs):
    instance = session.query(model).filter_by(**kwargs).one_or_none()
    if instance:
        return instance, False
    else:
        params = {k: v for k, v in kwargs.items() if not isinstance(v, ClauseElement)}
        params.update(defaults or {})
        instance = model(**params)
        try:
            session.add(instance)
            session.commit()
        except Exception:  # The actual exception depends on the specific database so we catch all exceptions. This is similar to the official documentation: https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html
            session.rollback()
            instance = session.query(model).filter_by(**kwargs).one()
            return instance, False
        else:
            return instance, True

Mise à jour 2020 (Python 3.9+ SEULEMENT)

Voici une version plus propre avec Python 3.9's le nouvel opérateur d'union des dictées (|=)

def get_or_create(session, model, defaults=None, **kwargs):
    instance = session.query(model).filter_by(**kwargs).one_or_none()
    if instance:
        return instance, False
    else:
        kwargs |= defaults or {}
        instance = model(**kwargs)
        try:
            session.add(instance)
            session.commit()
        except Exception:  # The actual exception depends on the specific database so we catch all exceptions. This is similar to the official documentation: https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html
            session.rollback()
            instance = session.query(model).filter_by(**kwargs).one()
            return instance, False
        else:
            return instance, True

Remarque :

Comme pour la version Django, cela permet d'identifier les contraintes de clés dupliquées et les erreurs similaires. Si votre get ou create n'est pas garanti de renvoyer un seul résultat, il peut toujours en résulter des conditions de course.

Pour remédier en partie à ce problème, il faudrait ajouter un autre élément de type one_or_none() juste après le session.commit() . Ceci n'est toujours pas une garantie à 100% contre les conditions de course, à moins que vous n'utilisiez également une fonction with_for_update() ou le mode de transaction sérialisable.

2 votes

Je pense que là où vous lisez "session.Query(model.filter_by(**kwargs).first()", vous devriez lire "session.Query(model.filter_by(**kwargs)).first()".

3 votes

Devrait-il y avoir un verrou autour de cela afin qu'un autre thread ne crée pas une instance avant que ce thread n'ait la possibilité de le faire ?

2 votes

@EoghanM : Normalement, votre session devrait être threadlocal, donc cela n'a pas d'importance. La session SQLAlchemy n'est pas conçue pour être thread-safe.

59voto

erik Points 199

Je me suis penché sur ce problème et j'ai trouvé une solution assez robuste :

def get_one_or_create(session,
                      model,
                      create_method='',
                      create_method_kwargs=None,
                      **kwargs):
    try:
        return session.query(model).filter_by(**kwargs).one(), False
    except NoResultFound:
        kwargs.update(create_method_kwargs or {})
        created = getattr(model, create_method, model)(**kwargs)
        try:
            session.add(created)
            session.flush()
            return created, True
        except IntegrityError:
            session.rollback()
            return session.query(model).filter_by(**kwargs).one(), False

Je viens d'écrire un article de blog assez vaste Je ne connais pas tous les détails, mais j'ai quelques idées sur la raison pour laquelle j'ai utilisé cette méthode.

  1. Il se décompose en un tuple qui indique si l'objet a existé ou non. Cela peut souvent s'avérer utile dans votre flux de travail.

  2. La fonction permet de travailler avec @classmethod les fonctions de créateur décorées (et les attributs qui leur sont propres).

  3. La solution protège contre les conditions de course (Race Conditions) lorsque plusieurs processus sont connectés au magasin de données.

EDIT : J'ai modifié session.commit() a session.flush() comme expliqué dans cet article de blog . Notez que ces décisions sont spécifiques au datastore utilisé (Postgres dans ce cas).

EDIT 2 : J'ai mis à jour l'utilisation d'un {} comme valeur par défaut dans la fonction car il s'agit d'un problème typique de Python. Merci pour votre aide. le commentaire Nigel ! Si vous êtes curieux de savoir ce qu'il en est, consultez le site suivant cette question StackOverflow y cet article de blog .

2 votes

Par rapport à ce que Spencer dit Cette solution est la bonne puisqu'elle permet d'éviter les conditions de course (en commettant/effaçant la session, attention) et imite parfaitement ce que fait Django.

0 votes

@kiddouk Non, il n'imite pas "parfaitement". L'approche de Django get_or_create es no sans risque pour les threads. Il n'est pas atomique. De plus, la fonction get_or_create renvoie un indicateur True si l'instance a été créée ou un indicateur False dans le cas contraire.

0 votes

@Kate si vous regardez la page d'accueil de Django get_or_create il fait presque exactement la même chose. Cette solution renvoie également le True/False pour signaler si l'objet a été créé ou récupéré, et n'est pas non plus atomique. Cependant, la sécurité des threads et les mises à jour atomiques concernent la base de données, et non Django, Flask ou SQLAlchemy, et dans cette solution comme dans celle de Django, elles sont résolues par des transactions sur la base de données.

8voto

jhnwsk Points 516

Cette recette SQLALchemy fait le travail de manière agréable et élégante.

La première chose à faire est de définir une fonction qui reçoit une session à utiliser et qui associe un dictionnaire à la Session() qui garde une trace de l'état actuel de la session. unique clés.

def _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw):
    cache = getattr(session, '_unique_cache', None)
    if cache is None:
        session._unique_cache = cache = {}

    key = (cls, hashfunc(*arg, **kw))
    if key in cache:
        return cache[key]
    else:
        with session.no_autoflush:
            q = session.query(cls)
            q = queryfunc(q, *arg, **kw)
            obj = q.first()
            if not obj:
                obj = constructor(*arg, **kw)
                session.add(obj)
        cache[key] = obj
        return obj

Un exemple d'utilisation de cette fonction serait dans un mixin :

class UniqueMixin(object):
    @classmethod
    def unique_hash(cls, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def unique_filter(cls, query, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def as_unique(cls, session, *arg, **kw):
        return _unique(
                    session,
                    cls,
                    cls.unique_hash,
                    cls.unique_filter,
                    cls,
                    arg, kw
            )

Et enfin la création du modèle unique get_or_create :

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

engine = create_engine('sqlite://', echo=True)

Session = sessionmaker(bind=engine)

class Widget(UniqueMixin, Base):
    __tablename__ = 'widget'

    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True, nullable=False)

    @classmethod
    def unique_hash(cls, name):
        return name

    @classmethod
    def unique_filter(cls, query, name):
        return query.filter(Widget.name == name)

Base.metadata.create_all(engine)

session = Session()

w1, w2, w3 = Widget.as_unique(session, name='w1'), \
                Widget.as_unique(session, name='w2'), \
                Widget.as_unique(session, name='w3')
w1b = Widget.as_unique(session, name='w1')

assert w1 is w1b
assert w2 is not w3
assert w2 is not w1

session.commit()

La recette va plus loin dans l'idée et propose différentes approches, mais j'ai utilisé celle-ci avec beaucoup de succès.

3 votes

J'aime cette recette si un seul objet SQLAlchemy Session peut modifier la base de données. Je peux me tromper, mais si d'autres sessions (SQLAlchemy ou non) modifient la base de données simultanément, je ne vois pas comment cela protège contre les objets qui pourraient avoir été créés par d'autres sessions pendant que la transaction est en cours. Dans ce cas, je pense que les solutions qui s'appuient sur le flushing après session.add() et la gestion des exceptions comme stackoverflow.com/a/21146492/3690333 sont plus fiables.

4voto

thebjorn Points 3878

Le plus proche sémantiquement est probablement :

def get_or_create(model, **kwargs):
    """SqlAlchemy implementation of Django's get_or_create.
    """
    session = Session()
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance, False
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance, True

Il n'est pas certain qu'il soit kasher de s'appuyer sur une définition globale de l'économie de marché. Session dans sqlalchemy, mais la version Django ne prend pas de connexion donc...

Le tuple retourné contient l'instance et un booléen indiquant si l'instance a été créée (c'est-à-dire que c'est False si nous lisons l'instance depuis la base de données).

L'approche de Django get_or_create est souvent utilisé pour s'assurer que les données globales sont disponibles, de sorte que je m'engage le plus tôt possible.

0 votes

Cela devrait fonctionner tant que la session est créée et suivie par scoped_session qui devrait mettre en œuvre une gestion des sessions à sécurité intrinsèque (cela existait-il en 2014 ?).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X