10 β Ma'lumotlar bazasi bilan ishlash¶
β¬ οΈ Oldingi: 09 β Middleware Β· π README Β· Keyingi: 11 β Loyiha tuzilishi va konfiguratsiya β‘οΈ
Bu bobda: botimizga "xotira" beramiz. Hozirgacha bot xabarga javob berardi va darrov hammasini unutardi β qayta ishga tushsa, kim oldin yozgani, kim ro'yxatdan o'tgani yo'qoladi. Endi ma'lumotni ma'lumotlar bazasida (DB) doimiy saqlashni o'rganamiz.
Bir muhim narsa: aiogram asinxron (
async/await), shuning uchun DB bilan ham asinxron ishlashimiz kerak β aks holda bitta sekin so'rov butun botni "muzlatib" qo'yadi. Shuning uchun oddiysqlite3o'rnigaaiosqlite(async SQLite drayveri) yoki SQLAlchemy 2.0 (async ORM) ishlatamiz.Yoritamiz: nega async DB kerakligi;
aiosqlitebilan jadval yaratish,INSERT/SELECT/UPDATE(CRUD); foydalanuvchini ro'yxatga olish; repository naqsh (SQL'ni bitta joyga jamlash); DB sessiyasini middleware orqali handler'ga uzatish (dependency injection); SQLAlchemy 2.0 (async) bilan ORM yondashuvi; oddiy migratsiya (jadval sxemasi o'zgarganda). SQL'ning o'zini chuqurroq o'rganmoqchi bo'lsangiz β SQL qo'llanmasi tayyor turibdi.Halol eslatma (verifikatsiya): bu bobdagi DB kodi, repository, middleware orqali DI va handler routing'i mening kompyuterimda offline (BotFather token'isiz) β
aiosqlite, SQLAlchemy 2.0 async va dispatcher'ningfeed_updatemexanizmi orqali haqiqatan ishga tushirilib tekshirildi. Jonli Telegram'ga xabar yuborish, long-polling yoki real foydalanuvchi/startbosishi esa BotFather token + internet talab qiladi β bunday joylar matnda "illustrativ β jonli bot talab qiladi" deb halol belgilangan.
10.1. Nega botga ma'lumotlar bazasi kerak?¶
Tasavvur qiling, botingiz 1000 ta foydalanuvchiga ega. Siz bilmoqchisiz:
- nechta odam botdan foydalanadi?
- kim qachon ro'yxatdan o'tgan?
- har bir foydalanuvchining tili qaysi (uz/en/ru)?
- foydalanuvchi nechta xabar yuborgan?
Bularni Python o'zgaruvchisida (masalan users = {} lug'atda) saqlasangiz β bot qayta ishga tushishi bilan hamma narsa o'chadi. Server o'chsa, kod yangilansa, xato bo'lsa β xotira bo'sh. Bu jiddiy bot uchun yaramaydi.
Ma'lumotlar bazasi β bu ma'lumotni diskda doimiy saqlaydigan tizim. Bot qayta ishga tushsa ham ma'lumot joyida turadi.
Boshlash uchun eng oddiy variant β SQLite. Bu alohida server talab qilmaydigan, bitta .db faylida yashaydigan baza. Botlar uchun a'lo: o'rnatish shart emas, nusxa olish oson, minglab foydalanuvchiga bemalol yetadi. Keyinroq loyiha katta bo'lsa, PostgreSQL'ga o'tasiz β kod deyarli o'zgarmaydi (ayniqsa SQLAlchemy ishlatsangiz).
10.2. Nega sqlite3 emas, aiosqlite?¶
Python'da sqlite3 moduli bor (standart kutubxonada). Lekin u bloklovchi (sinxron): so'rov bajarilguncha butun dastur kutadi.
aiogram esa asinxron β bitta jarayonda yuzlab foydalanuvchini "bir vaqtda" xizmat qiladi. Agar handler ichida sinxron sqlite3 so'rovini chaqirsangiz, o'sha so'rov tugaguncha boshqa hamma foydalanuvchi kutadi. Bu botni sekinlashtiradi.
Yechim β aiosqlite: bu sqlite3ning async qobig'i. await bilan ishlatiladi va boshqa handlerlarni bloklamaydi.
Eng kichik misol β baza ochib, jadval yaratamiz:
import asyncio
import aiosqlite
async def main():
# baza fayli avtomatik yaratiladi (yo'q bo'lsa)
async with aiosqlite.connect("bot.db") as db:
await db.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
username TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
"""
)
await db.commit() # o'zgarishni diskka yozish
print("Jadval tayyor")
asyncio.run(main())
Diqqat qiling:
async with aiosqlite.connect(...)β baza ulanishini ochadi va blok tugaganda avtomatik yopadi.- Har bir so'rov
await db.execute(...)bilan. await db.commit()β yozish so'rovlari (INSERT,UPDATE,DELETE,CREATE) shundan keyingina doimiy bo'ladi.commitqilmasangiz β o'zgarish yo'qoladi. Bu eng ko'p uchraydigan boshlovchi xato.id INTEGER PRIMARY KEYβ bu yerda Telegram'ninguser.id'sini ishlatamiz, chunki u har bir foydalanuvchi uchun yagona.
SQL sintaksisini (
CREATE TABLE, ustun turlari,PRIMARY KEY) batafsil bilmoqchi bo'lsangiz β SQL: CREATE va ma'lumot turlari boblariga qarang. Bu yerda biz faqat botga kerakli minimumni ko'ramiz.
10.3. CRUD: yaratish, o'qish, yangilash, o'chirish¶
CRUD β Create (qo'shish), Read (o'qish), Update (yangilash), Delete (o'chirish). Bu DB bilan ishlashning to'rt asosiy amali. Hammasini aiosqlite bilan ko'rib chiqamiz.
Create β foydalanuvchi qo'shish¶
async def add_user(db, user_id: int, full_name: str, username: str | None) -> bool:
cur = await db.execute(
"INSERT OR IGNORE INTO users (id, full_name, username) VALUES (?, ?, ?)",
(user_id, full_name, username),
)
await db.commit()
return cur.rowcount > 0 # True = yangi qo'shildi, False = oldin bor edi
Muhim nuqtalar:
?belgilari β bu parametr o'rni. Qiymatlarni kortejda ((user_id, full_name, username)) alohida beramiz. Hech qachon qiymatni to'g'ridan-to'g'ri SQL satrigaf"... {user_id}"ko'rinishida qo'shmang β bu SQL injection xavfini ochadi. Har doim?ishlating.INSERT OR IGNOREβ agar shuidallaqachon bo'lsa, xato bermaydi, shunchaki e'tiborsiz qoldiradi. Foydalanuvchi/startni yuz marta bossa ham, baza ichida bitta yozuv bo'ladi.cur.rowcountβ nechta qator o'zgarganini aytadi. Yangi qo'shilsa1, e'tiborsiz qoldirilsa0. Buni "yangi foydalanuvchimi?" deb tekshirishga ishlatamiz.
Read β foydalanuvchini o'qish¶
async def get_user(db, user_id: int) -> dict | None:
db.row_factory = aiosqlite.Row # qatorni lug'atga o'xshatib o'qish
async with db.execute("SELECT * FROM users WHERE id = ?", (user_id,)) as cur:
row = await cur.fetchone()
return dict(row) if row else None
db.row_factory = aiosqlite.Rowβ bu satrlarnirow["full_name"]kabi nom bo'yicha o'qishga imkon beradi (aks holda faqat indeks:row[1]).fetchone()β bitta qatorni qaytaradi (yokiNone).- Ko'p qator kerak bo'lsa β
fetchall().
Update va Delete¶
async def set_username(db, user_id: int, username: str) -> None:
await db.execute("UPDATE users SET username = ? WHERE id = ?", (username, user_id))
await db.commit()
async def delete_user(db, user_id: int) -> None:
await db.execute("DELETE FROM users WHERE id = ?", (user_id,))
await db.commit()
WHERE shartini unutmang: WHEREsiz UPDATE/DELETE butun jadvalga ta'sir qiladi. Bu xatoni hamma kamida bir marta qiladi β siz qilmang.
Offline tekshirildi. Yuqoridagi
add_user/get_user/count_usersfunksiyalari:memory:bazada ishga tushirilib sinaldi: yangi user qo'shilgandaTrue, takror bo'lsaFalse;get_user(999)esaNoneqaytardi. CRUD ishlaydi.
10.4. CRUD'ni botga ulaymiz: ro'yxatga olish¶
Endi haqiqiy bot β har kim /start bosganda uni bazaga yozamiz. Bu eng keng tarqalgan vazifa: "foydalanuvchini ro'yxatga olish".
import asyncio
import logging
import aiosqlite
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.types import Message
router = Router()
DB_PATH = "bot.db"
async def init_db():
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
username TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
"""
)
await db.commit()
@router.message(CommandStart())
async def start_handler(message: Message):
async with aiosqlite.connect(DB_PATH) as db:
cur = await db.execute(
"INSERT OR IGNORE INTO users (id, full_name, username) VALUES (?, ?, ?)",
(message.from_user.id, message.from_user.full_name, message.from_user.username),
)
await db.commit()
is_new = cur.rowcount > 0
async with db.execute("SELECT COUNT(*) FROM users") as c:
(total,) = await c.fetchone()
if is_new:
await message.answer(f"Xush kelibsiz! Siz {total}-foydalanuvchisiz.")
else:
await message.answer(f"Qaytib kelganingizdan xursandmiz. Jami: {total}.")
async def main():
logging.basicConfig(level=logging.INFO)
await init_db()
bot = Bot(token="BU_YERGA_TOKEN_EMAS") # token .env dan o'qiladi (10.8-bo'limga qarang)
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Bu jonli botda quyidagicha ko'rinadi (illustrativ β jonli Telegram va token talab qiladi):
Foydalanuvchi: /start
Bot: Xush kelibsiz! Siz 1-foydalanuvchisiz.
(o'sha odam yana /start bosadi)
Bot: Qaytib kelganingizdan xursandmiz. Jami: 1.
Kod ishlaydi, lekin bitta muammosi bor: har handler'da qaytadan aiosqlite.connect(...) ochilyapti. Bu:
- Takrorlanish β har handler'da bir xil ulanish kodi.
- Sekin β har xabar uchun yangi ulanish ochiladi/yopiladi.
- SQL handler'ga aralashgan β biznes-mantiq va DB kodi bir joyda.
Buni tozalashning ikki vositasi bor: repository naqsh (SQL'ni jamlash) va middleware (sessiyani bir marta ochib, handler'ga berish). Keyingi bo'limlarda shularni qilamiz.
10.5. Repository naqsh β SQL'ni bitta joyga jamlash¶
Yuqoridagi botda SQL handler ichida turibdi. Agar 10 ta handler bo'lsa va har birida SELECT/INSERT aralash bo'lsa β kodni o'qish va o'zgartirish qiyinlashadi. Agar ertaga ustun nomini o'zgartirsangiz, 10 joyni tuzatishingiz kerak.
Repository naqshning g'oyasi sodda: barcha DB so'rovlarini bitta klassga jamlaymiz. Handler endi SQL ko'rmaydi β u faqat repo.get(uid), repo.add_user(...) kabi mazmunli metodlarni chaqiradi.
import aiosqlite
class UserRepo:
"""Foydalanuvchilar bilan bog'liq barcha DB amallari shu yerda."""
def __init__(self, db: aiosqlite.Connection) -> None:
self.db = db
async def get(self, user_id: int) -> dict | None:
self.db.row_factory = aiosqlite.Row
async with self.db.execute("SELECT * FROM users WHERE id = ?", (user_id,)) as cur:
row = await cur.fetchone()
return dict(row) if row else None
async def add_if_absent(self, user_id: int, full_name: str, username: str | None) -> bool:
cur = await self.db.execute(
"INSERT OR IGNORE INTO users (id, full_name, username) VALUES (?, ?, ?)",
(user_id, full_name, username),
)
return cur.rowcount > 0
async def count(self) -> int:
async with self.db.execute("SELECT COUNT(*) FROM users") as cur:
(n,) = await cur.fetchone()
return n
Endi handler ancha toza ko'rinadi:
@router.message(CommandStart())
async def start_handler(message: Message, repo: UserRepo):
is_new = await repo.add_if_absent(
message.from_user.id, message.from_user.full_name, message.from_user.username
)
total = await repo.count()
text = (
f"Xush kelibsiz! Siz {total}-foydalanuvchisiz."
if is_new
else f"Qaytib kelganingizdan xursandmiz. Jami: {total}."
)
await message.answer(text)
Diqqat: handler repo: UserRepo parametrini so'rab oldi, lekin uni o'zi yaratmadi. Bu repo qayerdan keladi? β Mana shu yerga middleware kiradi.
10.6. DB sessiyasini middleware orqali handler'ga uzatish¶
9-bobda middleware bilan tanishgansiz: u handler chaqirilishidan oldin ishlaydi va data lug'atiga qiymat qo'sha oladi. aiogram esa handler parametrlarini data kalitlari bilan avtomatik moslashtiradi (dependency injection). Ya'ni middleware data["repo"] = ... qilsa, handler async def h(message, repo) deb yozsa β repo o'sha qiymatga to'ladi.
Bu bizga ideal: middleware har update uchun DB sessiyasini ochadi, UserRepo yasaydi, dataga qo'yadi, handler tugagach commit qiladi va sessiyani yopadi.
from typing import Any, Awaitable, Callable
import aiosqlite
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
class DbSessionMiddleware(BaseMiddleware):
def __init__(self, db_path: str) -> None:
self.db_path = db_path
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
async with aiosqlite.connect(self.db_path) as db:
data["repo"] = UserRepo(db) # handler'ga "repo" ni beramiz
result = await handler(event, data)
await db.commit() # handler tugagach saqlaymiz
return result
Middleware'ni dispatcher'ga ulaymiz β update darajasida, ya'ni har turdagi update'ga (message, callback va h.k.):
aiogram 3.x'da
__call__imzosi aniq shunday:(self, handler, event, data). Middleware'nirouter.message.middleware(...)(faqat xabar uchun) yokidp.update.middleware(...)(hamma uchun) bilan ulaysiz.datalug'atiga qo'ygan qiymatlaringiz handler parametrlariga mos kelsa avtomatik uzatiladi. Tekshirildi: docs.aiogram.dev β Middlewares va Dependency Injection bo'limlari.
Nega bu yaxshi? Endi:
- Handler DB'ni qanday ochish-yopishni bilmaydi β faqat
reponi ishlatadi. - Sessiya bir marta ochiladi, handler tugagach yopiladi β resurs tejaladi.
commitmarkazlashgan β har handler'daawait db.commit()yozish unutilmaydi.- Ertaga SQLite'dan PostgreSQL'ga o'tsangiz β faqat middleware va repo o'zgaradi, handlerlar emas.
Offline tekshirildi (eng muhim qism). SQLAlchemy variantida (10.7) bu butun zanjir β
DbSessionMiddlewareβ handler DI β repository β soxta token ("123456:AAH-FakeTest_abc") bilanBotyaratib,dp.feed_update(bot, Update(...))orqali uchta/startupdate'i uzatilib sinaldi. Natija: 1-update "Siz 1-foydalanuvchisiz", 2-update (o'sha user) "Qaytib kelganingizdan xursandmiz. Jami: 1", 3-update (boshqa user) "Siz 2-foydalanuvchisiz" β ya'ni routing, DI va ro'yxatga olish mantig'i to'g'ri ishladi. Bu test BotFather token'isiz bajarildi.
10.7. SQLAlchemy 2.0 (async) β ORM yondashuvi¶
aiosqlite β bu "yalang'och" SQL: siz SELECT/INSERT yozasiz. Loyiha o'sgani sayin SQL qatorlarini boshqarish zerikarli bo'ladi. ORM (Object-Relational Mapping) jadval qatorini Python obyektiga aylantiradi: User klassi = users jadvali, user.full_name = ustun.
aiogram bilan eng ko'p ishlatiladigan ORM β SQLAlchemy 2.0, uning async rejimida. O'rnatish:
(SQLAlchemy SQLite uchun ham aynan aiosqlite drayverini ishlatadi; greenlet async ishi uchun kerak.)
Model β jadvalni Python klassi sifatida¶
from datetime import datetime
from sqlalchemy import BigInteger, String, func, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True) # Telegram user id
full_name: Mapped[str] = mapped_column(String(128))
username: Mapped[str | None] = mapped_column(String(64), nullable=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
Bu 2.0 uslubi: Mapped[...] va mapped_column(...). Eski 1.x uslubidagi Column(...) to'g'ridan-to'g'ri atributga (id = Column(Integer, ...)) endi tavsiya etilmaydi β biz 2.0 idiomini ishlatamiz.
Telegram
user.idkatta bo'lishi mumkin, shuning uchunIntegeremasBigInteger. PostgreSQL'da bu muhim; SQLite'da farqi yo'q, ammo bir xil model ikkala bazada ham to'g'ri ishlaydi.
Engine va session yaratish¶
def make_engine(url: str = "sqlite+aiosqlite:///bot.db"):
return create_async_engine(url)
def make_sessionmaker(engine) -> async_sessionmaker[AsyncSession]:
return async_sessionmaker(engine, expire_on_commit=False)
async def create_tables(engine) -> None:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
create_async_engineβ async dvigatel. URLsqlite+aiosqlite:///bot.dbko'rinishida (+aiosqliteasync drayverni bildiradi).async_sessionmaker(..., expire_on_commit=False)β sessiya "fabrikasi".expire_on_commit=Falsemuhim:commit'dan keyin obyekt atributlarini (user.full_name) yana o'qiganda kutilmagan qo'shimcha so'rov bo'lmaydi.create_tablesβ modeldan jadvallarni yaratadi (bot ishga tushganda bir marta chaqiriladi).
Repository β endi ORM bilan¶
class UserRepo:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def get(self, user_id: int) -> User | None:
return await self.session.get(User, user_id)
async def add_if_absent(self, user_id: int, full_name: str, username: str | None) -> bool:
if await self.get(user_id) is not None:
return False
self.session.add(User(id=user_id, full_name=full_name, username=username))
await self.session.flush()
return True
async def count(self) -> int:
return await self.session.scalar(select(func.count()).select_from(User)) or 0
Endi SQL yo'q β session.get(User, id), session.add(User(...)), select(...). ORM SQL'ni o'zi generatsiya qiladi.
Middleware β SQLAlchemy sessiyasi bilan¶
class DbSessionMiddleware(BaseMiddleware):
def __init__(self, session_pool: async_sessionmaker) -> None:
self.session_pool = session_pool
async def __call__(self, handler, event, data):
async with self.session_pool() as session:
data["session"] = session
data["repo"] = UserRepo(session)
result = await handler(event, data)
await session.commit()
return result
Ulanishi:
engine = make_engine("sqlite+aiosqlite:///bot.db")
await create_tables(engine)
Session = make_sessionmaker(engine)
dp = Dispatcher()
dp.include_router(router)
dp.update.middleware(DbSessionMiddleware(Session))
Handler bir xil qoladi β async def start_handler(message, repo): .... Mana ORM va repository naqshning kuchi: pastki qatlam (SQLite SQL β ORM) o'zgardi, lekin handler'lar tegmadi.
Offline tekshirildi. Yuqoridagi
Usermodeli,UserRepo(ORM) vaDbSessionMiddleware(SQLAlchemy) fayl bazasida (sqlite+aiosqlite:///...) ishga tushirildi:add_if_absentyangi user uchunTrue, takror uchunFalseqaytardi; ikkinchi sessiyadacount() == 2,get(100).full_name == "Ali Valiyev",created_atto'lgan,get(999) is None. Hammasi to'g'ri.
Qaysi birini tanlash β aiosqlite yoki SQLAlchemy?¶
aiosqlite |
SQLAlchemy 2.0 | |
|---|---|---|
| O'rganish | Oson, sof SQL | Murakkabroq |
| Kichik bot | A'lo, ortiqcha narsa yo'q | Ortiqcha bo'lishi mumkin |
| Katta loyiha | SQL ko'payadi | Modellar tartibli |
| Baza almashtirish | Qo'lda | Deyarli avtomatik (URL o'zgaradi) |
| Migratsiya vositasi | Qo'lda (10.8) | Alembic (kuchli) |
Boshlovchi uchun maslahat: kichik bot β aiosqlite, jiddiy/o'sadigan loyiha β SQLAlchemy. Ikkalasida ham repository + middleware naqshi bir xil qoladi.
10.8. Oddiy migratsiya β sxema o'zgarganda¶
Bot ishlab turibdi, foydalanuvchilar bor. Endi siz users jadvaliga yangi ustun (masalan lang β til) qo'shmoqchisiz. Ammo CREATE TABLE IF NOT EXISTS allaqachon mavjud jadvalni o'zgartirmaydi β u faqat yo'q bo'lsa yaratadi. Eski jadvalda yangi ustun paydo bo'lmaydi.
Yechim β migratsiya: sxemani bosqichma-bosqich, versiyalab o'zgartirish. SQLite'da buni PRAGMA user_version (bazaga o'rnatilgan versiya hisoblagichi) bilan qo'lda qilish mumkin:
import aiosqlite
# Har bir migratsiya β bitta qadam. Tartibni HECH QACHON o'zgartirmang, faqat oxiriga qo'shing.
MIGRATIONS = [
"CREATE TABLE users (id INTEGER PRIMARY KEY, full_name TEXT NOT NULL);", # v1
"ALTER TABLE users ADD COLUMN username TEXT;", # v2
"ALTER TABLE users ADD COLUMN is_banned INTEGER DEFAULT 0;", # v3
]
async def apply_migrations(db: aiosqlite.Connection) -> int:
async with db.execute("PRAGMA user_version") as cur:
(current,) = await cur.fetchone()
applied = 0
for version, sql in enumerate(MIGRATIONS, start=1):
if version > current: # faqat hali qo'llanmaganlar
await db.executescript(sql)
await db.execute(f"PRAGMA user_version = {version}")
applied += 1
await db.commit()
return applied
Mantiq:
PRAGMA user_versionβ baza hozir qaysi versiyada ekanini saqlaydi (boshida0).- Har migratsiya tartib raqamiga ega (1, 2, 3...).
version > currentbo'lganini qo'llaymiz. - Qo'llangach versiyani oshiramiz. Ikkinchi marta chaqirilsa β hech narsa qo'llanmaydi (idempotent).
- Yangi ustun kerak bo'lsa β
MIGRATIONSro'yxatining oxiriga yangiALTER TABLE ...qo'shasiz. Eski qadamlarga tegmaysiz.
Bot ishga tushganda await apply_migrations(db) ni bir marta chaqirasiz β baza avtomatik so'nggi versiyaga "ko'tariladi".
Offline tekshirildi.
:memory:bazada: birinchi chaqiruvda 3 migratsiya qo'llandi (user_version=3), ikkinchi chaqiruvda 0 (idempotent).PRAGMA table_info(users)ustunlari aynan['id', 'full_name', 'username', 'is_banned']chiqdi.
SQLAlchemy ishlatsangiz, qo'lda emas β Alembic vositasidan foydalanasiz: u model o'zgarishini avtomatik aniqlab, migratsiya skriptini yozadi. Alembic alohida katta mavzu; boshlovchi uchun yuqoridagi PRAGMA user_version usuli soddaligi bilan a'lo.
10.9. Amaliy maslahatlar (qisqacha)¶
- Har doim
?parametr ishlating,f"..."bilan SQL yasamang. Bu SQL injection'dan himoya. commitni unutmang β yozish so'rovlaricommit'siz saqlanmaydi. Middleware'da markazlashtirsangiz, unutmaslik osonroq.WHEREni unutmangUPDATE/DELETE'da.- Indeks: tez-tez
WHERE username = ?bo'yicha qidirsangiz,usernameustuniga indeks qo'shing (CREATE INDEX ...). Bu katta jadvalda qidiruvni tezlashtiradi β SQL: Indekslar. - Bloklovchi kodni handler'da chaqirmang. Agar majburan sinxron kutubxona ishlatsangiz,
asyncio.to_thread(...)ichida bajaring. - Node.js'da bot yozsangiz, bu naqshlar deyarli bir xil (Prisma/Knex + middleware) β Node.js qo'llanmasi bilan solishtirib ko'rishingiz mumkin.
Mashqlar¶
Maslahat: mashqlarni
aiosqlitebilan:memory:bazada sinab ko'ring β fayl yaratmasdan tez tekshirasiz. Handler mashqlarini 10.6/10.7'dagifeed_updatenamunasi bilan token'siz tekshirish mumkin.
Oson¶
messagesjadvalini yarating:id(avtomatik),user_id,text,created_at.CREATE TABLE'ni yozing.aiosqlitebilanadd_message(db, user_id, text)funksiyasini yozing (INSERT,?parametrlar bilan,commit).get_messages(db, user_id)β foydalanuvchining barcha xabarlarini ro'yxat (list[dict]) ko'rinishida qaytaring (row_factory+fetchall).count_users(db)funksiyasini yozing β jadvaldagi foydalanuvchilar sonini qaytaradi.- Nima uchun handler ichida
f"INSERT ... VALUES ({user_id})"yozish xavfli? Bir jumlada tushuntiring va to'g'ri variantini yozing.
O'rta¶
UserRepogaset_lang(self, user_id, lang)metodini qo'shing. Foydalanuvchi tiliniUPDATEbilan o'zgartirsin.- UPSERT:
set_langshunday bo'lsinki, foydalanuvchi bazada yo'q bo'lsa qo'shsin, bor bo'lsa yangilasin.INSERT ... ON CONFLICT(id) DO UPDATE SET ...ishlating. DbSessionMiddleware(aiosqlite versiyasi) yozing vadp.update.middleware(...)bilan ulang. Handlerreponi parametr sifatida olsin.top_users(db, limit=5)β eng ko'p xabar yuborgan 5 foydalanuvchini qaytaring (ORDER BY ... DESC LIMIT ?). (usersjadvalidamsg_countustuni bor deb faraz qiling.)- SQLAlchemy 2.0 modelida
Messageklassini yozing:id,user_id(UsergaForeignKey),text.mapped_columnvaMapped[...]ishlating.
Qiyin¶
- Migratsiya tizimini kengaytiring:
MIGRATIONSro'yxatigalang TEXT DEFAULT 'uz'qo'shadigan v4 qadamini qo'shing vaapply_migrationsni qayta chaqirilganda faqat v4 qo'llanishini (1 qaytarishini) tekshiring. - 10.6'dagi handler+middleware botini token'siz
feed_updatebilan sinang: ikki marta/start(bitta user) va bir marta boshqa user'dan yuborib, javoblar to'g'riliginiassertqiling. (10.6/10.7 namunasidan foydalaning.) UserRepogaban(self, user_id)vais_banned(self, user_id)metodlarini qo'shing. So'ng "ban tekshiruvchi" outer middleware yozing: agar foydalanuvchi ban qilingan bo'lsa, handler umuman chaqirilmasin (returnqiling,handler(event, data)ni chaqirmang).
Yechimlar
Oson¶
1.
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
text TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
INTEGER PRIMARY KEY AUTOINCREMENT β id har INSERTda avtomatik o'sadi.
2.
async def add_message(db, user_id: int, text: str) -> None:
await db.execute(
"INSERT INTO messages (user_id, text) VALUES (?, ?)",
(user_id, text),
)
await db.commit()
3.
import aiosqlite
async def get_messages(db, user_id: int) -> list[dict]:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT * FROM messages WHERE user_id = ? ORDER BY id", (user_id,)
) as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
4.
async def count_users(db) -> int:
async with db.execute("SELECT COUNT(*) FROM users") as cur:
(n,) = await cur.fetchone()
return n
5. Chunki foydalanuvchi user_id o'rniga zararli matn (masalan 1); DROP TABLE users; --) yuborsa, u SQL'ga aralashib jadvalni o'chirishi mumkin β bu SQL injection. To'g'ri variant qiymatni ? orqali alohida uzatadi:
? ishlatilganda drayver qiymatni "ma'lumot" deb qabul qiladi, hech qachon SQL kod sifatida bajarmaydi.
O'rta¶
6.
async def set_lang(self, user_id: int, lang: str) -> None:
await self.db.execute(
"UPDATE users SET lang = ? WHERE id = ?", (lang, user_id)
)
# commit middleware'da yoki shu yerda:
await self.db.commit()
7. UPSERT β bazada bor/yo'qligiga qaramay ishlaydi:
async def set_lang(self, user_id: int, full_name: str, lang: str) -> None:
await self.db.execute(
"""
INSERT INTO users (id, full_name, lang) VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET lang = excluded.lang
""",
(user_id, full_name, lang),
)
await self.db.commit()
excluded β bu INSERT qilmoqchi bo'lgan yangi qiymatga ishora. Offline sinaldi: set_lang(1, "Ali", "uz") keyin set_lang(1, "Ali", "en") β bazada lang == "en".
8.
from typing import Any, Awaitable, Callable
import aiosqlite
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
class DbSessionMiddleware(BaseMiddleware):
def __init__(self, db_path: str) -> None:
self.db_path = db_path
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
async with aiosqlite.connect(self.db_path) as db:
data["repo"] = UserRepo(db)
result = await handler(event, data)
await db.commit()
return result
# ulash:
dp.update.middleware(DbSessionMiddleware("bot.db"))
# handler:
@router.message(CommandStart())
async def start(message, repo: UserRepo):
await repo.add_if_absent(message.from_user.id, message.from_user.full_name, message.from_user.username)
await message.answer("Ro'yxatga olindingiz")
9.
import aiosqlite
async def top_users(db, limit: int = 5) -> list[dict]:
db.row_factory = aiosqlite.Row
async with db.execute(
"SELECT full_name, msg_count FROM users ORDER BY msg_count DESC LIMIT ?",
(limit,),
) as cur:
return [dict(r) for r in await cur.fetchall()]
Olim (5) chiqdi.
10.
from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column
class Message(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("users.id"))
text: Mapped[str] = mapped_column(String(4096))
Qiyin¶
11.
MIGRATIONS = [
"CREATE TABLE users (id INTEGER PRIMARY KEY, full_name TEXT NOT NULL);", # v1
"ALTER TABLE users ADD COLUMN username TEXT;", # v2
"ALTER TABLE users ADD COLUMN is_banned INTEGER DEFAULT 0;", # v3
"ALTER TABLE users ADD COLUMN lang TEXT DEFAULT 'uz';", # v4 (yangi)
]
# v3 gacha qo'llangan bazada qayta chaqirsangiz:
n = await apply_migrations(db)
assert n == 1 # faqat v4 qo'llanadi
# user_version endi 4, lang ustuni qo'shildi
version > current faqat hali qo'llanmaganlarni tanlaydi.
12.
import asyncio
from datetime import datetime
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Chat, Message, Update, User as TgUser
SENT = []
router = Router()
@router.message(CommandStart())
async def start(message: Message, repo: UserRepo):
is_new = await repo.add_if_absent(message.from_user.id, message.from_user.full_name, message.from_user.username)
total = await repo.count()
SENT.append(f"{'yangi' if is_new else 'qayta'}:{total}")
def msg(uid, name):
return Message(message_id=1, date=datetime.now(),
chat=Chat(id=uid, type="private"),
from_user=TgUser(id=uid, is_bot=False, first_name=name),
text="/start")
async def main():
# ... engine, Session, create_tables tayyorlangan deb faraz qiling ...
bot = Bot(token="123456:AAH-FakeTest_abc") # SOXTA token
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(router)
dp.update.middleware(DbSessionMiddleware(Session))
await dp.feed_update(bot, Update(update_id=1, message=msg(100, "Ali")))
await dp.feed_update(bot, Update(update_id=2, message=msg(100, "Ali")))
await dp.feed_update(bot, Update(update_id=3, message=msg(200, "Olim")))
await bot.session.close()
assert SENT == ["yangi:1", "qayta:1", "yangi:2"]
print("OK")
asyncio.run(main())
13.
# repo metodlari:
async def ban(self, user_id: int) -> None:
await self.db.execute("UPDATE users SET is_banned = 1 WHERE id = ?", (user_id,))
await self.db.commit()
async def is_banned(self, user_id: int) -> bool:
async with self.db.execute("SELECT is_banned FROM users WHERE id = ?", (user_id,)) as cur:
row = await cur.fetchone()
return bool(row and row[0])
# outer middleware β handler chaqirilishidan OLDIN va filterlardan oldin ishlaydi:
class BanMiddleware(BaseMiddleware):
async def __call__(self, handler, event, data):
repo = data.get("repo")
user = data.get("event_from_user") # aiogram avtomatik beradi
if repo and user and await repo.is_banned(user.id):
return # handler CHAQIRILMAYDI -> foydalanuvchi e'tiborsiz
return await handler(event, data)
# ulash tartibi muhim: avval DB (repo kerak), keyin ban:
dp.update.outer_middleware(DbSessionMiddleware(...)) # repo'ni qo'yadi
dp.update.outer_middleware(BanMiddleware()) # repo'dan foydalanadi
outer_middleware filterlardan oldin ishlagani uchun ban qilingan foydalanuvchining xabari hatto handler filterlarigacha yetib bormaydi. handler(event, data)ni chaqirmasdan return qilsangiz β zanjir to'xtaydi.
β¬ οΈ Oldingi: 09 β Middleware Β· π README Β· Keyingi: 11 β Loyiha tuzilishi va konfiguratsiya β‘οΈ