در نوشته قبلی درباره SQLAlchemy نوشتم و همچنین درباره scoped_session که مشکل برنامه های مولتی ترد رو حل میکرد ولی چحوری باید این دو تا رو با Flask جور کنیم.
خود Flask به صورت پیش فرض قابلیت ارتباط با SQLAlchemy رو داره ولی به نظرم و اونجوری که کار کردم یه مقدار دست و پای آدم رو میبنده به همین خاطر خودم ترجیح میدم تو پروژه ها اونا رو از هم جدا کنم.
این کد رو ببینید
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
from flask import Flask from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker app = Flask(__name__) db_engine = create_engine( "mysql+pymysql://{}:{}@{}/{}?charset=utf8mb4".format( DB_USER, DB_PASSWORD, DB_SERVER, DB_NAME ), pool_size=5, max_overflow=10, echo=False, ) db_session_handler = sessionmaker(bind=db_engine) @app.route("/test1") def test1(): dbs = db_session_handler() item = dbs.query(Item).get(1) item.name = "new" ...heavy operations dbs.commit() return "ok" @app.route("/test2") def test2(): dbs = db_session_handler() item = dbs.query(Item).get(1) if item: dbs.delete(item) return "ok" @app.teardown_appcontext def close_session(*args, **kwargs): global dbs dbs.close() if __name__ == "__main__": app.run(host="0.0.0.0", debug=True, threaded=True) |
هر زمان که یک Session ایجاد کنید یک اتصال جداگانه به سرور دیتابیس ایجاد و به اون Session اختصاص داده میشه
حالا مشکلش چیه؟
فرض کنید دو درخواست به دو آدرس test1 و test2 در یک لحظه مشخص به صورت همزمان ارسال بشه در این صورت تابع db_session_handler که global هست باید یک session ایجاد کنه اما همونطور که اینجا اشاره شده تابع sessionmaker به صورت thread-safe نیست و در واقع non-current هست پس در اینجا به هر دو درخواست یک session میده حالا test1 و test2 دارن رو دیتابیس عملیات انجام میدن اما کار teest2 زودتر تموم میشه بعد Session بسته میشه اما کار test1 تموم نشده و چند لحظه بعد که تموم میشه و میخواد commit کنه خطا میده که session بسته شده
راه حلش چیه؟
SQLAlchemy تکنیکی به نام Scoped_Session و معرفی کرده که مخصوص برنامه های مولتی ترد و یا برنامه هایی که تعداد درخواست های زیادی در یک لحظه بهشون ارسال میشه هست (مثل یک سایت)
کد زیر رو ببینید
1 2 3 4 |
from sqlalchemy.orm import sessionmaker, scoped_session db_session_handler = sessionmaker(bind=db_engine) DB_Session = scoped_session(db_session_handler) |
به این صورت که شما هر بار که تابع DB_Session رو صدا بزنید یک Session به شما میده و SQLAlchemy در سیستم خودش این رو مدیریت میکنه که یک Session جدید بده یا اینکه Session فعال رو بده یعنی متوجه میشه که درخواست جدید هست یا همون قدیمیه.
به یکی از دو صورت زیر استفاده میشه که به نظر من اولیه خوانا تر هست و خودم از همین استفاده میکنم
1 2 3 4 |
dbs = DB_Session() dbs.query(...) # or DB_Session.query(...) |
برای اینکه Scoped Session رو ببندید کافیه تابع DB_Session.remove رو اجرا کنید با اینکار متغیر فعال Session حذف میشه و بعد تابع close اون session اجرا میشه یعنی اگه Connection Poolling فعال باشه این اتصال به Pool برمیگرده (که در اینصورت باید db_engine.dispose رو هم اجرا کنید تا اتصال های آماده به کار Pool غیر فعال و منابع سخت افزاریشون ازاد بشن) در غیر این صورت اتصال بسته میشه و منابع سخت افزاری آزاد میشه
کد اول برنامه به این صورت در میاد
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
from flask import Flask from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session app = Flask(__name__) db_engine = create_engine( "mysql+pymysql://{}:{}@{}/{}?charset=utf8mb4".format( DB_USER, DB_PASSWORD, DB_SERVER, DB_NAME ), pool_size=5, max_overflow=10, echo=False, ) db_session_handler = sessionmaker(bind=db_engine) DB_Session = scoped_session(db_session_handler) @app.teardown_appcontext def remove_scoped_session(*args, **kwargs): global DB_Session, db_engine DB_Session.remove() db_engine.dispose() |
حالا کافیه تو کدتون DB_Session رو صدا بزنید تا یک Session جدید بگیرید مثل
1 2 3 4 |
dbs = DB_Session() dbs.query(...) # or DB_Session.query(...) |
خوب حالا جمع بندی میکنم به همراه بیان نکات مهم
- در اینجا Connection Pooling رو فعال کردم چون برای برنامه های مولتی ترد و یا در کل برنامه هایی که در هر لحظه چند درخواست همزمان بهشون ارسال میشه برای اینکه کارایی برنامه بیشتر بشه و منابع سیستم هر بار برای ایجاد و بستن اتصال بیهوده مصرف نشه (تو لینکی که ابتدای پست اشاره کردم این مورد رو کامل توضیح دادم ) و همچنین برای اینکه با خطای Too Many Connections مواجه نشید
- از Scoped Session استفاده کردم که مخصوص برنامه های مولتی ترد و یا در کل برنامه هایی که در یک لحظه چند درخواست بهشون ارسال میشه.
- از teardown فلسک استفاده کردم تا بعد از تموم شدن درخواست اتصال رو به Pool برگردونه.