ORM backendlari

Bitta adapter shartnomasi ortidagi SQLAlchemy 2.0 va Tortoise — va ular orasidagi farq bo‘lgan to‘rtta qator.

Mavjud v0.2.0

fastfort/orm/ dan yuqorida hech narsa SQLAlchemy yoki Tortoise’ni import qila olmaydi — buzilsa, test sborkani muvaffaqiyatsiz qiladi. Introspeksiya modelni ModelSpecga aylantiradi — oddiy, o‘zgarmas, JSON’ga serializatsiya qilinadigan ma’lumot — va undan yuqoridagi har bir qatlam aynan shu bilan ishlaydi.

Natija aniq bo‘ldi: ikkinchi adapter admin qismida bitta ham qator o‘zgartirmasdan qo‘shildi.

SQLAlchemy

uv add "fastfort[sqlalchemy,postgres]"
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase

from fastfort.orm.sqlalchemy import SQLAlchemyBackend


class Base(DeclarativeBase):
    pass


engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/shop")

# Not a preference. The admin's views read attributes off an object *after* the
# unit of work has committed — the label in a success message, the primary key
# to redirect to. With the default, each of those is a synchronous refresh
# against an async session, which raises MissingGreenlet.
session_factory = async_sessionmaker(engine, expire_on_commit=False)

fort = FastFort(
    settings=settings,
    backend=SQLAlchemyBackend(session_factory=session_factory, base=Base),
)

FastFort hech qachon o‘zi ulanish ochmaydi. U siz qurgan fabrikadan foydalanadi.

Tortoise

uv add "fastfort[tortoise,postgres]"

To‘rtta qator farq qiladi. Qolgan hammasi — sozlamalar, @admin.register, list_display, amallar, eksport, import — bir xil, chunki qolgan hammasi ORM qatlamidan yuqorida joylashgan va modelni umuman ko‘rmaydi.

from tortoise import Tortoise

from fastfort.orm.tortoise import TortoiseBackend

await Tortoise.init(
    db_url="postgres://user:pass@localhost/shop",
    modules={"models": ["app.models"]},
    # Tortoise keeps its connections in a contextvar, and an ASGI server runs
    # the lifespan in a different task from the requests. Without this flag the
    # init above is invisible to every view: start-up looks healthy and the
    # first page that touches the database is a 500. `RegisterTortoise` from
    # `tortoise.contrib.fastapi` passes it for you.
    _enable_global_fallback=True,
)

fort = FastFort(settings=settings, backend=TortoiseBackend())
fort.set_user_model(User)
fort.mount(app)

«Ikkinchisi xuddi shunday ishlaydi» — bu test

tests/orm/test_conformance.py ikkala backendga ham bir xil model shakllari bo‘yicha bir xil savollarni beradi — bir xil ustunlar, bir xil bog‘lanishlar, bir xil o‘chirishlar — va javoblarni solishtiradi. Yuqoridagi da’vo shunchaki gap emas, aynan test bo‘lishining sababi shu.

fastfort/orm/coerce.py — ikkala adapter uchun umumiy bo‘lgan yagona narsa, chunki so‘rov satrini ustun turiga aylantirish ustunga tegishli masala, u yoki bu ORM’ga emas.

Ma’lumotlar bazalari

SQLitePostgreSQLMySQL
Asosiy CRUD, filtrlar, qidiruv✅✅✅
JSON✅✅ JSONB✅
Massivlar, hstore, diapazonlar, inet, citext—✅—
PostGIS geometriyasi—✅—
pgvector—✅—
uv add "fastfort[sqlalchemy,sqlite]"
uv add "fastfort[sqlalchemy,postgres]"
uv add "fastfort[sqlalchemy,mysql]"

Dvijoklar orasida farq qiladigan SQL bilan bog‘liq har narsa backendning dialekt modulida joylashgan — u aynan «uchalasida ham ishlaydi» degan da’voni umidga emas, testga asoslash uchun mavjud.

Adapter amal qiladigan ikkita qoida

Adapterlar hech qachon commit qilmaydi

Ular flush qiladi. UnitOfWork kontekst menejeri toza chiqishda commit qiladi va istisno yuz berganda orqaga qaytaradi (rollback), shu sababli yarim muvaffaqiyatsiz so‘rov ortida hech narsa qolmaydi — hatto qirqinchi qatorda muvaffaqiyatsiz bo‘lgan ommaviy amal ham.

Rollback har bir atributni yaroqsiz qiladi

await uow.rollback() dan so‘ng instance.id ni o‘qish — greenlet ko‘prigidan tashqarida asinxron sessiya uchun sinxron yangilash bo‘lib, bu xatoga olib keladi.

Shuning uchun: hali kerak bo‘lgan har bir qiymatni — yorliqni, birlamchi kalitni — rollback’dan oldin o‘qing, yoki avval javobni render qiling, keyin rollback qiling. Bu adapterlar ustida turgan kodni buzishning eng keng tarqalgan usuli.

So‘rov oqimi

introspect(model) → ModelSpec          once, cached on the backend
ModelAdmin(spec)                       once, validated when built
request → ListQuery.from_params(...)   the validation boundary
        → backend.unit_of_work()       one transaction per request
        → backend.adapter(model, uow)  request-scoped, never cached
        → adapter.list/get/create/...  flushes, never commits
        → Renderer.render(template)    server-side HTML

Adapter har bir so‘rov doirasida yaratiladi va hech qachon keshlanmaydi; spec esa keshlanadi — shuning uchun register_type biror narsa introspeksiya qilinishidan oldin ishga tushishi kerak.

Backend qo‘shish

Protokollar fastfort/orm/base.pyda: Backend, ModelAdapter, UnitOfWork. Shu uchtasini amalga oshiring — va moslik testlari to‘plami hammasi to‘g‘ri qilinganini aytadi — fastfort/orm/ dan yuqorida esa hech narsani o‘zgartirish shart emas, chunki bu chegara konventsiya emas, test orqali ta’minlanadi.