Есть ли в SQLAlchemy эквивалент get_or_create Джанго?

104

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

Django 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
  • 1
    Для тех, кто просто хочет добавить объект, если он еще не существует, см. session.merge : stackoverflow.com/questions/12297156/…
Теги:
sqlalchemy

8 ответов

66
Лучший ответ

Что в основном способ сделать это, нет ярлыка, легко доступного 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
  • 2
    Я думаю, что там, где вы читаете "session.Query (model.filter_by (** kwargs) .first ()"), вы должны читать "session.Query (model.filter_by (** kwargs)). First ()".
  • 0
    @pkoch: так и должно быть, спасибо :)
Показать ещё 19 комментариев
70

Следуя решению @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

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

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

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

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

myCountry = get_or_create(session, Country, name=countryName)
  • 2
    Для тех из вас, кто ищет меня, это правильное решение для создания строки, если она еще не существует.
  • 2
    Вам не нужно добавлять новый экземпляр в сеанс? В противном случае, если вы вызовете session.commit () в вызывающем коде, ничего не произойдет, поскольку новый экземпляр не будет добавлен в сеанс.
Показать ещё 3 комментария
33

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

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

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

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

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

  • Решение защищает от условий гонки, когда у вас есть несколько процессов, связанных с хранилищем данных.

EDIT: я изменил session.commit() на session.flush(), как описано в в этом сообщении в блоге. Обратите внимание, что эти решения специфичны для используемого хранилища данных (Postgres в этом случае).

EDIT 2: Ive обновлен, используя {} в качестве значения по умолчанию в функции, поскольку это типичный Python. Спасибо за комментарий, Найджел! Если вам интересно об этом, прочтите https://stackoverflow.com/questions/5712904/empty-dictionary-as-default-value-for-keyword-argument-in-python-function-dicti и этот пост в блоге.

  • 1
    По сравнению с тем, что говорит spencer, это решение является хорошим, поскольку оно предотвращает условия гонки (путем фиксации / очистки сессии, остерегается) и идеально имитирует то, что делает Django.
  • 2
    Это должен быть принятый ответ !!!
Показать ещё 6 комментариев
6

Измененная версия erik отлично answer

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

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

  • 0
    Мне нравится, что это позволяет избежать отката несвязанных изменений, однако повторный запрос IntegrityError все еще может завершиться ошибкой с NoResultFound с уровнем изоляции MySQL по умолчанию REPEATABLE READ если сеанс ранее запрашивал модель в той же транзакции. Лучшее решение, которое я мог бы предложить, - это вызвать session.commit() перед этим запросом, что также не идеально, так как пользователь может этого не ожидать. Ссылочный ответ не имеет этой проблемы, так как session.rollback () имеет тот же эффект, что и запуск новой транзакции.
  • 0
    Да, пока. Будет ли работать запрос во вложенной транзакции? Вы правы в том, что commit внутри этой функции, возможно, хуже, чем rollback , хотя для определенных случаев использования он может быть приемлемым.
Показать ещё 3 комментария
6

Думаю, я просто искал то же самое. Этот рецепт SQLALchemy делает работу приятной и элегантной.

3

Скорее всего семантически возможно:

def get_or_create(model, **kwargs):
    """SqlAlchemy implementation of Django 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, если мы читаем экземпляр из db).

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

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

Я слегка упростил @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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню