web-dev-qa-db-fra.com

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

Je souhaite obtenir un objet de la base de données s'il existe déjà (en fonction des paramètres fournis) ou le créer s'il ne l'est pas.

Django's get_or_create (ou source ) fait ceci. Existe-t-il un raccourci équivalent dans SQLAlchemy?

Je l'écris actuellement 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
142
FogleBird

C'est fondamentalement la façon de le faire, il n'y a pas de raccourci AFAIK disponible.

Vous pouvez le généraliser bien sûr:

def get_or_create(session, model, defaults=None, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance, False
    else:
        params = dict((k, v) for k, v in kwargs.iteritems() if not isinstance(v, ClauseElement))
        params.update(defaults or {})
        instance = model(**params)
        session.add(instance)
        return instance, True
86
Wolph

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

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

Supposons que mon objet modèle est:

class Country(Base):
    __table= '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)
96
Kevin.

J'ai joué avec ce problème et j'ai fini avec 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 volumineux sur tous les détails, mais quelques idées assez pour expliquer pourquoi je l'ai utilisé.

  1. Il décompresse en un tuple qui vous indique si l'objet existait ou non. Cela peut souvent être utile dans votre flux de travail.

  2. La fonction donne la possibilité de travailler avec @classmethod Les fonctions du créateur décorées (et leurs attributs spécifiques).

  3. La solution protège contre les conditions de concurrence lorsque plusieurs processus sont connectés au magasin de données.

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

EDIT 2: J'ai mis à jour en utilisant {} comme valeur par défaut dans la fonction car c'est typique Python gotcha. Merci pour le commentaire , Nigel! Si votre curieux de savoir ce qui est arrivé, jetez un œil à cette question de StackOverflow et cet article de blog .

49
erik

Une version modifiée de l'excellent erik réponse

def get_one_or_create(session,
                      model,
                      create_method='',
                      create_method_kwargs=None,
                      **kwargs):
    try:
        return session.query(model).filter_by(**kwargs).one(), True
    except NoResultFound:
        kwargs.update(create_method_kwargs or {})
        try:
            with session.begin_nested():
                created = getattr(model, create_method, model)(**kwargs)
                session.add(created)
            return created, False
        except IntegrityError:
            return session.query(model).filter_by(**kwargs).one(), True
  • Utilisez un transaction imbriquée pour annuler uniquement l'ajout du nouvel élément au lieu de tout annuler (voir ceci réponse pour utiliser des transactions imbriquées avec SQLite)
  • Bouge toi create_method. Si l'objet créé a des relations et que des membres lui sont affectés via ces relations, il est automatiquement ajouté à la session. Par exemple. créer un book, qui a user_id et user en tant que relation correspondante, puis en faisant book.user=<user object> à l'intérieur de create_method ajoutera book à la session. Cela signifie que create_method doit être à l'intérieur de with pour pouvoir éventuellement revenir en arrière. Notez que begin_nested déclenche automatiquement un flush.

Notez que si vous utilisez MySQL, le niveau d’isolation de la transaction doit être défini sur READ COMMITTED plutôt que REPEATABLE READ pour que cela fonctionne. Django's get_or_create (et ici ) utilise le même stratagème, voir aussi le Django documentation .

10
Adversus

Cette recette SQLALchemy fait le travail bien et élégant.

La première chose à faire est de définir une fonction avec laquelle une session doit être utilisée, et associe un dictionnaire à la session () qui garde une trace de l’actuel 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, créer le 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):
    __table= '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.

5
jhnwsk

La sémantique la plus proche 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

je ne sais pas comment il est possible de s’appuyer sur un Session défini globalement dans sqlalchemy, mais la version Django ne prend pas une connexion, alors ...

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

Django's get_or_create est souvent utilisé pour s'assurer que des données globales sont disponibles. Je m'engage dès que possible.

3
thebjorn

Selon le niveau d'isolement que vous avez adopté, aucune des solutions ci-dessus ne fonctionnerait. La meilleure solution que j'ai trouvée est un RAW SQL sous la forme suivante:

INSERT INTO table(f1, f2, unique_f3) 
SELECT 'v1', 'v2', 'v3' 
WHERE NOT EXISTS (SELECT 1 FROM table WHERE f3 = 'v3')

Ceci est sécurisé sur le plan des transactions quels que soient le niveau d’isolement et le degré de parallélisme.

Attention: pour le rendre efficace, il serait sage d'avoir un INDEX pour la colonne unique.

1
fcracker79

J'ai légèrement simplifié @ Kevin. solution pour éviter d’envelopper toute la fonction dans une instruction if/else. De cette façon, il n’ya qu’un seul return que je trouve plus propre:

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()

    if not instance:
        instance = model(**kwargs)
        session.add(instance)

    return instance
1
jmberros