Memperbarui database secara efisien menggunakan SQLAlchemy ORM

116

Saya memulai aplikasi baru dan melihat menggunakan ORM - khususnya, SQLAlchemy.

Katakanlah saya punya kolom 'foo' di database saya dan saya ingin menambahnya. Di sqlite lurus, ini mudah:

db = sqlite3.connect('mydata.sqlitedb')
cur = db.cursor()
cur.execute('update table stuff set foo = foo + 1')

Saya menemukan persamaan SQLAlchemy SQL-builder:

engine = sqlalchemy.create_engine('sqlite:///mydata.sqlitedb')
md = sqlalchemy.MetaData(engine)
table = sqlalchemy.Table('stuff', md, autoload=True)
upd = table.update(values={table.c.foo:table.c.foo+1})
engine.execute(upd)

Ini sedikit lebih lambat, tapi tidak banyak di dalamnya.

Inilah tebakan terbaik saya untuk pendekatan SQLAlchemy ORM:

# snip definition of Stuff class made using declarative_base
# snip creation of session object
for c in session.query(Stuff):
    c.foo = c.foo + 1
session.flush()
session.commit()

Ini melakukan hal yang benar, tetapi itu membutuhkan waktu kurang dari lima puluh kali selama dua pendekatan lainnya. Saya kira itu karena harus membawa semua data ke dalam memori sebelum dapat bekerja dengannya.

Apakah ada cara untuk menghasilkan SQL yang efisien menggunakan ORM SQLAlchemy? Atau menggunakan ORM python lainnya? Atau haruskah saya kembali menulis SQL dengan tangan?

John Fouhy
sumber
1
Oke, saya berasumsi jawabannya adalah "ini bukan sesuatu yang dilakukan ORM dengan baik". Baiklah; Saya hidup dan belajar.
John Fouhy
Ada beberapa eksperimen yang dijalankan pada ORM yang berbeda dan bagaimana eksperimen tersebut bekerja di bawah beban dan tekanan. Tidak memiliki tautan yang berguna, tetapi layak dibaca.
Matthew Schinckel
Masalah lain yang ada dengan contoh terakhir (ORM) adalah bahwa ia tidak atom .
Marian

Jawaban:

181

ORM SQLAlchemy dimaksudkan untuk digunakan bersama dengan lapisan SQL, bukan menyembunyikannya. Tetapi Anda harus selalu mengingat satu atau dua hal saat menggunakan ORM dan SQL biasa dalam transaksi yang sama. Pada dasarnya, dari satu sisi, modifikasi data ORM hanya akan masuk ke database saat Anda menghapus perubahan dari sesi Anda. Dari sisi lain, pernyataan manipulasi data SQL tidak memengaruhi objek yang ada di sesi Anda.

Jadi jika Anda berkata

for c in session.query(Stuff).all():
    c.foo = c.foo+1
session.commit()

itu akan melakukan apa yang dikatakannya, mengambil semua objek dari database, memodifikasi semua objek dan kemudian ketika saatnya untuk menghapus perubahan ke database, perbarui baris satu per satu.

Sebaliknya Anda harus melakukan ini:

session.execute(update(stuff_table, values={stuff_table.c.foo: stuff_table.c.foo + 1}))
session.commit()

Ini akan dijalankan sebagai satu kueri seperti yang Anda harapkan, dan karena setidaknya konfigurasi sesi default kedaluwarsa semua data dalam sesi saat komit, Anda tidak memiliki masalah data lama.

Dalam seri 0.5 yang hampir dirilis, Anda juga dapat menggunakan metode ini untuk memperbarui:

session.query(Stuff).update({Stuff.foo: Stuff.foo + 1})
session.commit()

Itu pada dasarnya akan menjalankan pernyataan SQL yang sama seperti cuplikan sebelumnya, tetapi juga memilih baris yang diubah dan menghentikan data usang apa pun dalam sesi tersebut. Jika Anda tahu Anda tidak menggunakan data sesi apa pun setelah pembaruan, Anda juga dapat menambahkan synchronize_session=Falseke pernyataan pembaruan dan menghilangkan pilihan itu.

Semut Aasma
sumber
2
dengan cara ke-3, apakah itu akan memicu acara orm (seperti after_update)?
Ken
@Ken, tidak, tidak akan. Lihat dokumen API untuk Query.update docs.sqlalchemy.org/en/13/orm/… . Alih-alih Anda memiliki acara untuk after_bulk_update docs.sqlalchemy.org/en/13/orm/…
TrilceAC
91
session.query(Clients).filter(Clients.id == client_id_list).update({'status': status})
session.commit()

Coba ini =)

Vin
sumber
Metode ini berhasil untuk saya. Tapi masalahnya lambat. Diperlukan waktu yang cukup lama untuk beberapa 100k catatan data. Apakah mungkin ada metode yang lebih cepat?
baermathias
Terima kasih banyak, pendekatan ini berhasil untuk saya. Sangat buruk bahwa sqlachemy tidak memiliki cara yang lebih pendek untuk memperbarui jsonkolom
Jai Prakash
6
Bagi mereka yang masih mengalami masalah kinerja saat menggunakan metode ini: secara default ini mungkin melakukan PILIHAN untuk setiap rekaman terlebih dahulu, dan hanya UPDATE setelahnya. Meneruskan synchronize_session = False ke metode update () mencegah hal ini terjadi, tetapi pastikan untuk hanya melakukan ini jika Anda tidak menggunakan objek yang Anda perbarui lagi sebelum commit ().
teuneboon
25

Ada beberapa cara untuk MEMPERBARUI menggunakan sqlalchemy

1) for c in session.query(Stuff).all():
       c.foo += 1
   session.commit()

2) session.query().\
       update({"foo": (Stuff.foo + 1)})
   session.commit()

3) conn = engine.connect()
   stmt = Stuff.update().\
       values(Stuff.foo = (Stuff.foo + 1))
   conn.execute(stmt)
Nima Soroush
sumber
6

Berikut adalah contoh cara menyelesaikan masalah yang sama tanpa harus memetakan bidang secara manual:

from sqlalchemy import Column, ForeignKey, Integer, String, Date, DateTime, text, create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.attributes import InstrumentedAttribute

engine = create_engine('postgres://postgres@localhost:5432/database')
session = sessionmaker()
session.configure(bind=engine)

Base = declarative_base()


class Media(Base):
  __tablename__ = 'media'
  id = Column(Integer, primary_key=True)
  title = Column(String, nullable=False)
  slug = Column(String, nullable=False)
  type = Column(String, nullable=False)

  def update(self):
    s = session()
    mapped_values = {}
    for item in Media.__dict__.iteritems():
      field_name = item[0]
      field_type = item[1]
      is_column = isinstance(field_type, InstrumentedAttribute)
      if is_column:
        mapped_values[field_name] = getattr(self, field_name)

    s.query(Media).filter(Media.id == self.id).update(mapped_values)
    s.commit()

Jadi untuk memperbarui instance Media, Anda dapat melakukan sesuatu seperti ini:

media = Media(id=123, title="Titular Line", slug="titular-line", type="movie")
media.update()
pembajak tanah
sumber
1

Dengan pengujian yang cukup, saya akan mencoba:

for c in session.query(Stuff).all():
     c.foo = c.foo+1
session.commit()

(IIRC, commit () bekerja tanpa flush ()).

Saya telah menemukan bahwa terkadang melakukan kueri besar dan kemudian melakukan iterasi dengan python bisa mencapai 2 kali lipat lebih cepat daripada banyak kueri. Saya berasumsi bahwa iterasi pada objek query kurang efisien daripada iterasi pada daftar yang dihasilkan oleh metode all () dari objek query.

[Harap perhatikan komentar di bawah - ini tidak mempercepat sama sekali].

Matthew Schinckel
sumber
2
Menambahkan .all () dan menghapus .flush () tidak mengubah waktu sama sekali.
John Fouhy
1

Jika itu karena overhead dalam hal membuat objek, maka itu mungkin tidak bisa dipercepat sama sekali dengan SA.

Jika itu karena memuat objek terkait, maka Anda mungkin dapat melakukan sesuatu dengan pemuatan lambat. Apakah ada banyak objek yang dibuat karena referensi? (Yaitu, mendapatkan objek Perusahaan juga mendapatkan semua objek Orang terkait).

Matthew Schinckel
sumber
Nah, mejanya sendiri-sendiri. Saya belum pernah menggunakan ORM sebelumnya - apakah ini hanya sesuatu yang buruk bagi mereka?
John Fouhy
1
Ada biaya tambahan karena membuat Objek, tetapi menurut saya itu sepadan dengan hukuman - mampu menyimpan objek secara terus-menerus dalam database itu luar biasa.
Matthew Schinckel