Есть ли в SQLAlchemy эквивалент get_or_create Джанго? - PullRequest
138 голосов
/ 30 марта 2010

Я хочу получить объект из базы данных, если он уже существует (на основе предоставленных параметров), или создать его, если его нет.

Django's get_or_create (или source ) делает это. Есть ли эквивалентный ярлык в SQLAlchemy?

Я сейчас пишу это так:

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

Ответы [ 8 ]

94 голосов
/ 21 мая 2011

Следуя решению @WoLpH, вот код, который работал для меня (простая версия):

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

С этим я могу получить любой объект моей модели.

Предположим, мой объект модели:

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

Чтобы получить или создать мой объект, я пишу:

myCountry = get_or_create(session, Country, name=countryName)
83 голосов
/ 06 апреля 2010

Это в основном способ сделать это, быстрого доступа к AFAIK нет

Вы можете обобщить это конечно:

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
48 голосов
/ 15 января 2014

Я играл с этой проблемой и в итоге нашел достаточно надежное решение:

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

Я только что написал довольно обширное сообщение в блоге обо всех деталях, но несколько идей о том, почему я использовал это.

  1. Распаковывается в кортеж, который сообщает вам, существовал объект или нет. Это часто может быть полезно в вашем рабочем процессе.

  2. Функция дает возможность работать с @classmethod оформленными функциями создателя (и специфическими для них атрибутами).

  3. Решение защищает от состояния гонки, если к хранилищу данных подключено более одного процесса.

РЕДАКТИРОВАТЬ: я изменил session.commit() на session.flush(), как объяснено в этом сообщении в блоге . Обратите внимание, что эти решения зависят от используемого хранилища данных (в данном случае Postgres).

РЕДАКТИРОВАТЬ 2: я обновил, используя {} в качестве значения по умолчанию в функции, так как это типичная ошибка Python. Спасибо за комментарий , Найджел! Если вам интересно узнать об этом, проверьте этот вопрос StackOverflow и это сообщение в блоге .

10 голосов
/ 24 мая 2016

Модифицированная версия Эрика отлично ответ

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
  • Используйте вложенную транзакцию , чтобы откатить только добавление нового элемента, а не откатывать все назад (см. Этот ответ об использовании вложенных транзакций с SQLite)
  • Перемещение create_method. Если созданный объект имеет отношения и ему назначаются члены через эти отношения, он автоматически добавляется в сеанс. Например. создайте book, который имеет user_id и user в качестве соответствующих отношений, затем выполнение book.user=<user object> внутри create_method добавит book к сеансу. Это означает, что create_method должно быть внутри with, чтобы извлечь выгоду из возможного отката. Обратите внимание, что begin_nested автоматически вызывает сброс.

Обратите внимание, что при использовании MySQL уровень изоляции транзакции должен быть установлен на READ COMMITTED вместо REPEATABLE READ, чтобы это работало. get_or_create здесь ) Django использует ту же стратегию, см. Также документацию Django .

5 голосов
/ 01 июня 2013

Этот рецепт SQLALchemy делает работу красиво и элегантно.

Первое, что нужно сделать, это определить функцию, которой назначен Session для работы, и связать словарь с Session (), который отслеживает текущие уникальные ключи.

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

Примером использования этой функции может быть миксин:

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
            )

И, наконец, создание уникальной модели 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()

Рецепт углубляется в идею и предлагает различные подходы, но я использовал этот с большим успехом.

3 голосов
/ 27 сентября 2014

Наиболее близким по семантике, вероятно, является:

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

не уверен, насколько кошерно полагаться на глобально определенный Session в sqlalchemy, но версия Django не требует подключения, так что ...

Возвращенный кортеж содержит экземпляр и логическое значение, указывающее, был ли создан экземпляр (т. Е. Ложно, если мы читаем экземпляр из БД).

Django get_or_create часто используется, чтобы убедиться, что глобальные данные доступны, поэтому я фиксирую в кратчайшие сроки.

1 голос
/ 01 декабря 2016

Я немного упростил @Kevin. решение, чтобы избежать оборачивания всей функции в оператор if / else. Таким образом, есть только один return, который я считаю чище:

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 голос
/ 20 февраля 2016

В зависимости от выбранного уровня изоляции, ни одно из перечисленных выше решений не будет работать. Лучшее решение, которое я нашел, - это RAW SQL в следующей форме:

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

Это транзакционно безопасно независимо от уровня изоляции и степени параллелизма.

Осторожно: для повышения эффективности было бы целесообразно иметь ИНДЕКС для уникального столбца.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...