SQLAlchemy'nin Django'nun get_or_create eşdeğeri var mı?


161

Ben zaten (sağlanan parametrelere dayalı) varsa veritabanından bir nesne almak veya yoksa oluşturmak istiyorum.

Django's get_or_create(veya source ) bunu yapar. SQLAlchemy'de eşdeğer bir kısayol var mı?

Şu anda açıkça şöyle yazıyorum:

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
Nesne az eklemek isteyenler için görmek, henüz yoksa session.merge: stackoverflow.com/questions/12297156/...
Anton TARASENKO

Yanıtlar:


96

Temel olarak bunu yapmanın yolu, AFAIK'in hazır bir kısayolu yok.

Elbette genelleştirebilirsiniz:

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
Ben "session.Query (model.filter_by (** kwargs) .first ()" okuduğunuz yerde, "session.Query (model.filter_by (** kwargs)). İlk ()" okumak gerekir düşünüyorum.
pkoch

3
Başka bir iş parçacığının, bu iş parçacığı için bir şansı olmadan önce bir örnek oluşturmaması için bunun etrafında bir kilit olmalı mı?
EoghanM

2
@EoghanM: Normalde oturumunuz threadlocal olur, bu da önemli değildir. SQLAlchemy oturumu iş parçacığı için güvenli değildir.
Wolph

5
@WolpH aynı kaydı aynı anda oluşturmaya çalışan başka bir işlem olabilir. Django'nun get_or_create uygulamasına bakın. Bütünlük hatasını kontrol eder ve benzersiz kısıtlamaların uygun şekilde kullanılmasına dayanır.
Ivan Virabyan

1
@IvanVirabyan: @EoghanM'in oturum örneği hakkında konuştuğunu varsaydım. Bu durumda blokun try...except IntegrityError: instance = session.Query(...)etrafında bir tane olmalıdır session.add.
Wolph

109

@WoLpH çözümünün ardından, bu benim için çalışan kod (basit sürüm):

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

Bununla, modelimin herhangi bir nesnesini elde edebiliyorum.

Varsayalım benim model nesnem:

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

Nesnemi almak veya oluşturmak için şunu yazıyorum:

myCountry = get_or_create(session, Country, name=countryName)

3
Benim gibi arama yapanlar için, zaten yoksa bir satır oluşturmak için uygun çözüm budur.
Spencer Rathbun

3
Oturuma yeni örneği eklemenize gerek yok mu? Aksi takdirde, arama kodunda bir session.commit () düzenlerseniz, yeni örnek oturuma eklenmediğinden hiçbir şey olmaz.
CadentOrange

1
Bunun için teşekkür ederim. Bunu o kadar kullanışlı buldum ki, ileride kullanmak üzere bir özet oluşturdum. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

nerede kod koymak gerekir ?, idam bağlam hatası çalışma dışarı olsun?
Victor Alvarado

7
Oturumu argüman olarak ilettiğiniz göz önüne alındığında, commit(veya en azından flushbunun yerine yalnızca bir tane kullanmaktan kaçınmak) daha iyi olabilir . Bu, oturum kontrolünü bu yöntemin arayanına bırakır ve erken bir taahhüt verme riski taşımaz. Ayrıca, one_or_none()yerine kullanmak first()biraz daha güvenli olabilir.
exhuma

53

Bu sorunla oynuyorum ve oldukça sağlam bir çözüm buldum:

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

Tüm detaylar hakkında oldukça geniş bir blog yazısı yazdım , ancak bunu neden kullandığım hakkında birkaç fikir.

  1. Nesnenin var olup olmadığını söyleyen bir demet açar. Bu genellikle iş akışınızda yararlı olabilir.

  2. İşlev, @classmethodsüslü içerik oluşturucu işlevleriyle (ve bunlara özgü niteliklerle) çalışma yeteneği verir .

  3. Veri deposuna bağlı birden fazla işleminiz olduğunda çözüm Yarış Koşullarına karşı koruma sağlar.

DÜZENLEME: ben değiştim session.commit()için session.flush()açıklandığı gibi bu blog yayınında . Bu kararların kullanılan veri deposuna özel olduğunu unutmayın (bu durumda Postgres).

DÜZENLEME 2: Tipik bir Python gotcha olduğundan işlevde varsayılan değer olarak bir {} kullanarak güncelledim. Yorum için teşekkürler Nigel! Bu sorun hakkında merak ediyorsanız, bu StackOverflow sorusuna ve bu blog gönderisine göz atın .


1
Spencer'in söylediklerine kıyasla , bu çözüm Yarış koşullarını (oturumu gerçekleştirerek / yıkayarak, dikkat ederek) önlediğinden ve Django'nun yaptıklarını mükemmel bir şekilde taklit ettiği için iyi bir çözümdür.
kiddouk

@kiddouk Hayır, "mükemmel" değil. Django adlı get_or_createbir değil parçacığı güvenli. Atomik değil. Ayrıca, get_or_createörnek oluşturulmuşsa Django's True bayrağını, aksi halde False bayrağını döndürür.
Kar

@ Datego'ya bakarsanız get_or_createneredeyse aynı şeyi yapar. Bu çözüm aynı zamanda True/Falsenesnenin yaratılıp getirilip getirilmediğini göstermek için bayrağını döndürür ve atomik de değildir. Ancak, iş parçacığı güvenliği ve atomik güncelleştirmeler veritabanı için bir endişe kaynağıdır, Django, Flask veya SQLAlchemy için değil ve hem bu çözümde hem de Django'larda veritabanı üzerindeki işlemler tarafından çözülür.
erik

1
Yeni bir kayıt için null olmayan bir alanın null değeri sağlandığını varsayalım, IntegrityError değerini yükseltir. Her şey berbat durumda, şimdi gerçekte ne olduğunu bilmiyoruz ve başka bir hata alıyoruz, kayıt bulunamadı.
rajat

2
Olmamalı IntegrityErrorvaka dönüş Falsebu istemci beri nesneyi oluşturmadı?
kevmitch

11

Erik'in mükemmel cevabının değiştirilmiş bir versiyonu

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
  • Bir kullan iç içe işlem geri tek rulo yerine her şeyi geri haddeleme yeni ürünün eklenmesi (bu Bkz cevabı SQLite ile iç içe geçmiş işlem kullanmak için)
  • Yürü create_method. Oluşturulan nesnenin ilişkileri varsa ve bu ilişkiler yoluyla üyelere atanırsa, otomatik olarak oturuma eklenir. Örneğin book, bir ilişkisi olan user_idve userbuna karşılık gelen bir ilişki oluşturmak, daha sonra book.user=<user object>içinde yapmak oturuma create_methodkatkıda bulunacaktır book. Bu, geri dönüşten yararlanmak create_methodiçin içeride olması gerektiği anlamına gelir with. begin_nestedBir yıkamayı otomatik olarak tetiklediğini unutmayın .

MySQL kullanılıyorsa, işlem yalıtım seviyesinin çalışması için bunun READ COMMITTEDyerine ayarlanması gerekir REPEATABLE READ. Django'nun get_or_create (ve burada ) aynı stratagem'i kullanır, ayrıca Django belgelerine de bakın .


Ben oturum daha önce aynı işlem modeli sorguladı , ancak IntegrityErroryeniden sorgu hala NoResultFoundMySQL varsayılan yalıtım düzeyi ile başarısız olabilir , bu ilgisiz değişiklikleri geri önler gibi REPEATABLE READ. Gelebileceğim en iyi çözüm, session.commit()bu sorgudan önce aramaktır, bu da kullanıcı beklemeyebileceği için ideal değildir. Session.rollback () yöntemi yeni bir işlem başlatma ile aynı etkiye sahip olduğundan, başvurulan yanıtta bu sorun yoktur.
kevmitch

Ha, TIL. Sorguyu iç içe bir işleme koymak işe yarar mı? Belirli kullanım durumları için kabul edilebilir olsa da commit, bu fonksiyonun içinde bir yapmaktan daha kötü olduğu konusunda haklısınız rollback.
Adversus

Evet, ilk sorguyu iç içe bir işleme yerleştirmek en azından ikinci sorgunun çalışmasını mümkün kılar. Yine de, kullanıcı aynı işlemde daha önce modeli açıkça sorguladıysa da başarısız olur. Bunun kabul edilebilir olduğuna karar verdim ve kullanıcı sadece bunu yapmamak veya istisnayı yakalamak ve commit()kendilerine karar vermek konusunda uyarılmalıdır . Kod hakkındaki anlayışım doğruysa, Django bunu yapar.
kevmitch

Django belgelerinde, 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 okudukları `` TAAHHÜT TASARRUFU OKUYUN '' etkilerini kullandıklarını söylüyorlar REPEATABLE READ. Hiçbir etki yoksa durum kurtarılamaz gibi görünüyor, eğer etki o zaman son sorgu iç içe olabilir?
Adversus

Bu ilginç READ COMMITED, belki de veritabanı varsayılanlarına dokunmama kararımı yeniden düşünmeliyim. Ben SAVEPOINTbir sorgu yapılmış önce bir geri yükleme test sanki bu sorgu asla oldu sanki test ettim REPEATABLE READ. Bu nedenle, dış tümce içinde sorgu IntegrityErrorhiç çalışabilmesi için sorgu iç içe bir işlemde try yan tümcesi içine gerekli buldum .
kevmitch

6

Bu SQLALchemy tarifi işi güzel ve zarif yapıyor.

Yapılacak ilk şey, çalışmak için bir Oturum verilen bir işlevi tanımlamak ve geçerli benzersiz anahtarları izleyen Session () ile bir sözlük ilişkilendirmektir .

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

Bu işlevi kullanmanın bir örneği bir mixin'de olabilir:

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
            )

Ve son olarak benzersiz get_or_create modelini yaratın:

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

Tarif fikrin derinliklerine iner ve farklı yaklaşımlar sağlar, ancak bunu büyük bir başarıyla kullandım.


1
Yalnızca tek bir SQLAlchemy Session nesnesi veritabanını değiştirebilirse bu tarifi beğendim. Yanlış olabilir, ancak diğer oturumlar (SQLAlchemy ya da değil) aynı anda veritabanını değiştirmek, bu işlem devam ederken diğer oturumlar tarafından oluşturulmuş olabilir nesnelere karşı nasıl koruduğunu görmüyorum. Bu gibi durumlarda, session.add () 'den sonra yıkamaya ve stackoverflow.com/a/21146492/3690333 gibi kural dışı durum işlemlerine dayanan çözümlerin daha güvenilir olduğunu düşünüyorum.
TrilceAC

3

Anlamsal olarak en yakın olanı muhtemelen:

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

küresel olarak tanımlanmış bir güvenin ne kadar koşucu olduğundan emin değilim Sessionsqlalchemy'de güvenin koşucu değilim, ancak Django sürümü bir bağlantı almıyor bu yüzden ...

Döndürülen demet, örneği ve örneğin yaratıldığını belirten bir boole içerir (örneğin, örneği db'den okursak False olur).

Django's get_or_creategenellikle küresel verilerin mevcut olduğundan emin olmak için kullanılır, bu yüzden mümkün olan en erken noktada taahhüt ediyorum.


Oturum oluşturulduğu ve izlendiği sürece scoped_session, iş parçacığı için güvenli oturum yönetimi uygulanmalıdır (2014'te mevcut mu?).
cowbert

2

@Kevin'i biraz basitleştirdim. if/ elsedeyimine tüm işlevi sarmaktan kaçınmak için çözüm Bu şekilde returndaha temiz bulduğum sadece bir tane var :

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

Kabul ettiğiniz izolasyon seviyesine bağlı olarak, yukarıdaki çözümlerin hiçbiri işe yaramaz. Bulduğum en iyi çözüm aşağıdaki biçimde bir RAW SQL'dir:

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

İzolasyon seviyesi ve paralellik derecesi ne olursa olsun işlemsel olarak güvenlidir.

Dikkat: Verimli hale getirmek için, benzersiz sütun için bir INDEX'e sahip olmak akıllıca olacaktır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.