SQLAlchemy tem um equivalente ao get_or_create do Django?

160

Desejo obter um objeto do banco de dados, se ele já existir (com base nos parâmetros fornecidos) ou criá-lo, se não existir.

O Django get_or_create(ou fonte ) faz isso. Existe um atalho equivalente no SQLAlchemy?

Atualmente, estou escrevendo explicitamente assim:

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
fonte
4
Para aqueles que apenas deseja adicionar objeto se ele ainda não existe, consulte session.merge: stackoverflow.com/questions/12297156/...
Anton Tarasenko

Respostas:

96

Essa é basicamente a maneira de fazer isso, não há atalho prontamente disponível para o AFAIK.

Você pode generalizá-lo, é claro:

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
fonte
2
Eu acho que onde você lê "session.Query (model.filter_by (** kwargs) .first ()", você deve ler "session.Query (model.filter_by (** kwargs))..
First
3
Deveria haver um bloqueio em torno disso para que outro thread não crie uma instância antes que ele possa?
EoghanM
2
@EoghanM: Normalmente a sua sessão seria threadlocal, então isso não importa. A sessão SQLAlchemy não deve ser segura para threads.
Wolph 23/05
5
@WolpH, pode ser outro processo tentando criar o mesmo registro simultaneamente. Veja a implementação do Django de get_or_create. Ele verifica se há erros de integridade e se baseia no uso adequado de restrições exclusivas.
Ivan Virabyan
1
@IvanVirabyan: Eu assumi que @EoghanM estava falando sobre a instância da sessão. Nesse caso, deve haver uma try...except IntegrityError: instance = session.Query(...)volta ao session.addquarteirão.
Wolph
109

Seguindo a solução do @WoLpH, este é o código que funcionou para mim (versão simples):

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

Com isso, eu sou capaz de obter ou criar qualquer objeto do meu modelo.

Suponha que meu objeto de modelo seja:

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

Para obter ou criar meu objeto, escrevo:

myCountry = get_or_create(session, Country, name=countryName)
Kevin.
fonte
3
Para aqueles que pesquisam como eu, esta é a solução adequada para criar uma linha, se ela ainda não existir.
Spencer Rathbun
3
Você não precisa adicionar a nova instância à sessão? Caso contrário, se você emitir um session.commit () no código de chamada, nada acontecerá, pois a nova instância não será adicionada à sessão.
CadentOrange
1
Obrigado por isso. Achei isso tão útil que criei uma essência para uso futuro. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador
onde eu preciso colocar o código ?, eu trabalho com erro de contexto de execução?
Victor Alvarado
7
Como você passa a sessão como argumento, pode ser melhor evitar commit(ou pelo menos usar apenas um flush). Isso deixa o controle da sessão para o chamador desse método e não corre o risco de emitir uma confirmação prematura. Além disso, usar em one_or_none()vez de first()pode ser um pouco mais seguro.
Exhuma #
52

Eu estou jogando com esse problema e acabei com uma solução bastante robusta:

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

Acabei de escrever um post bastante amplo sobre todos os detalhes, mas algumas idéias sobre o porquê disso.

  1. Ele é descompactado em uma tupla que informa se o objeto existe ou não. Isso geralmente pode ser útil no seu fluxo de trabalho.

  2. A função permite trabalhar com @classmethodfunções decoradas do criador (e atributos específicos a elas).

  3. A solução protege contra condições de corrida quando você tem mais de um processo conectado ao armazenamento de dados.

EDIT: mudei session.commit()para session.flush()conforme explicado nesta postagem do blog . Observe que essas decisões são específicas para o armazenamento de dados usado (neste caso, o Postgres).

EDIT 2: Eu atualizei usando um {} como valor padrão na função, pois isso é uma pegadinha típica do Python. Obrigado pelo comentário , Nigel! Se você está curioso sobre isso, confira esta pergunta do StackOverflow e esta postagem no blog .

erik
fonte
1
Comparado com o que spencer diz , essa solução é boa, pois evita as condições de corrida (comprometendo / liberando a sessão, cuidado) e imitando perfeitamente o que o Django faz.
Kiddouk
@kiddouk Não, ele não imita "perfeitamente". O Django's nãoget_or_create é seguro para threads. Não é atômico. Além disso, o Django retorna uma flag True se a instância foi criada ou uma flag False caso contrário. get_or_create
Kar
@ Kate, se você olhar para o Django get_or_create, faz quase exatamente a mesma coisa. Essa solução também retorna o True/Falsesinalizador para sinalizar se o objeto foi criado ou buscado e também não é atômico. No entanto, a segurança de threads e as atualizações atômicas são uma preocupação para o banco de dados, não para o Django, Flask ou SQLAlchemy, e nesta solução e no Django, são resolvidos por transações no banco de dados.
315 erik
1
Suponha que um campo não nulo tenha sido fornecido com um valor nulo para um novo registro; isso aumentará o IntegrityError. A coisa toda fica bagunçada, agora não sabemos o que realmente aconteceu e recebemos outro erro: nenhum registro foi encontrado.
rajat
2
O caso não deveria IntegrityErrorretornar, Falsepois esse cliente não criou o objeto?
kevmitch
11

Uma versão modificada da excelente resposta de erik

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
  • Use uma transação aninhada para reverter apenas a adição do novo item em vez de reverter tudo (consulte esta resposta para usar transações aninhadas com SQLite)
  • Move create_method. Se o objeto criado tiver relações e receber membros por meio dessas relações, ele será adicionado automaticamente à sessão. Por exemplo, crie um book, que tenha user_ide usercomo relacionamento correspondente, e fazer book.user=<user object>dentro dele create_methodserá adicionado bookà sessão. Isso significa que create_methoddeve estar dentro withpara se beneficiar de uma eventual reversão. Observe que begin_nesteddispara automaticamente um flush.

Observe que, se você estiver usando o MySQL, o nível de isolamento da transação deve ser definido como READ COMMITTEDe não REPEATABLE READpara que isso funcione. O get_or_create do Django (e aqui ) usa o mesmo estratagema, veja também a documentação do Django .

Adversus
fonte
Eu gosto que isso evite reverter alterações não relacionadas, no entanto, a IntegrityErrorre-consulta ainda poderá falhar NoResultFoundcom o nível de isolamento padrão do MySQL REPEATABLE READse a sessão tiver consultado anteriormente o modelo na mesma transação. A melhor solução que eu poderia encontrar é ligar session.commit()antes dessa consulta, o que também não é o ideal, pois o usuário pode não esperar. A resposta referenciada não tem esse problema, pois o session.rollback () tem o mesmo efeito de iniciar uma nova transação.
Kevmitch # 23/16
TIL. Colocar a consulta em uma transação aninhada funcionaria? Você está certo de que, commitdentro dessa função, é discutivelmente pior do que fazer a rollback, embora para casos de uso específicos possa ser aceitável.
Adversus
Sim, colocar a consulta inicial em uma transação aninhada torna pelo menos possível que a segunda consulta funcione. Ele ainda falhará se o usuário tiver consultado explicitamente o modelo antes na mesma transação. Decidi que isso é aceitável e o usuário deve ser avisado para não fazer isso ou capturar a exceção e decidir se é o caso commit(). Se meu entendimento do código está correto, é isso que o Django faz.
Kevmitch #
Na documentação do django , eles dizem para usar as , 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 influências do `READ COMMITTED SAVEPOINT` REPEATABLE READ. Se nenhum efeito, então, a situação parece inalcançável; se efeito, a última consulta pode ser aninhada?
Adversus
Isso é interessante READ COMMITED, talvez eu deva repensar minha decisão de não tocar nos padrões do banco de dados. Eu testei que a restauração de um SAVEPOINTantes de uma consulta ser feita faz com que ela nunca aconteça REPEATABLE READ. Portanto, achei necessário incluir a consulta na cláusula try em uma transação aninhada para que a consulta na IntegrityErrorcláusula exceto possa funcionar.
Kevmitch # 8/16
6

Esta receita SQLALchemy faz o trabalho agradável e elegante.

A primeira coisa a fazer é definir uma função que recebe uma Sessão para trabalhar e associa um dicionário à Session () que controla as chaves exclusivas atuais .

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

Um exemplo de utilização dessa função seria em um 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
            )

E, finalmente, criando o modelo exclusivo 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()

A receita vai mais fundo na idéia e oferece abordagens diferentes, mas eu usei essa com muito sucesso.

jhnwsk
fonte
1
Eu gosto desta receita se apenas um único objeto SQLAlchemy Session puder modificar o banco de dados. Posso estar errado, mas se outras sessões (SQLAlchemy ou não) modificarem o banco de dados simultaneamente, não vejo como isso protege contra objetos que podem ter sido criados por outras sessões enquanto a transação está em andamento. Nesses casos, acho que as soluções que dependem da liberação após session.add () e do tratamento de exceções, como stackoverflow.com/a/21146492/3690333, são mais confiáveis.
precisa saber é o seguinte
3

O mais próximo semanticamente é provavelmente:

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

não tenho certeza de quão kosher é confiar em um definido globalmente Sessionno sqlalchemy, mas a versão do Django não aceita uma conexão, então ...

A tupla retornada contém a instância e um booleano indicando se a instância foi criada (ou seja, é False se lemos a instância no banco de dados).

O Django's get_or_createé frequentemente usado para garantir que dados globais estejam disponíveis, por isso estou comprometendo o mais cedo possível.

thebjorn
fonte
isso deve funcionar desde que a sessão seja criada e rastreada scoped_session, o que deve implementar o gerenciamento de sessões com segurança de threads (isso existia em 2014?).
cowbert
2

Simplifiquei levemente o @Kevin. solução para evitar agrupar toda a função em uma instrução if/ else. Desta forma, há apenas um return, que eu acho mais limpo:

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
fonte
1

Dependendo do nível de isolamento adotado, nenhuma das soluções acima funcionaria. A melhor solução que encontrei é um SQL RAW no seguinte formato:

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

Isso é transacionalmente seguro, independentemente do nível de isolamento e do grau de paralelismo.

Cuidado: para torná-lo eficiente, seria aconselhável ter um ÍNDICE para a coluna exclusiva.

fcracker79
fonte