Apakah SQLAlchemy setara dengan get_or_create Django?

160

Saya ingin mendapatkan objek dari database jika sudah ada (berdasarkan parameter yang disediakan) atau membuatnya jika tidak ada.

Django get_or_create(atau sumber ) melakukan ini. Apakah ada cara pintas yang setara di SQLAlchemy?

Saat ini saya menuliskannya secara eksplisit seperti ini:

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
sumber
4
Bagi mereka yang hanya ingin menambahkan objek jika belum ada, lihat session.merge: stackoverflow.com/questions/12297156/…
Anton Tarasenko

Jawaban:

96

Itu pada dasarnya cara untuk melakukannya, tidak ada jalan pintas yang tersedia AFAIK.

Anda dapat menggeneralisasikannya tentu saja:

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
sumber
2
Saya pikir di mana Anda membaca "session.Query (model.filter_by (** kwargs) .first ()", Anda harus membaca "session.Query (model.filter_by (** kwargs)). First ()".
pkoch
3
Haruskah ada kunci di sekitar ini sehingga utas lain tidak membuat instance sebelum utas ini memiliki kesempatan?
EoghanM
2
@ EoghanM: Biasanya sesi Anda akan menjadi threadlocal jadi ini tidak masalah. Sesi SQLAlchemy tidak dimaksudkan untuk thread-safe.
Wolph
5
@WolpH itu bisa menjadi proses lain yang mencoba untuk membuat catatan yang sama secara bersamaan. Lihatlah implementasi Django dari get_or_create. Ini memeriksa kesalahan integritas, dan bergantung pada penggunaan yang tepat dari kendala unik.
Ivan Virabyan
1
@IvanVirabyan: Saya berasumsi @EoghanM berbicara tentang contoh sesi. Dalam hal ini harus ada di try...except IntegrityError: instance = session.Query(...)sekitar session.addblok.
Wolph
109

Mengikuti solusi @WoLpH, ini adalah kode yang berfungsi untuk saya (versi sederhana):

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

Dengan ini, saya bisa mendapatkan_atau membuat objek model saya.

Misalkan objek model saya adalah:

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

Untuk mendapatkan atau membuat objek saya, saya menulis:

myCountry = get_or_create(session, Country, name=countryName)
Kevin.
sumber
3
Bagi Anda yang mencari seperti saya, ini adalah solusi tepat untuk membuat baris jika belum ada.
Spencer Rathbun
3
Tidakkah Anda perlu menambahkan instance baru ke sesi? Kalau tidak, jika Anda mengeluarkan session.commit () dalam kode panggilan, tidak ada yang akan terjadi karena instance baru tidak ditambahkan ke sesi.
CadentOrange
1
Terima kasih untuk ini. Saya telah menemukan ini sangat berguna sehingga saya membuat intisari untuk digunakan di masa depan. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador
di mana saya harus meletakkan kode ?, saya bisa menyelesaikan kesalahan konteks eksekusi?
Victor Alvarado
7
Mengingat Anda lulus sesi sebagai argumen, mungkin lebih baik untuk menghindari commit(atau setidaknya hanya menggunakan a flush). Ini meninggalkan kontrol sesi kepada penelepon metode ini dan tidak akan mengambil risiko mengeluarkan komit prematur. Juga, menggunakan one_or_none()bukannya first()mungkin sedikit lebih aman.
exhuma
52

Saya telah bermain dengan masalah ini dan berakhir dengan solusi yang cukup kuat:

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

Saya hanya menulis posting blog yang cukup luas pada semua detail, tetapi beberapa ide mengapa saya menggunakan ini.

  1. Itu membongkar ke sebuah tuple yang memberitahu Anda jika objek itu ada atau tidak. Ini sering berguna dalam alur kerja Anda.

  2. Fungsi ini memberikan kemampuan untuk bekerja dengan @classmethodfungsi-fungsi pencipta yang didekorasi (dan atribut khusus untuk mereka).

  3. Solusinya melindungi terhadap Kondisi Ras ketika Anda memiliki lebih dari satu proses yang terhubung ke datastore.

EDIT: Saya sudah berubah session.commit()menjadi session.flush()seperti yang dijelaskan dalam posting blog ini . Perhatikan bahwa keputusan ini khusus untuk datastore yang digunakan (Postgres dalam kasus ini).

EDIT 2: Saya telah memperbarui menggunakan {} sebagai nilai default dalam fungsi karena ini adalah gotcha khas Python. Terima kasih atas komentarnya , Nigel! Jika Anda penasaran dengan gotcha ini, lihat pertanyaan StackOverflow ini dan posting blog ini .

erik
sumber
1
Dibandingkan dengan apa yang dikatakan spencer , solusi ini adalah solusi yang baik karena mencegah kondisi Balapan (dengan melakukan / menyiram sesi, waspada) dan meniru dengan sempurna apa yang dilakukan Django.
kiddouk
@ kiddouk Tidak, itu tidak meniru "sempurna". Django get_or_createadalah tidak benang-aman. Itu bukan atom. Juga, Django get_or_createmengembalikan bendera Benar jika turunannya dibuat atau bendera Palsu sebaliknya.
Kar
@ Kate jika Anda melihat Django get_or_createitu melakukan hal yang hampir sama persis. Solusi ini juga mengembalikan True/Falsebendera ke sinyal jika objek dibuat atau diambil, dan juga bukan atom. Namun, keamanan thread dan pembaruan atom menjadi perhatian untuk database, bukan untuk Django, Flask atau SQLAlchemy, dan dalam kedua solusi ini dan Django, diselesaikan dengan transaksi pada database.
erik
1
Misalkan bidang yang bukan nol diberikan nilai nol untuk rekaman baru, itu akan meningkatkan IntegrityError. Semuanya menjadi kacau, sekarang kita tidak tahu apa yang sebenarnya terjadi dan kita mendapatkan kesalahan lain, bahwa tidak ada catatan yang ditemukan.
rajat
2
Haruskah IntegrityErrorcase kembali Falsekarena klien ini tidak membuat objek?
kevmitch
11

Versi modifikasi dari jawaban erik yang sangat baik

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
  • Gunakan transaksi bersarang untuk hanya memutar kembali penambahan item baru alih-alih mengembalikan semuanya (Lihat jawaban ini untuk menggunakan transaksi bersarang dengan SQLite)
  • Pindahkan create_method. Jika objek yang dibuat memiliki hubungan dan itu ditugaskan anggota melalui hubungan tersebut, itu secara otomatis ditambahkan ke sesi. Misalnya membuat book, yang memiliki user_iddan usersebagai hubungan yang sesuai, maka melakukan book.user=<user object>di dalam create_methodakan menambah booksesi. Ini berarti bahwa create_methodharus ada di dalam withuntuk mendapatkan keuntungan dari rollback akhirnya. Perhatikan bahwa begin_nestedsecara otomatis memicu flush.

Perhatikan bahwa jika menggunakan MySQL, level isolasi transaksi harus diatur agar READ COMMITTEDtidak REPEATABLE READberfungsi. Get_or_create Django (dan di sini ) menggunakan strategi yang sama, lihat juga dokumentasi Django .

Adversus
sumber
Saya suka bahwa ini menghindari mengembalikan perubahan yang tidak terkait, namun permintaan IntegrityErrorulang mungkin masih gagal dengan NoResultFoundtingkat isolasi default MySQL REPEATABLE READjika sesi sebelumnya menanyakan model dalam transaksi yang sama. Solusi terbaik yang bisa saya buat adalah menelepon session.commit()sebelum permintaan ini, yang juga tidak ideal karena pengguna mungkin tidak mengharapkannya. Jawaban yang dirujuk tidak memiliki masalah ini karena session.rollback () memiliki efek yang sama untuk memulai transaksi baru.
kevmitch
Hah, TIL. Apakah menempatkan kueri dalam transaksi bertumpuk berfungsi? Anda benar bahwa commitdi dalam fungsi ini bisa dibilang lebih buruk daripada melakukan rollback, meskipun untuk kasus penggunaan khusus dapat diterima.
Adversus
Ya, menempatkan kueri awal dalam transaksi bersarang memungkinkan setidaknya kueri kedua berfungsi. Ini masih akan gagal jika pengguna secara eksplisit menanyakan model sebelumnya dalam transaksi yang sama. Saya telah memutuskan bahwa ini dapat diterima dan pengguna hanya harus diperingatkan untuk tidak melakukan ini atau menangkap pengecualian dan memutuskan apakah akan melakukannya commit()sendiri. Jika pemahaman saya tentang kode itu benar, inilah yang dilakukan Django.
kevmitch
Dalam dokumentasi Django mereka mengatakan untuk menggunakan , 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 pengaruh `BACA BERKOMITMEN BACA` yang dibaca dengan REPEATABLE READ. Jika tidak ada efek maka situasinya tampaknya tidak dapat diselamatkan, jika efek maka permintaan terakhir dapat disarangkan?
Adversus
Itu menarik READ COMMITED, mungkin saya harus memikirkan kembali keputusan saya untuk tidak menyentuh default database. Saya telah menguji bahwa memulihkan SAVEPOINTdari sebelum permintaan dibuat membuatnya seolah-olah permintaan itu tidak pernah diterima REPEATABLE READ. Oleh karena itu, saya merasa perlu melampirkan kueri dalam klausa coba dalam transaksi bersarang sehingga kueri dalam IntegrityErrorklausa kecuali dapat bekerja sama sekali.
kevmitch
6

Resep SQLALchemy ini melakukan pekerjaan dengan baik dan elegan.

Hal pertama yang harus dilakukan adalah mendefinisikan fungsi yang diberi Sesi untuk dikerjakan, dan mengaitkan kamus dengan Sesi () yang melacak kunci unik saat ini .

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

Contoh penggunaan fungsi ini adalah dalam 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
            )

Dan akhirnya membuat model get_or_create unik:

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

Resepnya masuk lebih dalam ke ide dan memberikan pendekatan yang berbeda tetapi saya telah menggunakan ini dengan sukses besar.

jhnwsk
sumber
1
Saya suka resep ini jika hanya objek SQLAlchemy Session tunggal yang dapat memodifikasi database. Saya mungkin salah, tetapi jika sesi lain (SQLAlchemy atau tidak) memodifikasi database secara bersamaan, saya tidak melihat bagaimana ini melindungi terhadap objek yang mungkin telah dibuat oleh sesi lain saat transaksi sedang berlangsung. Dalam kasus tersebut, saya pikir solusi yang mengandalkan pembilasan setelah session.add () dan penanganan pengecualian seperti stackoverflow.com/a/21146492/3690333 lebih dapat diandalkan.
TrilceAC
3

Semantik terdekat mungkin:

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

tidak yakin bagaimana halal bergantung pada yang didefinisikan secara global Sessiondalam sqlalchemy, tetapi versi Django tidak mengambil koneksi jadi ...

Tuple yang dikembalikan berisi instance dan boolean yang menunjukkan jika instance dibuat (yaitu False jika kita membaca instance dari db).

Django get_or_createsering digunakan untuk memastikan bahwa data global tersedia, jadi saya berkomitmen sedini mungkin.

sebelum lahir
sumber
ini harus bekerja selama Sesi dibuat dan dilacak oleh scoped_session, yang harus menerapkan manajemen sesi thread-safe (apakah ini ada pada 2014?).
cowbert
2

Saya sedikit menyederhanakan @Kevin. solusi untuk menghindari pembungkus seluruh fungsi dalam if/ elsepernyataan. Dengan cara ini hanya ada satu return, yang saya temukan lebih bersih:

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

Tergantung pada tingkat isolasi yang Anda adopsi, tidak ada solusi di atas yang akan berfungsi. Solusi terbaik yang saya temukan adalah RAW SQL dalam bentuk berikut:

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

Ini aman secara transaksi apa pun tingkat isolasi dan tingkat paralelisme.

Hati-hati: untuk membuatnya efisien, akan lebih bijaksana untuk memiliki INDEX untuk kolom unik.

fcracker79
sumber