SQLAlchemy: Membuat vs. Menggunakan Kembali Sesi

98

Hanya pertanyaan singkat: SQLAlchemy berbicara tentang menelepon sessionmaker()sekali tetapi memanggil Session()kelas yang dihasilkan setiap kali Anda perlu berbicara dengan DB Anda. Bagi saya itu berarti saat saya akan melakukan yang pertama session.add(x)atau yang serupa, yang pertama akan saya lakukan

from project import Session
session = Session()

Apa yang saya lakukan sampai sekarang adalah melakukan panggilan session = Session()dalam model saya sekali dan kemudian selalu mengimpor sesi yang sama di mana saja dalam aplikasi saya. Karena ini adalah aplikasi web, ini biasanya berarti sama (seperti satu tampilan dijalankan).

Tapi dimana bedanya? Apa kerugian menggunakan satu sesi sepanjang waktu dibandingkan menggunakannya untuk hal-hal database saya sampai fungsi saya selesai dan kemudian membuat yang baru saat saya ingin berbicara dengan DB saya lagi?

Saya mengerti bahwa jika saya menggunakan banyak utas, masing-masing harus mendapatkan sesi mereka sendiri. Tapi dengan menggunakan scoped_session(), saya sudah memastikan bahwa masalah itu tidak ada, bukan?

Harap klarifikasi jika ada asumsi saya yang salah.

javex
sumber

Jawaban:

225

sessionmaker()adalah sebuah pabrik, itu ada untuk mendorong penempatan opsi konfigurasi untuk membuat Sessionobjek baru hanya di satu tempat. Ini opsional, karena Anda dapat dengan mudah menelepon Session(bind=engine, expire_on_commit=False)kapan pun Anda membutuhkan yang baru Session, kecuali yang verbose dan mubazir, dan saya ingin menghentikan penyebaran "pembantu" skala kecil yang masing-masing mendekati masalah redundansi ini di beberapa dan cara yang lebih membingungkan.

Jadi sessionmaker()hanyalah alat untuk membantu Anda membuat Sessionobjek saat Anda membutuhkannya.

Bagian selanjutnya. Saya pikir pertanyaannya adalah, apa perbedaan antara membuat yang baru Session()di berbagai titik versus hanya menggunakan satu sepenuhnya. Jawabannya, tidak terlalu banyak. Sessionadalah wadah untuk semua objek yang Anda masukkan ke dalamnya, dan kemudian juga melacak transaksi terbuka. Pada saat Anda menelepon rollback()atau commit(), transaksi selesai, dan Sessiontidak memiliki koneksi ke database hingga dipanggil untuk mengeluarkan SQL lagi. Tautan yang dipegangnya ke objek yang dipetakan adalah referensi yang lemah, asalkan objek bersih dari perubahan yang tertunda, bahkan dalam hal itu Sessionkehendak akan mengosongkan dirinya kembali ke keadaan baru ketika aplikasi Anda kehilangan semua referensi ke objek yang dipetakan. Jika Anda membiarkannya dengan default"expire_on_commit"pengaturan, maka semua objek kedaluwarsa setelah komit. Jika itu Sessionhang sekitar selama lima atau dua puluh menit, dan semua jenis hal telah berubah dalam database saat Anda menggunakannya lagi, itu akan memuat semua status baru saat Anda mengakses objek itu lagi meskipun mereka telah duduk di memori selama dua puluh menit.

Dalam aplikasi web, kami biasanya berkata, hai mengapa Anda tidak membuat yang baru Sessiondi setiap permintaan, daripada menggunakan yang sama berulang kali. Praktik ini memastikan bahwa permintaan baru mulai "bersih". Jika beberapa objek dari permintaan sebelumnya belum dikumpulkan sampah, dan jika Anda telah menonaktifkan "expire_on_commit", mungkin beberapa status dari permintaan sebelumnya masih berkeliaran, dan bahkan mungkin cukup lama. Jika Anda berhati-hati untuk membiarkan expire_on_commitmenyala dan pasti menelepon commit()atau rollback()atas permintaan, maka tidak apa-apa, tetapi jika Anda memulai dengan yang baru Session, maka tidak ada pertanyaan bahwa Anda mulai bersih. Jadi ide untuk memulai setiap permintaan dengan yang baruSessionsebenarnya hanyalah cara paling sederhana untuk memastikan Anda memulai dari awal, dan membuat penggunaan expire_on_commitcukup banyak opsional, karena tanda ini dapat menyebabkan banyak SQL tambahan untuk operasi yang memanggil commit()di tengah rangkaian operasi. Tidak yakin apakah ini menjawab pertanyaan Anda.

Babak berikutnya adalah apa yang Anda sebutkan tentang threading. Jika aplikasi Anda multithread, sebaiknya pastikan yang Sessiondigunakan bersifat lokal untuk ... sesuatu. scoped_session()secara default menjadikannya lokal untuk utas saat ini. Dalam aplikasi web, lokal ke permintaan bahkan lebih baik. Flask-SQLAlchemy sebenarnya mengirimkan "fungsi cakupan" kustom scoped_session()sehingga Anda mendapatkan sesi yang mencakup permintaan. Rata-rata aplikasi Piramida memasukkan Sesi ke dalam registri "permintaan". Saat menggunakan skema seperti ini, ide "buat Sesi baru berdasarkan permintaan" terus terlihat seperti cara paling mudah untuk menjaga semuanya tetap lurus.

zzzeek
sumber
17
Wow, ini menjawab semua pertanyaan saya di bagian SQLAlchemy dan bahkan menambahkan beberapa info tentang Flask dan Pyramid! Bonus tambahan: jawaban pengembang;) Saya berharap saya dapat memilih lebih dari sekali. Terima kasih banyak!
javex
Satu klarifikasi, jika memungkinkan: Anda mengatakan expire_on_commit "dapat menyebabkan banyak SQL ekstra" ... dapatkah Anda memberikan detail lebih lanjut? Saya pikir expire_on_commit hanya memperhatikan apa yang terjadi di RAM, bukan apa yang terjadi di database.
Veky
3
expire_on_commit dapat menghasilkan lebih banyak SQL jika Anda menggunakan kembali Sesi yang sama, dan beberapa objek masih berkeliaran di Sesi itu, saat Anda mengaksesnya, Anda akan mendapatkan PILIH baris tunggal untuk masing-masing karena masing-masing di-refresh secara individual status mereka dalam hal transaksi baru.
zzzeek
1
Hai, @zzzeek. Terima kasih atas jawaban yang sangat bagus. Saya sangat baru di python dan beberapa hal yang ingin saya klarifikasi: 1) Apakah saya mengerti benar ketika saya membuat "sesi" baru dengan memanggil metode Session () itu akan membuat Transaksi SQL, kemudian transaksi akan dibuka sampai saya melakukan / memutar kembali sesi ? 2) Apakah session () menggunakan semacam kolam koneksi atau membuat koneksi baru ke sql setiap kali?
Alex Gurskiy
27

Selain jawaban zzzeek yang luar biasa, berikut ini resep sederhana untuk membuat sesi tertutup sendiri dengan cepat:

from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

@contextmanager
def db_session(db_url):
    """ Creates a context with an open SQLAlchemy session.
    """
    engine = create_engine(db_url, convert_unicode=True)
    connection = engine.connect()
    db_session = scoped_session(sessionmaker(autocommit=False, autoflush=True, bind=engine))
    yield db_session
    db_session.close()
    connection.close()

Pemakaian:

from mymodels import Foo

with db_session("sqlite://") as db:
    foos = db.query(Foo).all()
Berislav Lopac
sumber
3
Adakah alasan mengapa Anda tidak hanya membuat sesi baru, tetapi juga koneksi baru?
danqing
Tidak juga - ini adalah contoh cepat untuk menunjukkan mekanismenya, meskipun masuk akal untuk membuat semuanya segar dalam pengujian, di mana saya paling sering menggunakan pendekatan ini. Seharusnya mudah untuk memperluas fungsi ini dengan koneksi sebagai argumen opsional.
Berislav Lopac