SQLAlchemy에는 Django의 get_or_create와 동등한 기능이 있습니까?


166

이미 존재하는 경우 (제공된 매개 변수를 기반으로) 데이터베이스에서 객체를 가져 오거나 그렇지 않은 경우 생성하고 싶습니다.

장고 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/...
안톤 타라 센코

답변:


106

이것이 기본적으로 수행하는 방법이며 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

2020 업데이트

다음은 Python 3.9의 새로운 dict 공용체 연산자 (| =) 가 포함 된 더 깨끗한 버전입니다.

def get_or_create(session, Model, defaults=None, **kwargs):
    instance = session.query(Model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        kwargs |= defaults or {}
        instance = Model(**kwargs)
        session.add(instance)
        return instance

2
"session.Query (model.filter_by (** kwargs) .first ()"를 읽을 때 "session.Query (model.filter_by (** kwargs)). first ()"를
읽어야한다고 생각합니다

3
이 스레드가 기회를 갖기 전에 다른 스레드가 인스턴스를 만들지 않도록이 주위에 잠금이 있어야합니까?
EoghanM

2
@EoghanM : 일반적으로 세션은 스레드 로컬이므로 중요하지 않습니다. SQLAlchemy 세션은 스레드로부터 안전하지 않습니다.
Wolph

5
@WolpH 동일한 레코드를 동시에 만들려는 또 다른 프로세스 일 수 있습니다. Django의 get_or_create 구현을 살펴보십시오. 무결성 오류를 확인하고 고유 한 제약 조건의 적절한 사용에 의존합니다.
Ivan Virabyan

1
@IvanVirabyan : @EoghanM이 세션 인스턴스에 대해 이야기하고 있다고 가정했습니다. 이 경우 블록 try...except IntegrityError: instance = session.Query(...)주위에 있어야합니다 session.add.
Wolph

115

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

3
나처럼 검색하는 사람들에게는 행이 아직 존재하지 않는 경우 행을 만드는 적절한 솔루션입니다.
Spencer Rathbun 2012

3
새 인스턴스를 세션에 추가 할 필요가 없습니까? 그렇지 않으면 호출 코드에서 session.commit ()을 발행하면 새 인스턴스가 세션에 추가되지 않으므로 아무 일도 일어나지 않습니다.
CadentOrange

1
감사합니다. 나는 이것이 매우 유용하다는 것을 알았으므로 나중에 사용할 수 있도록 요점을 만들었습니다. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

코드를 어디에 넣어야합니까?, 실행 컨텍스트 오류가 발생합니까?
Victor Alvarado

7
세션을 인수로 전달하면를 피하는 것이 좋습니다 commit(또는 최소한 flush대신 a 만 사용). 이렇게하면이 메서드의 호출자에게 세션 제어가 남고 조기 커밋을 실행할 위험이 없습니다. 또한 one_or_none()대신 사용 하는 first()것이 약간 더 안전 할 수 있습니다.
exhuma

54

나는이 문제를 가지고 놀았고 상당히 강력한 솔루션으로 끝났습니다.

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 gotcha이므로 함수의 기본값으로 {}를 사용하여 업데이트했습니다. 댓글 주셔서 감사합니다 , 나이젤! 이 잡았다 대한 호기심 경우, 체크 아웃 이 StackOverflow의 질문이 블로그 게시물을 .


1
spencer가 말한 것과 비교할 때 ,이 솔루션은 경쟁 조건 (세션을 커밋 / 플러시함으로써)을 방지하고 Django가하는 일을 완벽하게 모방하기 때문에 좋은 솔루션입니다.
kiddouk 2014 년

@kiddouk 아니요, "완벽하게"모방하지 않습니다. Django get_or_create는 스레드로부터 안전 하지 않습니다 . 원자가 아닙니다. 또한 Django get_or_create는 인스턴스가 생성 된 경우 True 플래그를 반환하고 그렇지 않은 경우 False 플래그를 반환합니다.
Kar

@Kate 장고를 보면 get_or_create거의 똑같은 일을합니다. 이 솔루션은 또한 True/False객체가 생성되었거나 가져 왔는지 여부를 알리는 플래그를 반환하며 원 자성이 아닙니다. 그러나 스레드 안전성 및 원자 적 업데이트는 Django, Flask 또는 SQLAlchemy가 아닌 데이터베이스의 문제이며,이 솔루션과 Django 모두에서 데이터베이스의 트랜잭션으로 해결됩니다.
erik

1
새 레코드에 대해 null이 아닌 필드에 null 값이 제공되었다고 가정하면 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. Django의 get_or_create (및 여기 )는 동일한 전략을 사용합니다 . Django 문서 도 참조하세요 .


나는 이것이 관련되지 않은 변경 사항을 롤백하는 것을 피하는 것을 좋아하지만 세션이 이전에 동일한 트랜잭션에서 모델을 쿼리 한 경우 MySQL 기본 격리 수준으로 IntegrityError다시 쿼리가 여전히 실패 할 수 있습니다 . 제가 생각해 낼 수있는 가장 좋은 해결책은 이 쿼리 를 호출 하기 전에 호출 하는 것입니다. 이는 사용자가 예상하지 못할 수도 있기 때문에 이상적이지 않습니다. session.rollback ()이 새 트랜잭션을 시작하는 것과 동일한 효과를 갖기 때문에 참조 된 답변에는이 문제가 없습니다. NoResultFoundREPEATABLE READsession.commit()
kevmitch

허, TIL. 쿼리를 중첩 된 트랜잭션에 넣을 수 있습니까? 것을 당신 맞아요 commit이 함수의 내부가하는 것보다 틀림없이 더 나쁘다 rollback특정 사용 사례 것이 허용 될 수 있지만,.
Adversus

예, 초기 쿼리를 중첩 된 트랜잭션에 넣으면 최소한 두 번째 쿼리가 작동 할 수 있습니다. 사용자가 동일한 트랜잭션에서 이전에 모델을 명시 적으로 쿼리 한 경우에도 여전히 실패합니다. 나는 이것이 수용 가능하다고 결정했고 사용자는 이것을하지 않도록 경고하거나 예외를 포착하고 commit()스스로 결정해야 합니다. 코드에 대한 나의 이해가 맞다면 이것은 Django가하는 일입니다.
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 2011

흥미로운 점은 READ COMMITED데이터베이스 기본값을 건드리지 않기로 한 내 결정을 재고해야 할 것입니다. SAVEPOINT쿼리가 만들어지기 전에 를 복원 하면 해당 쿼리가 REPEATABLE READ. 따라서 IntegrityErrorexcept 절의 쿼리가 전혀 작동 할 수 있도록 중첩 트랜잭션의 try 절에 쿼리를 묶어야한다는 것을 알았습니다 .
kevmitch

6

이 SQLALchemy 레시피 는 훌륭하고 우아한 작업을 수행합니다.

가장 먼저해야 할 일은 작업 할 세션이 제공되는 함수를 정의하고 현재 고유 키 를 추적하는 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()

레시피는 아이디어에 더 깊이 들어가고 다양한 접근 방식을 제공하지만이 방법을 성공적으로 사용했습니다.


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

전 세계적으로 정의 된 Sessionsqlalchemy에서 하지 않지만 Django 버전은 연결을 취하지 않습니다.

반환 된 튜플은 인스턴스와 인스턴스가 생성되었는지 여부를 나타내는 부울을 포함합니다 (즉, db에서 인스턴스를 읽으면 False입니다).

Django get_or_create는 전역 데이터를 사용할 수 있는지 확인하는 데 자주 사용되므로 가능한 한 빠른 시점에 커밋하고 있습니다.


이것은 scoped_session스레드로부터 안전한 세션 관리를 구현해야하는 세션이 생성되고 추적되는 한 작동합니다 (2014 년에 존재 했습니까?).
cowbert

3

@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를 사용하는 것이 좋습니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.