Есть ли в SQLAlchemy эквивалент get_or_create в Django?

160

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

Джанго get_or_create(или источник ) делает это. Есть ли эквивалентный ярлык в 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
FogleBird
источник
4
Для тех, кто просто хочет добавить объект, если он еще не существует, смотрите session.merge: stackoverflow.com/questions/12297156/…
Антон Тарасенко

Ответы:

96

Это в основном способ сделать это, нет быстрого доступа 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
Wolph
источник
2
Я думаю, что если вы читаете «session.Query (model.filter_by (** kwargs) .first ()», вы должны читать «session.Query (model.filter_by (** kwargs)).
First
3
Должна ли быть блокировка вокруг этого, чтобы другой поток не создал экземпляр до того, как этот поток сможет это сделать?
EoghanM
2
@EoghanM: Обычно ваш сеанс будет локальным, поэтому это не имеет значения. Сеанс SQLAlchemy не предназначен для работы с потоками.
Вольф
5
@WolpH это может быть другой процесс, пытающийся создать одну и ту же запись одновременно. Посмотрите на реализацию get_or_create в Django. Он проверяет ошибки целостности и полагается на правильное использование уникальных ограничений.
Иван Вирабян
1
@IvanVirabyan: Я предположил, что @EoghanM говорил об экземпляре сеанса. В этом случае должно быть try...except IntegrityError: instance = session.Query(...)вокруг session.addблока.
Вольф
109

Следуя решению @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)
Кевин.
источник
3
Для тех из вас, кто ищет, как я, это правильное решение для создания строки, если она еще не существует.
Спенсер Рэтбун
3
Вам не нужно добавлять новый экземпляр в сеанс? В противном случае, если вы вызовете session.commit () в вызывающем коде, ничего не произойдет, поскольку новый экземпляр не будет добавлен в сеанс.
CadentOrange
1
Спасибо тебе за это. Я нашел это настолько полезным, что создал его для будущего использования. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
январь
где мне нужно поместить код?, я получаю отработку ошибки контекста выполнения?
Виктор Альварадо,
7
Учитывая, что вы передаете сеанс в качестве аргумента, может быть лучше избежать commit(или хотя бы использовать flushвместо него только a ). Это оставляет управление сессией вызывающей стороне этого метода и не рискует выпустить преждевременную фиксацию. Кроме того, использование one_or_none()вместо first()может быть немного безопаснее.
exhuma
52

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

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 и этот пост в блоге .

Erik
источник
1
По сравнению с тем, что говорит spencer , это решение является хорошим, поскольку оно предотвращает условия гонки (путем фиксации / сброса сеанса, будьте осторожны) и идеально имитирует то, что делает Django.
Киддук
@kiddouk Нет, это не "идеально". Джанго get_or_createэто не потокобезопасна. Это не атомно. Кроме того, Django get_or_createвозвращает флаг True, если экземпляр был создан, или флаг False в противном случае.
Кар
@ Кейт, если ты смотришь на Джанго, get_or_createэто делает почти то же самое. Это решение также возвращает True/Falseфлаг, чтобы сигнализировать, был ли объект создан или извлечен, и также не является атомарным. Однако безопасность потоков и атомарные обновления являются проблемой для базы данных, а не для Django, Flask или SQLAlchemy, и как в этом, так и в Django решениях решаются транзакциями в базе данных.
Эрик
1
Предположим, что ненулевому полю было предоставлено пустое значение для новой записи, оно вызовет IntegrityError. Все запутано, теперь мы не знаем, что на самом деле произошло, и мы получаем еще одну ошибку, что запись не найдена.
Раджат
2
Разве дело не должно IntegrityErrorвозвращаться, Falseтак как этот клиент не создал объект?
kevmitch
11

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

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. Если созданный объект имеет отношения и ему назначаются члены через эти отношения, он автоматически добавляется в сеанс. Например, создайте a 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 .

Adversus
источник
Мне нравится, что это позволяет избежать отката несвязанных изменений, однако IntegrityErrorповторный запрос все равно может завершиться неудачно с NoResultFoundуровнем изоляции MySQL по умолчанию, REPEATABLE READесли сеанс ранее запрашивал модель в той же транзакции. Лучшее решение, которое я мог бы придумать, это позвонить session.commit()перед этим запросом, что также не идеально, поскольку пользователь может этого не ожидать. Ссылочный ответ не имеет этой проблемы, так как session.rollback () имеет тот же эффект, что и запуск новой транзакции.
kevmitch
Да, пока. Сработает ли помещение запроса во вложенную транзакцию? Вы правы, что commitвнутри этой функции, возможно, хуже, чем делать rollback, хотя для определенных случаев использования это может быть приемлемо.
Adversus
Да, размещение начального запроса во вложенной транзакции позволяет, по крайней мере, работать второму запросу. Это все равно не удастся, если пользователь явно запросил модель ранее в той же транзакции. Я решил, что это приемлемо, и пользователя следует просто предупредить, чтобы он не делал этого или иным образом не ловил исключение и решал, стоит ли ему это делать commit(). Если я правильно понимаю код, это то, что делает Джанго.
kevmitch
В документации django говорится, что они используют `READ COMMITTED , so it does not look like they try to handle this. Looking at the [source](https://github.com/django/django/blob/master/django/db/models/query.py#L491) confirms this. I'm not sure I understand your reply, you mean the user should put his/her query in a nested transaction? It's not clear to me how a SAVEPOINT`, с которыми считывает влияния REPEATABLE READ. Если нет эффекта, то ситуация кажется неразрешимой, если эффект, то самый последний запрос может быть вложенным?
Adversus
Это интересно READ COMMITED, может быть, мне следует пересмотреть свое решение не трогать значения по умолчанию для базы данных. Я проверил, что восстановление SAVEPOINTдо того, как был сделан запрос, делает его так, как если бы этот запрос никогда не происходил REPEATABLE READ. Поэтому я посчитал необходимым заключить запрос в предложении try во вложенную транзакцию, чтобы запрос в предложении IntegrityErrorкроме мог работать вообще.
kevmitch
6

Это рецепт 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

Пример использования этой функции был бы в 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
            )

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

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

jhnwsk
источник
1
Мне нравится этот рецепт, если только один объект сеанса SQLAlchemy может изменить базу данных. Я могу ошибаться, но если другие сеансы (SQLAlchemy или нет) изменяют базу данных одновременно, я не вижу, как это защищает от объектов, которые могли быть созданы другими сеансами, пока транзакция продолжается. В этих случаях я думаю, что решения, которые основаны на очистке после session.add () и обработке исключений, таких как stackoverflow.com/a/21146492/3690333 , более надежны.
TrilceAC
3

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

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 не требует подключения, так что ...

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

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

thebjorn
источник
это должно работать до тех пор, пока Session создается и отслеживается scoped_session, что должно реализовывать поточно-ориентированное управление сессиями (существовало ли это в 2014 году?).
Cowbert
2

Я немного упростил @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
jmberros
источник
1

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

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

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

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

fcracker79
источник