Чи є в 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

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

2
Я думаю , що там , де ви читаєте "session.Query (model.filter_by (** kwargs) .first ()", ви повинні читати "session.Query (model.filter_by (** kwargs)) перший ().".
pkoch

3
Чи повинен бути замок навколо цього, щоб інший потік не створив екземпляр, перш ніж ця нитка має шанс?
EoghanM

2
@EoghanM: Зазвичай ваш сеанс буде нитковим, тому це не має значення. Сеанс SQLAlchemy не повинен бути безпечним для потоків.
Вольф

5
@WolpH це може бути інший процес, який намагається створити один і той же запис одночасно. Подивіться на реалізацію Django get_or_create. Він перевіряє наявність помилок цілісності та спирається на належне використання унікальних обмежень.
Іван Вірабян

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
jangeador

де мені потрібно поставити код? я опрацьовую помилку контексту виконання?
Віктор Альварадо

7
Зважаючи на те, що ви передаєте сеанс як аргумент, може бути краще уникати commit(або принаймні використовувати лише flushзамість нього). Це залишає контроль сеансу для абонента цього методу і не ризикує надати передчасне виконання. Крім того, використання one_or_none()замість цього first()може бути дещо безпечнішим.
ексгума

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. Рішення захищає від умов перегонів, коли у вас зберігається більше одного процесу.

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

EDIT 2: Я оновив, використовуючи {} як значення за замовчуванням у функції, оскільки це типовий параметр Python. Дякую за коментар , Найджеле! Якщо вас цікавить ця проблема , перегляньте це питання StackOverflow та цю публікацію в блозі .


1
У порівнянні з тим, що говорить Спенсер , це рішення є гарним, оскільки запобігає умовам гонки (виконуючи / пропускаючи сесію, остерігайтеся) і ідеально імітує те, що робить Джанго.
kiddouk

@kiddouk Ні, це не імітує "ідеально". Джанго get_or_createце НЕ потокобезпечна. Це не атомне. Також Django get_or_createповертає True-прапор, якщо створений екземпляр або False flag іншим чином.
Кар

@Kate, якщо ви подивитеся на Django's, get_or_createце робить майже те саме. Це рішення також повертає True/Falseпрапор для сигналізації, якщо об'єкт був створений або вилучений, а також не є атомним. Однак безпека потоків та атомні оновлення викликають занепокоєння для бази даних, а не для Django, Flask або SQLAlchemy, і в цьому рішенні, і в Django вирішуються транзакції над базою даних.
Ерік

1
Припустимо, для нового запису було вказано нульове значення, воно підвищить IntegrityError. Вся справа псується, зараз ми не знаємо, що насправді сталося, і ми отримуємо ще одну помилку, що жодної записи не знайдено.
rajat

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. Якщо створений об'єкт має відносини і йому призначаються члени через ці відносини, він автоматично додається до сеансу. Наприклад, створіть 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 .


Мені подобається, що це дозволяє уникнути відкочування непов’язаних змін, однак IntegrityErrorповторний запит все ще може бути невдалим із NoResultFoundрівнем ізоляції MySQL за замовчуванням, REPEATABLE READякщо сеанс раніше запитував модель у тій же транзакції. Найкраще рішення, яке я міг би придумати, - це зателефонувати session.commit()перед цим запитом, який також не є ідеальним, оскільки користувач може цього не очікувати. Відповідна відповідь не має цієї проблеми, оскільки session.rollback () має той же ефект, що і при запуску нової транзакції.
кевміч

Так, ТІЛ. Чи введений запит буде вкладеною транзакцією? Ви маєте рацію, що commitвсередині цієї функції, можливо, гірше, ніж виконувати rollback, хоча для конкретних випадків використання це може бути прийнятним.
Adversus

Так, введення початкового запиту в вкладену транзакцію дає можливість принаймні працювати другому запиту. Він все одно не зможе, якщо користувач явно поставив запит на модель раніше в тій же транзакції. Я вирішив, що це прийнятно, і потрібно просто попередити користувача, щоб цього не робити чи іншим чином вилучити виняток, і вирішити, чи варто commit()їм. Якщо моє розуміння коду є правильним, саме це робить Джанго.
kevmitch

У документації про джанго вони говорять про використання "ЧИТАТИ , 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 ЗВ'ЯЗАНО ЗБЕРЕЖЕНО" впливів, що читаються з REPEATABLE READ. Якщо ніякого ефекту тоді ситуація не здається незмінною, якщо ефект, то останній запит може бути вкладений?
Адверс

Це цікаво READ COMMITED, можливо, я повинен переосмислити своє рішення не торкатися стандартних баз даних. Я перевірив, що відновлення SAVEPOINTз до початку запиту робить так, ніби цей запит ніколи не виникає REPEATABLE READ. Тому я вважав за необхідне вкласти в запит спробу вкладений транзакцію, щоб запит у IntegrityErrorпункті, окрім пункту, міг працювати взагалі.
кевміч

6

Цей рецепт SQLALchemy робить роботу приємною та елегантною.

Перше, що потрібно зробити, - це визначити функцію, з якою надано сесії для роботи, і пов'язує словник із сесією (), яка відслідковує поточні унікальні ключі.

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

Рецепт заглиблюється в ідею та пропонує різні підходи, але я використовував цей із великим успіхом.


1
Мені подобається цей рецепт, якщо лише один об’єкт SQLAlchemy Session може змінювати базу даних. Я можу помилятися, але якщо інші сеанси (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 не має зв'язку, тому ...

Повернутий кортеж містить екземпляр та булеве значення, що вказують, чи створений екземпляр (тобто це помилково, якщо ми читаємо екземпляр з db).

get_or_createЧасто Django використовується для того, щоб переконатися, що доступні глобальні дані, тому я беру на себе зобов’язання якомога раніше.


це має працювати до тих пір, поки буде створено та відстежено сесію scoped_session, яка повинна впровадити безпечне управління сеансами (чи існувало це у 2014 році?).
коуберт

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

1

Залежно від рівня ізоляції, який ви прийняли, жодне з перерахованих вище рішень не працює. Найкраще рішення, яке я знайшов - це RAW SQL у такій формі:

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

Це транзакційно безпечно, незалежно від рівня ізоляції та ступеня паралелізму.

Остерігайтеся: для того, щоб зробити його ефективним, було б розумно створити INDEX для унікальної колонки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.