คู่มือที่ครอบคลุมเกี่ยวกับการจัดการเซสชัน SQLAlchemy ใน Python โดยเน้นที่เทคนิคการจัดการธุรกรรมที่แข็งแกร่งเพื่อให้มั่นใจถึงความสมบูรณ์และความสอดคล้องของข้อมูลในแอปพลิเคชันของคุณ
การจัดการเซสชัน Python SQLAlchemy: การควบคุมการจัดการธุรกรรมเพื่อความสมบูรณ์ของข้อมูล
SQLAlchemy เป็นไลบรารี Python ที่มีประสิทธิภาพและยืดหยุ่น ซึ่งมีชุดเครื่องมือที่ครอบคลุมสำหรับการโต้ตอบกับฐานข้อมูล หัวใจสำคัญของ SQLAlchemy คือแนวคิดของ เซสชัน ซึ่งทำหน้าที่เป็นพื้นที่เตรียมการสำหรับการดำเนินการทั้งหมดที่คุณทำกับฐานข้อมูลของคุณ การจัดการเซสชันและธุรกรรมที่เหมาะสมเป็นสิ่งสำคัญอย่างยิ่งสำหรับการรักษาความสมบูรณ์ของข้อมูลและรับประกันพฤติกรรมของฐานข้อมูลที่สอดคล้องกัน โดยเฉพาะอย่างยิ่งในแอปพลิเคชันที่ซับซ้อนซึ่งจัดการคำขอพร้อมกัน
ทำความเข้าใจเกี่ยวกับเซสชัน SQLAlchemy
เซสชัน SQLAlchemy แสดงถึงหน่วยงานของงาน ซึ่งเป็นการสนทนากับฐานข้อมูล โดยจะติดตามการเปลี่ยนแปลงที่เกิดขึ้นกับออบเจ็กต์ ทำให้คุณสามารถบันทึกการเปลี่ยนแปลงเหล่านั้นลงในฐานข้อมูลเป็นการดำเนินการแบบอะตอมมิกเดียวได้ คิดว่ามันเป็นพื้นที่ทำงานที่คุณทำการแก้ไขข้อมูลก่อนที่จะบันทึกอย่างเป็นทางการ หากไม่มีเซสชันที่มีการจัดการที่ดี คุณจะเสี่ยงต่อความไม่สอดคล้องของข้อมูลและการเสียหายที่อาจเกิดขึ้น
การสร้างเซสชัน
ก่อนที่คุณจะเริ่มโต้ตอบกับฐานข้อมูลของคุณ คุณต้องสร้างเซสชัน ซึ่งเกี่ยวข้องกับการสร้างการเชื่อมต่อกับฐานข้อมูลโดยใช้เอ็นจินของ SQLAlchemy ก่อน
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
# Database connection string
db_url = 'sqlite:///:memory:' # Replace with your database URL (e.g., PostgreSQL, MySQL)
# Create an engine
engine = create_engine(db_url, echo=False) # echo=True to see the generated SQL
# Define a base for declarative models
Base = declarative_base()
# Define a simple model
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
def __repr__(self):
return f""
# Create the table in the database
Base.metadata.create_all(engine)
# Create a session class
Session = sessionmaker(bind=engine)
# Instantiate a session
session = Session()
ในตัวอย่างนี้:
- เรานำเข้าโมดูล SQLAlchemy ที่จำเป็น
- เรากำหนดสตริงการเชื่อมต่อฐานข้อมูล (`db_url`) ตัวอย่างนี้ใช้ฐานข้อมูล SQLite ในหน่วยความจำเพื่อความเรียบง่าย แต่คุณจะแทนที่ด้วยสตริงการเชื่อมต่อที่เหมาะสมกับระบบฐานข้อมูลของคุณ (เช่น PostgreSQL, MySQL) รูปแบบเฉพาะจะแตกต่างกันไปตามเอ็นจินฐานข้อมูลและไดรเวอร์ที่คุณใช้ โปรดดูเอกสารประกอบ SQLAlchemy และเอกสารประกอบของผู้ให้บริการฐานข้อมูลของคุณสำหรับรูปแบบสตริงการเชื่อมต่อที่ถูกต้อง
- เราสร้าง `engine` โดยใช้ `create_engine()` เอ็นจินมีหน้าที่จัดการพูลการเชื่อมต่อและการสื่อสารกับฐานข้อมูล พารามิเตอร์ `echo=True` สามารถเป็นประโยชน์สำหรับการดีบัก เนื่องจากจะพิมพ์คำสั่ง SQL ที่สร้างขึ้นไปยังคอนโซล
- เรากำหนดคลาสฐาน (`Base`) โดยใช้ `declarative_base()` ซึ่งใช้เป็นคลาสฐานสำหรับโมเดล SQLAlchemy ทั้งหมดของเรา
- เรากำหนดโมเดล `User` โดยแมปไปยังตารางฐานข้อมูลที่ชื่อ `users`
- เราสร้างตารางในฐานข้อมูลโดยใช้ `Base.metadata.create_all(engine)`
- เราสร้างคลาสเซสชันโดยใช้ `sessionmaker(bind=engine)` ซึ่งกำหนดค่าคลาสเซสชันให้ใช้เอ็นจินที่ระบุ
- สุดท้าย เราสร้างเซสชันโดยใช้ `Session()`
ทำความเข้าใจเกี่ยวกับธุรกรรม
ธุรกรรม คือลำดับของการดำเนินการฐานข้อมูลที่ถือเป็นหน่วยงานเชิงตรรกะเดียว ธุรกรรมเป็นไปตามคุณสมบัติ ACID:
- Atomicity: การดำเนินการทั้งหมดในธุรกรรมสำเร็จทั้งหมดหรือล้มเหลวทั้งหมด หากส่วนใดส่วนหนึ่งของธุรกรรมล้มเหลว ธุรกรรมทั้งหมดจะถูกยกเลิก
- Consistency: ธุรกรรมต้องรักษาฐานข้อมูลให้อยู่ในสถานะที่ถูกต้อง จะละเมิดข้อจำกัดหรือกฎของฐานข้อมูลไม่ได้
- Isolation: ธุรกรรมพร้อมกันจะถูกแยกออกจากกัน การเปลี่ยนแปลงที่เกิดขึ้นจากธุรกรรมหนึ่งจะไม่สามารถมองเห็นได้โดยธุรกรรมอื่นจนกว่าธุรกรรมแรกจะ commit
- Durability: เมื่อธุรกรรมถูก commit การเปลี่ยนแปลงจะถาวรและจะรอดพ้นแม้กระทั่งความล้มเหลวของระบบ
SQLAlchemy มีกลไกในการจัดการธุรกรรม เพื่อให้มั่นใจว่าคุณสมบัติ ACID เหล่านี้ได้รับการดูแล
การจัดการธุรกรรมขั้นพื้นฐาน
การดำเนินการธุรกรรมที่พบบ่อยที่สุดคือ commit และ rollback
การ Commit ธุรกรรม
เมื่อการดำเนินการทั้งหมดภายในธุรกรรมเสร็จสมบูรณ์แล้ว คุณจะ commit ธุรกรรม ซึ่งจะบันทึกการเปลี่ยนแปลงลงในฐานข้อมูล
try:
# Add a new user
new_user = User(name='Alice Smith', email='alice.smith@example.com')
session.add(new_user)
# Commit the transaction
session.commit()
print("Transaction committed successfully!")
except Exception as e:
# Handle exceptions
print(f"An error occurred: {e}")
session.rollback()
print("Transaction rolled back.")
finally:
session.close()
ในตัวอย่างนี้:
- เราเพิ่มออบเจ็กต์ `User` ใหม่ลงในเซสชัน
- เราเรียก `session.commit()` เพื่อบันทึกการเปลี่ยนแปลงลงในฐานข้อมูล
- เราครอบโค้ดในบล็อก `try...except...finally` เพื่อจัดการกับข้อยกเว้นที่อาจเกิดขึ้น
- หากเกิดข้อยกเว้น เราเรียก `session.rollback()` เพื่อยกเลิกการเปลี่ยนแปลงใดๆ ที่เกิดขึ้นระหว่างธุรกรรม
- เราเรียก `session.close()` เสมอในบล็อก `finally` เพื่อปล่อยเซสชันและส่งคืนการเชื่อมต่อไปยังพูลการเชื่อมต่อ ซึ่งเป็นสิ่งสำคัญอย่างยิ่งในการหลีกเลี่ยงการรั่วไหลของทรัพยากร การไม่ปิดเซสชันอาจนำไปสู่การหมดการเชื่อมต่อและความไม่เสถียรของแอปพลิเคชัน
การ Rollback ธุรกรรม
หากเกิดข้อผิดพลาดใดๆ ในระหว่างธุรกรรม หรือหากคุณตัดสินใจว่าไม่ควรบันทึกการเปลี่ยนแปลง คุณจะ rollback ธุรกรรม ซึ่งจะคืนค่าฐานข้อมูลไปยังสถานะก่อนที่ธุรกรรมจะเริ่มต้น
try:
# Add a user with an invalid email (example to force a rollback)
invalid_user = User(name='Bob Johnson', email='invalid-email')
session.add(invalid_user)
# The commit will fail if the email is not validated on the database level
session.commit()
print("Transaction committed.")
except Exception as e:
print(f"An error occurred: {e}")
session.rollback()
print("Transaction rolled back successfully.")
finally:
session.close()
ในตัวอย่างนี้ หากการเพิ่ม `invalid_user` ทำให้เกิดข้อยกเว้น (เช่น เนื่องจากการละเมิดข้อจำกัดของฐานข้อมูล) การเรียก `session.rollback()` จะยกเลิกความพยายามในการแทรก ทำให้ฐานข้อมูลไม่เปลี่ยนแปลง
การจัดการธุรกรรมขั้นสูง
การใช้คำสั่ง `with` สำหรับการขอบเขตของธุรกรรม
วิธีที่ Pythonic และแข็งแกร่งกว่าในการจัดการธุรกรรมคือการใช้คำสั่ง `with` เพื่อให้มั่นใจว่าเซสชันถูกปิดอย่างถูกต้อง แม้ว่าจะมีข้อยกเว้นเกิดขึ้น
from contextlib import contextmanager
@contextmanager
def session_scope():
"""Provide a transactional scope around a series of operations."""
session = Session()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
# Usage:
with session_scope() as session:
new_user = User(name='Charlie Brown', email='charlie.brown@example.com')
session.add(new_user)
# Operations within the 'with' block
# If no exceptions occur, the transaction is committed automatically.
# If an exception occurs, the transaction is rolled back automatically.
print("User added.")
print("Transaction completed (committed or rolled back).")
ฟังก์ชัน `session_scope` คือ context manager เมื่อคุณเข้าสู่บล็อก `with` เซสชันใหม่จะถูกสร้างขึ้น เมื่อคุณออกจากบล็อก `with` เซสชันจะถูก commit (หากไม่มีข้อยกเว้นเกิดขึ้น) หรือ rollback (หากเกิดข้อยกเว้น) เซสชันจะถูกปิดเสมอในบล็อก `finally`
ธุรกรรมที่ซ้อนกัน (Savepoints)
SQLAlchemy สนับสนุนธุรกรรมที่ซ้อนกันโดยใช้ savepoints Savepoint ช่วยให้คุณสามารถ rollback ไปยังจุดที่เฉพาะเจาะจงภายในธุรกรรมที่ใหญ่กว่า โดยไม่กระทบต่อธุรกรรมทั้งหมด
try:
with session_scope() as session:
user1 = User(name='David Lee', email='david.lee@example.com')
session.add(user1)
session.flush() # Send changes to the database but don't commit yet
# Create a savepoint
savepoint = session.begin_nested()
try:
user2 = User(name='Eve Wilson', email='eve.wilson@example.com')
session.add(user2)
session.flush()
# Simulate an error
raise ValueError("Simulated error during nested transaction")
except Exception as e:
print(f"Nested transaction error: {e}")
savepoint.rollback()
print("Nested transaction rolled back to savepoint.")
# Continue with the outer transaction, user1 will still be added
user3 = User(name='Frank Miller', email='frank.miller@example.com')
session.add(user3)
except Exception as e:
print(f"Outer transaction error: {e}")
#Commit will commit user1 and user3, but not user2 due to the nested rollback
try:
with session_scope() as session:
#Verify only user1 and user3 exist
users = session.query(User).all()
for user in users:
print(user)
except Exception as e:
print(f"Unexpected Exception: {e}") #Should not happen
ในตัวอย่างนี้:
- เราเริ่มต้นธุรกรรมภายนอกโดยใช้ `session_scope()`
- เราเพิ่ม `user1` ลงในเซสชันและ flush การเปลี่ยนแปลงไปยังฐานข้อมูล `flush()` จะส่งการเปลี่ยนแปลงไปยังเซิร์ฟเวอร์ฐานข้อมูล แต่ *ไม่ได้* commit การเปลี่ยนแปลงเหล่านั้น ช่วยให้คุณดูว่าการเปลี่ยนแปลงนั้นถูกต้องหรือไม่ (เช่น ไม่มีการละเมิดข้อจำกัด) ก่อนที่จะ commit ธุรกรรมทั้งหมด
- เราสร้าง savepoint โดยใช้ `session.begin_nested()`
- ภายในธุรกรรมที่ซ้อนกัน เราเพิ่ม `user2` และจำลองข้อผิดพลาด
- เรา rollback ธุรกรรมที่ซ้อนกันไปยัง savepoint โดยใช้ `savepoint.rollback()` ซึ่งจะยกเลิกเฉพาะการเปลี่ยนแปลงที่เกิดขึ้นภายในธุรกรรมที่ซ้อนกันเท่านั้น (เช่น การเพิ่ม `user2`)
- เราดำเนินการต่อกับธุรกรรมภายนอกและเพิ่ม `user3`
- ธุรกรรมภายนอกถูก commit บันทึก `user1` และ `user3` ลงในฐานข้อมูล ในขณะที่ `user2` ถูกทิ้งเนื่องจากการ rollback savepoint
การควบคุมระดับ Isolation
ระดับ Isolation กำหนดระดับที่ธุรกรรมพร้อมกันถูกแยกออกจากกัน ระดับ Isolation ที่สูงขึ้นให้ความสอดคล้องของข้อมูลที่มากขึ้น แต่สามารถลด concurrency และประสิทธิภาพ SQLAlchemy ช่วยให้คุณสามารถควบคุมระดับ Isolation ของธุรกรรมของคุณได้
ระดับ Isolation ทั่วไป ได้แก่:
- Read Uncommitted: ระดับ Isolation ที่ต่ำที่สุด ธุรกรรมสามารถเห็นการเปลี่ยนแปลงที่ยังไม่ได้ commit ที่เกิดขึ้นจากธุรกรรมอื่น ซึ่งอาจนำไปสู่ dirty reads
- Read Committed: ธุรกรรมสามารถเห็นเฉพาะการเปลี่ยนแปลงที่ commit ที่เกิดขึ้นจากธุรกรรมอื่นเท่านั้น ซึ่งจะป้องกัน dirty reads แต่สามารถนำไปสู่ non-repeatable reads และ phantom reads
- Repeatable Read: ธุรกรรมสามารถเห็นข้อมูลเดียวกันตลอดทั้งธุรกรรม แม้ว่าธุรกรรมอื่นจะแก้ไขข้อมูลนั้นก็ตาม ซึ่งจะป้องกัน dirty reads และ non-repeatable reads แต่สามารถนำไปสู่ phantom reads
- Serializable: ระดับ Isolation ที่สูงที่สุด ธุรกรรมจะถูกแยกออกจากกันอย่างสมบูรณ์ ซึ่งจะป้องกัน dirty reads, non-repeatable reads และ phantom reads แต่สามารถลด concurrency ได้อย่างมาก
ระดับ Isolation เริ่มต้นขึ้นอยู่กับระบบฐานข้อมูล คุณสามารถตั้งค่าระดับ Isolation เมื่อสร้างเอ็นจินหรือเมื่อเริ่มต้นธุรกรรม
ตัวอย่าง (PostgreSQL):
from sqlalchemy.dialects.postgresql import dialect
# Set isolation level when creating the engine
engine = create_engine('postgresql://user:password@host:port/database',
connect_args={'options': '-c statement_timeout=1000'} #Example of timeout
)
# Set the isolation level when beginning a transaction (database specific)
# For postgresql, it's recommended to set it on the connection, not engine.
from sqlalchemy import event
from sqlalchemy.pool import Pool
@event.listens_for(Pool, "connect")
def set_isolation_level(dbapi_connection, connection_record):
existing_autocommit = dbapi_connection.autocommit
dbapi_connection.autocommit = True
cursor = dbapi_connection.cursor()
cursor.execute("SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE")
dbapi_connection.autocommit = existing_autocommit
cursor.close()
# Then transactions created via SQLAlchemy will use the configured isolation level.
สำคัญ: วิธีการตั้งค่าระดับ Isolation นั้นขึ้นอยู่กับฐานข้อมูล โปรดดูเอกสารประกอบฐานข้อมูลของคุณสำหรับไวยากรณ์ที่ถูกต้อง การตั้งค่าระดับ Isolation อย่างไม่ถูกต้องอาจนำไปสู่พฤติกรรมที่ไม่คาดคิดหรือข้อผิดพลาด
การจัดการ Concurrency
เมื่อผู้ใช้หรือกระบวนการหลายคนเข้าถึงข้อมูลเดียวกันพร้อมกัน สิ่งสำคัญคือต้องจัดการ concurrency อย่างถูกต้องเพื่อป้องกันการเสียหายของข้อมูลและรับประกันความสอดคล้องของข้อมูล SQLAlchemy มีกลไกหลายอย่างสำหรับการจัดการ concurrency รวมถึง optimistic locking และ pessimistic locking
Optimistic Locking
Optimistic locking สันนิษฐานว่าความขัดแย้งนั้นหายาก โดยจะตรวจสอบการแก้ไขที่เกิดขึ้นจากธุรกรรมอื่นก่อนที่จะ commit ธุรกรรม หากตรวจพบความขัดแย้ง ธุรกรรมจะถูกยกเลิก
ในการใช้งาน optimistic locking โดยทั่วไปคุณจะเพิ่มคอลัมน์เวอร์ชันลงในตารางของคุณ คอลัมน์นี้จะเพิ่มขึ้นโดยอัตโนมัติเมื่อใดก็ตามที่แถวถูกอัปเดต
from sqlalchemy import Column, Integer, String, Integer
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
content = Column(String)
version = Column(Integer, nullable=False, default=1)
def __repr__(self):
return f""
#Inside of the try catch block
def update_article(session, article_id, new_content):
article = session.query(Article).filter_by(id=article_id).first()
if article is None:
raise ValueError("Article not found")
original_version = article.version
# Update the content and increment the version
article.content = new_content
article.version += 1
# Attempt to update, checking the version column in the WHERE clause
rows_affected = session.query(Article).filter(
Article.id == article_id,
Article.version == original_version
).update({
Article.content: new_content,
Article.version: article.version
}, synchronize_session=False)
if rows_affected == 0:
session.rollback()
raise ValueError("Conflict: Article has been updated by another transaction.")
session.commit()
ในตัวอย่างนี้:
- เราเพิ่มคอลัมน์ `version` ลงในโมเดล `Article`
- ก่อนที่จะอัปเดตบทความ เราจะจัดเก็บหมายเลขเวอร์ชันปัจจุบัน
- ในคำสั่ง `UPDATE` เราจะรวมคําสั่ง `WHERE` ที่ตรวจสอบว่าคอลัมน์เวอร์ชันยังคงเท่ากับหมายเลขเวอร์ชันที่จัดเก็บหรือไม่ `synchronize_session=False` ป้องกันไม่ให้ SQLAlchemy โหลดออบเจ็กต์ที่อัปเดตอีกครั้ง เรากำลังจัดการเวอร์ชันอย่างชัดเจน
- หากคอลัมน์เวอร์ชันมีการเปลี่ยนแปลงโดยธุรกรรมอื่น คำสั่ง `UPDATE` จะไม่มีผลกับแถวใดๆ (rows_affected จะเป็น 0) และเราจะยกเว้น
- เรา rollback ธุรกรรมและแจ้งให้ผู้ใช้ทราบว่าเกิดความขัดแย้งขึ้น
Pessimistic Locking
Pessimistic locking สันนิษฐานว่าความขัดแย้งมีแนวโน้มที่จะเกิดขึ้น โดยจะรับ lock บนแถวหรือตารางก่อนที่จะแก้ไข ป้องกันไม่ให้ธุรกรรมอื่นแก้ไขข้อมูลจนกว่า lock จะถูกปล่อย
SQLAlchemy มีฟังก์ชันหลายอย่างสำหรับการรับ lock เช่น `with_for_update()`
# Example using PostgreSQL
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
# Database setup (replace with your actual database URL)
db_url = 'postgresql://user:password@host:port/database'
engine = create_engine(db_url, echo=False) #Set echo to true if you would like to see the SQL generated
Base = declarative_base()
class Item(Base):
__tablename__ = 'items'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(Integer)
def __repr__(self):
return f"- "
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
#Function to update the item (within a try/except)
def update_item_value(session, item_id, new_value):
# Acquire a pessimistic lock on the item
item = session.query(Item).filter(Item.id == item_id).with_for_update().first()
if item is None:
raise ValueError("Item not found")
# Update the item's value
item.value = new_value
session.commit()
return True
ในตัวอย่างนี้:
- เราใช้ `with_for_update()` เพื่อรับ lock บนแถว `Item` ก่อนที่จะอัปเดต ป้องกันไม่ให้ธุรกรรมอื่นแก้ไขแถวจนกว่าธุรกรรมปัจจุบันจะถูก commit หรือ rollback ฟังก์ชัน `with_for_update()` นั้นขึ้นอยู่กับฐานข้อมูล โปรดดูเอกสารประกอบฐานข้อมูลของคุณสำหรับรายละเอียด ฐานข้อมูลบางแห่งอาจมีกลไกหรือไวยากรณ์การ lock ที่แตกต่างกัน
สำคัญ: Pessimistic locking สามารถลด concurrency และประสิทธิภาพ ดังนั้นให้ใช้เฉพาะเมื่อจำเป็น
แนวทางปฏิบัติที่ดีที่สุดในการจัดการข้อยกเว้น
การจัดการข้อยกเว้นที่เหมาะสมเป็นสิ่งสำคัญอย่างยิ่งในการรับประกันความสมบูรณ์ของข้อมูลและป้องกันการหยุดทำงานของแอปพลิเคชัน ครอบการดำเนินการฐานข้อมูลของคุณในบล็อก `try...except` เสมอและจัดการข้อยกเว้นอย่างเหมาะสม
ต่อไปนี้เป็นแนวทางปฏิบัติที่ดีที่สุดในการจัดการข้อยกเว้น:
- Catch ข้อยกเว้นที่เฉพาะเจาะจง: หลีกเลี่ยงการ catch ข้อยกเว้นทั่วไปเช่น `Exception` Catch ข้อยกเว้นที่เฉพาะเจาะจงเช่น `sqlalchemy.exc.IntegrityError` หรือ `sqlalchemy.exc.OperationalError` เพื่อจัดการกับข้อผิดพลาดประเภทต่างๆ อย่างแตกต่างกัน
- Rollback ธุรกรรม: Rollback ธุรกรรมเสมอหากเกิดข้อยกเว้น
- Log ข้อยกเว้น: Log ข้อยกเว้นเพื่อช่วยวินิจฉัยและแก้ไขปัญหา รวมบริบทให้มากที่สุดใน log ของคุณ (เช่น ID ผู้ใช้ ข้อมูลอินพุต การประทับเวลา)
- Re-raise ข้อยกเว้นเมื่อเหมาะสม: หากคุณไม่สามารถจัดการข้อยกเว้นได้ ให้ re-raise เพื่ออนุญาตให้ตัวจัดการระดับสูงกว่าจัดการ
- Clean up ทรัพยากร: ปิดเซสชันเสมอและปล่อยทรัพยากรอื่นๆ ในบล็อก `finally`
import logging
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.exc import IntegrityError, OperationalError
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Database setup (replace with your actual database URL)
db_url = 'postgresql://user:password@host:port/database'
engine = create_engine(db_url, echo=False)
Base = declarative_base()
class Product(Base):
__tablename__ = 'products'
id = Column(Integer, primary_key=True)
name = Column(String)
price = Column(Integer)
def __repr__(self):
return f""
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
# Function to add a product
def add_product(session, name, price):
try:
new_product = Product(name=name, price=price)
session.add(new_product)
session.commit()
logging.info(f"Product '{name}' added successfully.")
return True
except IntegrityError as e:
session.rollback()
logging.error(f"IntegrityError: {e}")
#Handle database constraint violations (e.g., duplicate name)
return False
except OperationalError as e:
session.rollback()
logging.error(f"OperationalError: {e}")
#Handle connection errors or other operational issues
return False
except Exception as e:
session.rollback()
logging.exception(f"An unexpected error occurred: {e}")
# Handle any other unexpected errors
return False
finally:
session.close()
ในตัวอย่างนี้:
- เรากำหนดค่า logging เพื่อบันทึกเหตุการณ์ในระหว่างกระบวนการ
- เรา catch ข้อยกเว้นที่เฉพาะเจาะจงเช่น `IntegrityError` (สำหรับการละเมิดข้อจำกัด) และ `OperationalError` (สำหรับข้อผิดพลาดในการเชื่อมต่อ)
- เรา rollback ธุรกรรมในบล็อก `except`
- เรา log ข้อยกเว้นโดยใช้โมดูล `logging` วิธีการ `logging.exception()` จะรวม stack trace ในข้อความ log โดยอัตโนมัติ
- เรา re-raise ข้อยกเว้นหากเราไม่สามารถจัดการได้
- เราปิดเซสชันในบล็อก `finally`
การ Pooling การเชื่อมต่อฐานข้อมูล
SQLAlchemy ใช้การ pooling การเชื่อมต่อเพื่อจัดการการเชื่อมต่อฐานข้อมูลอย่างมีประสิทธิภาพ พูลการเชื่อมต่อจะดูแลชุดของการเชื่อมต่อที่เปิดอยู่กับฐานข้อมูล ทำให้แอปพลิเคชันสามารถนำการเชื่อมต่อที่มีอยู่อีกครั้งแทนที่จะสร้างการเชื่อมต่อใหม่สำหรับแต่ละคำขอ ซึ่งสามารถปรับปรุงประสิทธิภาพได้อย่างมาก โดยเฉพาะอย่างยิ่งในแอปพลิเคชันที่จัดการคำขอพร้อมกันจำนวนมาก
ฟังก์ชัน `create_engine()` ของ SQLAlchemy จะสร้างพูลการเชื่อมต่อโดยอัตโนมัติ คุณสามารถกำหนดค่าพูลการเชื่อมต่อได้โดยการส่งอาร์กิวเมนต์ไปยัง `create_engine()`
พารามิเตอร์พูลการเชื่อมต่อทั่วไป ได้แก่:
- pool_size: จำนวนการเชื่อมต่อสูงสุดในพูล
- max_overflow: จำนวนการเชื่อมต่อที่สามารถสร้างได้เกิน pool_size
- pool_recycle: จำนวนวินาทีหลังจากนั้นการเชื่อมต่อจะถูก recycle
- pool_timeout: จำนวนวินาทีที่จะรอให้การเชื่อมต่อพร้อมใช้งาน
engine = create_engine('postgresql://user:password@host:port/database',
pool_size=5, #Maximum pool size
max_overflow=10, #Maximum overflow
pool_recycle=3600, #Recycle connections after 1 hour
pool_timeout=30
)
สำคัญ: เลือกการตั้งค่าพูลการเชื่อมต่อที่เหมาะสมตามความต้องการของแอปพลิเคชันของคุณและความสามารถของเซิร์ฟเวอร์ฐานข้อมูลของคุณ พูลการเชื่อมต่อที่กำหนดค่าไม่ดีอาจนำไปสู่ปัญหาด้านประสิทธิภาพหรือการหมดการเชื่อมต่อ
ธุรกรรม Asynchronous (Async SQLAlchemy)
สำหรับแอปพลิเคชันสมัยใหม่ที่ต้องการ concurrency สูง โดยเฉพาะอย่างยิ่งแอปพลิเคชันที่สร้างขึ้นด้วยเฟรมเวิร์ก asynchronous เช่น FastAPI หรือ AsyncIO SQLAlchemy มีเวอร์ชัน asynchronous ที่เรียกว่า Async SQLAlchemy
Async SQLAlchemy มีเวอร์ชัน asynchronous ของคอมโพเนนต์ SQLAlchemy หลัก ทำให้คุณสามารถดำเนินการฐานข้อมูลได้โดยไม่ต้องบล็อก event loop ซึ่งสามารถปรับปรุงประสิทธิภาพและความสามารถในการปรับขนาดของแอปพลิเคชันของคุณได้อย่างมาก
ต่อไปนี้เป็นตัวอย่างพื้นฐานของการใช้ Async SQLAlchemy:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, Integer, String
import asyncio
# Database setup (replace with your actual database URL)
db_url = 'postgresql+asyncpg://user:password@host:port/database'
engine = create_async_engine(db_url, echo=False)
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
def __repr__(self):
return f""
async def create_db_and_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def add_user(name, email):
async with AsyncSession(engine) as session:
new_user = User(name=name, email=email)
session.add(new_user)
await session.commit()
async def main():
await create_db_and_tables()
await add_user("Async User", "async.user@example.com")
if __name__ == "__main__":
asyncio.run(main())
ความแตกต่างที่สำคัญจาก synchronous SQLAlchemy:
- `create_async_engine` ใช้แทน `create_engine`
- `AsyncSession` ใช้แทน `Session`
- การดำเนินการฐานข้อมูลทั้งหมดเป็น asynchronous และต้องรอโดยใช้ `await`
- ต้องใช้ไดรเวอร์ฐานข้อมูล Asynchronous (เช่น `asyncpg` สำหรับ PostgreSQL)
สำคัญ: Async SQLAlchemy ต้องใช้ไดรเวอร์ฐานข้อมูลที่สนับสนุนการดำเนินการ asynchronous ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้งและกำหนดค่าไดรเวอร์ที่ถูกต้องแล้ว
สรุป
การควบคุมการจัดการเซสชันและธุรกรรม SQLAlchemy เป็นสิ่งจำเป็นสำหรับการสร้างแอปพลิเคชัน Python ที่แข็งแกร่งและเชื่อถือได้ ซึ่งโต้ตอบกับฐานข้อมูล โดยการทำความเข้าใจแนวคิดของเซสชัน ธุรกรรม ระดับ Isolation และ concurrency และโดยการปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดสำหรับการจัดการข้อยกเว้นและการ pooling การเชื่อมต่อ คุณสามารถรับประกันความสมบูรณ์ของข้อมูลและเพิ่มประสิทธิภาพของแอปพลิเคชันของคุณ
ไม่ว่าคุณจะสร้างเว็บแอปพลิเคชันขนาดเล็กหรือระบบองค์กรขนาดใหญ่ SQLAlchemy มีเครื่องมือที่คุณต้องการเพื่อจัดการการโต้ตอบฐานข้อมูลของคุณอย่างมีประสิทธิภาพ อย่าลืมให้ความสำคัญกับความสมบูรณ์ของข้อมูลเสมอและจัดการกับข้อผิดพลาดที่อาจเกิดขึ้นอย่างสง่างามเพื่อให้มั่นใจในความน่าเชื่อถือของแอปพลิเคชันของคุณ
ลองพิจารณาสำรวจหัวข้อขั้นสูงเช่น:
- Two-Phase Commit (2PC): สำหรับธุรกรรมที่ครอบคลุมหลายฐานข้อมูล
- Sharding: สำหรับการกระจายข้อมูลไปยังเซิร์ฟเวอร์ฐานข้อมูลหลายเครื่อง
- Database migrations: การใช้เครื่องมือเช่น Alembic เพื่อจัดการการเปลี่ยนแปลงสคีมาฐานข้อมูล