10-bo‘lim
Ma'lumotlarni loyihalash
Domen modeli, ma'lumotlar bazasi sxemasi, migratsiyalar, kesh va ma'lumot yaxlitligi.
Ushbu bo‘lim mundarijasi
Ma'lumot - tizimning eng uzoq yashaydigan qismi. Kod bir necha marta qayta yoziladi, ma'lumot esa qoladi.
Domen modeli #
Kod ichida biznes tushunchalarini aks ettiruvchi obyektlar.
// YOMON: anemik model - faqat ma'lumot, mantiq yo'q
class Buyurtma
{
public int $id;
public int $mijozId;
public string $holat;
public float $jami;
public array $elementlar;
}
// Mantiq boshqa joyda sochilgan
class BuyurtmaServisi
{
public function bekorQiling(Buyurtma $buyurtma): void
{
if ($buyurtma->holat === 'yolda' || $buyurtma->holat === 'yetkazildi') {
throw new Exception('Bekor qilib bo\'lmaydi');
}
$buyurtma->holat = 'bekor';
}
}
// YAXSHI: boy model - ma'lumot va mantiq birga
final class Buyurtma
{
private array $elementlar = [];
private function __construct(
public readonly BuyurtmaId $id,
public readonly MijozId $mijozId,
private BuyurtmaHolati $holat,
public readonly DateTimeImmutable $yaratilgan,
) {}
public static function yangi(MijozId $mijozId): self
{
return new self(
BuyurtmaId::yaratish(),
$mijozId,
BuyurtmaHolati::Yangi,
new DateTimeImmutable(),
);
}
public function elementQoshing(Mahsulot $mahsulot, int $soni): void
{
if ($this->holat !== BuyurtmaHolati::Yangi) {
throw new BuyurtmaOzgartirilmaydi($this->id);
}
if ($soni < 1) {
throw new NotogriSoni($soni);
}
$this->elementlar[] = new BuyurtmaElementi(
$mahsulot->id,
$mahsulot->nomi,
$mahsulot->narx, // narx muzlatiladi
$soni,
);
}
public function jami(): Pul
{
return array_reduce(
$this->elementlar,
fn (Pul $yigindi, BuyurtmaElementi $e) => $yigindi->qoshing($e->summa()),
Pul::nol(),
);
}
public function bekorQiling(string $sabab): void
{
if (!$this->holat->bekorQilishMumkinmi()) {
throw new BuyurtmaBekorQilinmaydi($this->id, $this->holat);
}
$this->holat = BuyurtmaHolati::Bekor;
}
}
- Biznes qoidalari bir joyda - obyekt ichida
- Obyekt noto'g'ri holatga tusha olmaydi
- Kod biznes tilida o'qiladi
- Testlash oson - baza kerak emas
Anemik modelda esa qoida uch xil servisda takrorlanadi va biri unutiladi.
Qiymat obyektlari #
final class Pul
{
private function __construct(
public readonly int $tiyin,
public readonly string $valyuta,
) {
if ($tiyin < 0) {
throw new InvalidArgumentException('Pul manfiy bo\'lolmaydi');
}
}
public static function somdan(float $som, string $valyuta = 'UZS'): self
{
return new self((int) round($som * 100), $valyuta);
}
public static function nol(string $valyuta = 'UZS'): self
{
return new self(0, $valyuta);
}
public function qoshing(self $boshqa): self
{
$this->valyutaTekshiring($boshqa);
return new self($this->tiyin + $boshqa->tiyin, $this->valyuta);
}
public function kopaytiring(int $son): self
{
return new self($this->tiyin * $son, $this->valyuta);
}
public function kattaroqmi(self $boshqa): bool
{
$this->valyutaTekshiring($boshqa);
return $this->tiyin > $boshqa->tiyin;
}
public function som(): float
{
return $this->tiyin / 100;
}
private function valyutaTekshiring(self $boshqa): void
{
if ($this->valyuta !== $boshqa->valyuta) {
throw new ValyutalarMosKelmadi($this->valyuta, $boshqa->valyuta);
}
}
}
$narx = Pul::somdan(250_000);
$jami = $narx->kopaytiring(3);
echo $jami->som();
750000
- O'zgarmas (immutable) - yaratilgandan keyin o'zgarmaydi
- Qiymat bo'yicha tenglik -
idyo'q, mazmun muhim - O'z-o'zini tekshiradi - noto'g'ri holatda yaratilmaydi
Misollar: Pul, Email, Telefon, Manzil, Sana oralig'i,
Koordinata.
float $narx o'rniga Pul $narx ishlatish valyuta aralashib
ketishining oldini oladi.
Baza sxemasini loyihalash #
CREATE TABLE buyurtmalar (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
raqam VARCHAR(20) NOT NULL,
mijoz_id BIGINT UNSIGNED NOT NULL,
holat ENUM('yangi','tolangan','yigilmoqda','yolda','yetkazildi','bekor')
NOT NULL DEFAULT 'yangi',
jami_tiyin BIGINT UNSIGNED NOT NULL DEFAULT 0,
valyuta CHAR(3) NOT NULL DEFAULT 'UZS',
yaratilgan TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
yangilangan TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_raqam (raqam),
KEY idx_mijoz_sana (mijoz_id, yaratilgan DESC),
KEY idx_holat (holat),
CONSTRAINT fk_buyurtma_mijoz
FOREIGN KEY (mijoz_id) REFERENCES mijozlar(id)
ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
jami_tiyin BIGINT - kasr emas.
DECIMAL ham to'g'ri, lekin butun son bilan hisoblash xatosi
umuman bo'lmaydi va tillar orasida bir xil ishlaydi.
FLOAT esa hech qachon pul uchun ishlatilmasin.
Loyihalash qoidalari #
| Qoida | Sabab |
|---|---|
| Har bir jadvalda birlamchi kalit | Qatorni aniq identifikatsiya qilish |
| Tashqi kalitlar e'lon qilingan | Yaxlitlikni baza kafolatlaydi |
NOT NULL standart | NULL alohida ma'no bildirsagina |
Nomlar izchil (snake_case) | O'qish va yozish oson |
| Jadval nomi ko'plikda | mijozlar, buyurtmalar |
yaratilgan va yangilangan | Deyarli har doim kerak bo'ladi |
| Pul - butun son yoki DECIMAL | Yaxlitlash xatosi bo'lmasin |
utf8mb4 kodlash | Emoji va barcha belgilar |
| Indekslar so'rovlarga mos | Tez ishlash |
RESTRICT, CASCADE emas?ON DELETE CASCADE -- mijoz o'chsa, buyurtmalari ham o'chadi
ON DELETE RESTRICT -- buyurtmasi bor mijozni o'chirib bo'lmaydi
Moliyaviy ma'lumot hech qachon avtomatik o'chirilmasligi kerak.
Mijoz "o'chirilsa" - u aslida arxivlanadi:
ALTER TABLE mijozlar ADD ochirilgan TIMESTAMP NULL;
Yumshoq o'chirish #
trait YumshoqOchirish
{
public function ochiring(): void
{
$this->ochirilgan = new DateTimeImmutable();
}
public function ochirilganmi(): bool
{
return $this->ochirilgan !== null;
}
}
-- Faol yozuvlar uchun ko'rinish
CREATE VIEW faol_mijozlar AS
SELECT * FROM mijozlar WHERE ochirilgan IS NULL;
- Har bir so'rovga
WHERE ochirilgan IS NULLqo'shish kerak - biri unutiladi UNIQUEcheklovlar buziladi: o'chirilgan email qayta ishlatilmaydi- Jadval kattalashib boradi
Yechim: o'chirilgan yozuvlarni arxiv jadvaliga ko'chirish.
Migratsiyalar #
// database/migrations/2026_08_15_120000_buyurtmalarga_izoh_qoshish.php
final class BuyurtmalargaIzohQoshish
{
public function yuqoriga(PDO $pdo): void
{
$pdo->exec("
ALTER TABLE buyurtmalar
ADD COLUMN izoh VARCHAR(500) NULL AFTER holat
");
}
public function pastga(PDO $pdo): void
{
$pdo->exec("ALTER TABLE buyurtmalar DROP COLUMN izoh");
}
}
Migratsiya serverga chiqarilgan bo'lsa, u o'zgarmas hisoblanadi.
Xato bo'lsa - yangi migratsiya yozing:
// 2026_08_16_090000_izoh_uzunligini_oshirish.php
$pdo->exec("ALTER TABLE buyurtmalar MODIFY izoh TEXT NULL");
Xavfsiz migratsiya #
// XAVFLI: to'xtatishsiz deploy da ustun yo'qolsa - eski kod buziladi
$pdo->exec("ALTER TABLE mijozlar DROP COLUMN eski_telefon");
Ma'lumot yaxlitligi #
Uch darajada himoya:
// 1. Ilova darajasi - foydalanuvchiga tushunarli xabar
if ($soni < 1) {
throw new NotogriSoni('Soni kamida 1 bo\'lishi kerak');
}
-- 2. Baza darajasi - oxirgi himoya
CONSTRAINT chk_soni CHECK (soni >= 1)
// 3. Domen darajasi - obyekt noto'g'ri holatga tushmaydi
final class Soni
{
public function __construct(public readonly int $qiymat)
{
if ($qiymat < 1) {
throw new InvalidArgumentException('Soni musbat bo\'lishi kerak');
}
}
}
Ilova kodi o'zgaradi, birov INSERT ni qo'lda bajaradi, migratsiya
skripti xato yozadi.
Baza - oxirgi himoya chizig'i. U hech qachon ishonmasin.
Tranzaksiyalar #
final class BuyurtmaServisi
{
public function yarating(MijozId $mijozId, Savat $savat): Buyurtma
{
$this->pdo->beginTransaction();
try {
$buyurtma = Buyurtma::yangi($mijozId);
foreach ($savat->elementlar() as $element) {
$mahsulot = $this->mahsulotlar->qulflabToping($element->mahsulotId);
if (!$mahsulot->zaxiraYetarlimi($element->soni)) {
throw new ZaxiraYetarliEmas($mahsulot->nomi);
}
$buyurtma->elementQoshing($mahsulot, $element->soni);
$this->mahsulotlar->zaxiraKamaytiring($mahsulot->id, $element->soni);
}
$this->buyurtmalar->saqlang($buyurtma);
$this->pdo->commit();
return $buyurtma;
} catch (Throwable $xato) {
$this->pdo->rollBack();
throw $xato;
}
}
}
$this->pdo->beginTransaction();
$this->buyurtmalar->saqlang($buyurtma);
$this->tolovAPI->tolang($summa); // XAVFLI: 30 soniya kutishi mumkin
$this->email->yuboring($xabar); // XAVFLI
$this->pdo->commit();
Tashqi xizmat sekin javob bersa - baza qulflari uzoq turadi va butun tizim sekinlashadi.
To'g'ri yondashuv: tranzaksiyani yoping, keyin tashqi chaqiruvni qiling (yoki navbatga qo'ying).
Keshlash #
final class KeshlanganMahsulotRepozitoriyi implements MahsulotRepozitoriyi
{
public function __construct(
private MahsulotRepozitoriyi $keyingi,
private KeshInterfeysi $kesh,
private int $muddat = 3600,
) {}
public function toping(int $id): ?Mahsulot
{
$kalit = "mahsulot:{$id}";
$saqlangan = $this->kesh->oling($kalit);
if ($saqlangan !== null) {
return $saqlangan;
}
$mahsulot = $this->keyingi->toping($id);
if ($mahsulot !== null) {
$this->kesh->saqlang($kalit, $mahsulot, $this->muddat);
}
return $mahsulot;
}
public function saqlang(Mahsulot $mahsulot): Mahsulot
{
$natija = $this->keyingi->saqlang($mahsulot);
$this->kesh->ochiring("mahsulot:{$natija->id}");
return $natija;
}
}
Asosiy repozitoriy kesh haqida hech nima bilmaydi.
Keshni yoqish/o'chirish - shunchaki dekoratorni qo'shish yoki olib tashlash.
- Ma'lumot tez-tez o'zgarsa
- Eskirgan ma'lumot qabul qilib bo'lmaydigan bo'lsa (hisob qoldig'i)
- Foydalanuvchiga xos ma'lumot (uni keshlash - xavfsizlik xavfi)
- Sekinlik sababi kesh emas, yomon so'rov bo'lsa
Avval so'rovni optimallashtiring, keyin kesh qo'shing.
Ko'p ishlatiladigan so'rovlarni tekshirish #
// N+1 muammosi
$buyurtmalar = $repozitoriy->hammasi(); // 1 so'rov
foreach ($buyurtmalar as $buyurtma) {
echo $buyurtma->mijoz()->ism; // har biri uchun 1 so'rov!
}
// Jami: 1 + N so'rov
// Yechim: oldindan yuklash
$buyurtmalar = $repozitoriy->hammasiMijozlarBilan(); // 2 so'rov
SELECT b.*, m.ism, m.email
FROM buyurtmalar b
JOIN mijozlar m ON m.id = b.mijoz_id
WHERE b.yaratilgan > ?
100 ta buyurtma sahifasi 101 ta so'rov yuborsa, sahifa 2 soniyada ochiladi.
Har bir sahifada nechta so'rov ketayotganini o'lchang. Ishlab chiqish muhitida so'rovlar sonini ko'rsatuvchi panel o'rnating.
Ma'lumot arxivlash #
-- Eski buyurtmalarni arxivga ko'chirish
CREATE TABLE buyurtmalar_arxiv LIKE buyurtmalar;
INSERT INTO buyurtmalar_arxiv
SELECT * FROM buyurtmalar
WHERE yaratilgan < DATE_SUB(NOW(), INTERVAL 2 YEAR);
DELETE FROM buyurtmalar
WHERE yaratilgan < DATE_SUB(NOW(), INTERVAL 2 YEAR);
Juda katta jadvallar uchun MySQL o'zi bo'lishni qo'llab-quvvatlaydi:
ALTER TABLE buyurtmalar
PARTITION BY RANGE (YEAR(yaratilgan)) (
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p2026 VALUES LESS THAN (2027),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
Eski bo'limni o'chirish bir zumda bajariladi.
- Anemik modelni boy modelga aylantiring.
Pulqiymat obyektini yozing va testlang.EmailvaTelefonqiymat obyektlarini yozing.- Onlayn do'kon uchun to'liq SQL sxemasini yozing.
- Barcha tashqi kalitlar va indekslarni qo'shing.
- Ikkita migratsiya yozing: ustun qo'shish va uni o'zgartirish.
- "Kengaytir va qisqartir" naqshi bo'yicha ustun nomini o'zgartiring.
- Kesh dekoratorini yozing.
- Loyihangizda N+1 muammosini toping va tuzating.
- Yumshoq o'chirish uchun ko'rinish (VIEW) yarating.
Xulosa #
- Boy domen modeli biznes qoidalarini obyekt ichida saqlaydi.
- Qiymat obyektlari o'zgarmas, o'z-o'zini tekshiradi va turlarni aralashtirmaydi.
- Pulni butun son yoki
DECIMALda saqlang, hech qachonFLOATda emas. - Moliyaviy ma'lumot uchun
RESTRICT,CASCADEemas. - Migratsiya ishga tushgandan keyin o'zgarmas - yangisini yozing.
- To'xtatishsiz deploy uchun kengaytir va qisqartir naqshi.
- Yaxlitlikni uch darajada ta'minlang: domen, ilova, baza.
- Tranzaksiya ichida tashqi chaqiruv qilmang.
- Keshni dekorator sifatida qo'shing - asosiy kod bilmasin.
- N+1 muammosi - eng ko'p uchraydigan ishlash muammosi.
Keyingi bo'limda API loyihalashni ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.