Ачааллж байна...
Курс 2-ын Түвшин 3-ын бүтээн байгуулалтыг санаж байна уу? Номын сангийн систем — Book dataclass, Member class, Library class, зээлэх, буцаах логик, JSON файлд хадгалах.
Тэр программ таны компьютер дээр, терминал дээр ажилладаг байсан. Ганц хүн, ганц удаад.
Одоо тэр программ жинхэнэ вэб үйлчилгээ болж дахин төрөх гэж байна. Өгөгдлийн сантай, олон router-тэй, хамгаалалттай, pagination-тай, интернэтээр хандах боломжтой.
Энэ бол таны бүтээх хамгийн том, хамгийн бүрэн программ. Гурван хичээлээр барина:
Хичээл 1 (энэ): Төслийн суурь, model-ууд, өгөгдлийн сан, тохиргоо.
Хичээл 2: Ном болон гишүүний бүрэн CRUD, хоёр router, pagination.
Хичээл 3: Зээлэх/буцаах логик, бодит дүрмүүд, хамгаалалт, эцсийн аялал.
Энэ хичээлүүд нь дагаж бичих хичээлүүд. Гэрийн даалгавар биш. Бид мөр мөрөөр хамтдаа барина.
Эхлээд төлөвлөе. Бодит төсөл эхлүүлэхийн өмнөх ажил — юу хийхээ мэдэх.
Гурван зүйл
Ном (Book) — номын сангийн ном. Гарчиг, зохиолч, хэвлэгдсэн он, ISBN, нийт хувь, боломжтой хувь.
Гишүүн (Member) — номын сангийн гишүүн. Нэр, имэйл, бүртгүүлсэн огноо, идэвхтэй эсэх.
Зээл (Loan) — хэн ямар номыг хэзээ авсан, буцаасан эсэх. Энэ бол ном болон гишүүнийг холбодог бичлэг.
Гурав дахь нь чухал. Бүлэг 10-ын Хичээл 1-т бид "холбоо" гэсэн асуудлыг дурдсан — өгөгдөл хоорондоо хамааралтай байх. Одоо түүнийг бодитоор хийнэ.
Endpoint-ийн төлөвлөгөө
Method | Зам | Юу хийх | Хамгаалалт |
|---|---|---|---|
GET |
| Номын жагсаалт (хайлт, pagination) | Нээлттэй |
GET |
| Нэг ном | Нээлттэй |
POST |
| Ном нэмэх | API key |
PUT |
| Ном засах | API key |
DELETE |
| Ном устгах | API key |
GET |
| Гишүүдийн жагсаалт | API key |
GET |
| Нэг гишүүн | API key |
POST |
| Гишүүн нэмэх | API key |
PUT |
| Гишүүн засах | API key |
DELETE |
| Гишүүн устгах | API key |
POST |
| Ном зээлэх | API key |
POST |
| Ном буцаах | API key |
GET |
| Зээлийн түүх | API key |
Арван гурван endpoint, гурван router.
Хамгаалалтын логикийг анзаараарай. Номын жагсаалт нээлттэй — хэн ч номын санд ямар ном байгааг харж болно. Гишүүний мэдээлэл хаалттай — хувийн өгөгдөл. Бүх бичих үйлдэл хаалттай.
Энэ бол бодит шийдвэр: юу нээлттэй, юу хаалттай байхыг ухамсартай сонгох. Бүлэг 8-ын Хичээл 3-т ярьсан зарчим.
library-api/
├── venv/
├── .env
├── .env.example
├── .gitignore
├── library.db
├── main.py
└── app/
├── __init__.py
├── config.py
├── database.py
├── models.py
├── dependencies.py
├── errors.py
└── routers/
├── __init__.py
├── books.py
├── members.py
└── loans.pyТэмдэглэлийн API v3-ийн бүтэцтэй бараг ижил. Ялгаа: гурван router файл, нэг биш.
Энэ бол Бүлэг 7-т тогтоосон дүрмийн бодит хэрэглээ: нэг нөөц = нэг router = нэг файл.
Энэ бол шинэ төсөл тул шинэ хавтас, шинэ venv-ээр эхэлье. Бүлэг 1-ийн Хичээл 2-ын алхмуудыг давтана.
1. Шинэ хавтас үүсгэнэ. library-api (латин үсгээр, зайгүй).
2. VS Code-оор нээнэ. File -> Open Folder.
3. Терминал нээж, venv үүсгэнэ:
python -m venv venv4. Идэвхжүүлнэ:
.\venv\Scripts\ActivateМөрийн эхэнд (venv) харагдах ёстой.
5. Package суулгана:
pip install "fastapi[standard]" sqlmodel python-dotenvГурван package нэг тушаалаар. fastapi[standard] (сервер), sqlmodel (өгөгдлийн сан), python-dotenv (тохиргоо).
Хэрэв ModuleNotFoundError гарвал — venv идэвхтэй эсэхээ шалгаарай.
Хамгийн түрүүнд. Төслийн үндсэн хавтаст .gitignore файл үүсгэнэ:
.env
venv/
__pycache__/
*.pyc
*.dbХадгална.
Бүлэг 9-т ярьсанчлан: урьдчилан сэргийлэх нь цорын ганц найдвартай арга. Эхний өдөр үүсгэнэ.
.env файл үүсгэнэ:
API_KEY=номын-сангийн-нууц-түлхүүр
APP_NAME=Номын сангийн API
APP_VERSION=1.0.0
DATABASE_URL=sqlite:///library.db
MAX_PAGE_SIZE=50
MAX_LOANS_PER_MEMBER=3
ALLOWED_ORIGINS=http://localhost:3000.env.example файл үүсгэнэ:
API_KEY=энд-өөрийн-түлхүүрээ-бичнэ
APP_NAME=Номын сангийн API
APP_VERSION=1.0.0
DATABASE_URL=sqlite:///library.db
MAX_PAGE_SIZE=50
MAX_LOANS_PER_MEMBER=3
ALLOWED_ORIGINS=http://localhost:3000Хадгална.
MAX_LOANS_PER_MEMBER=3 — шинэ тохируулга. Гишүүн нэг зэрэг хамгийн ихдээ гурван ном зээлж болно. Энэ бол бизнесийн дүрэм бөгөөд түүнийг кодонд биш, тохиргоонд байлгах нь зөв — номын сан бодлогоо өөрчилвөл кодод гар хүрэхгүйгээр солино.
app хавтас үүсгэнэ. Дотор нь routers хавтас үүсгэнэ.
app/__init__.py болон app/routers/__init__.py — хоёулаа хоосон файл.
app/config.py:
python
# app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ["API_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
APP_NAME = os.getenv("APP_NAME", "Номын сангийн API")
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
MAX_PAGE_SIZE = int(os.getenv("MAX_PAGE_SIZE", "50"))
MAX_LOANS_PER_MEMBER = int(os.getenv("MAX_LOANS_PER_MEMBER", "3"))
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "").split(",")Хадгална.
Заавал хувьсагчид os.environ[...] (дутуу бол унана), заавал бусад os.getenv(...) default-тай. Бүлэг 9-ийн зарчим.
int() хөрвүүлэлт хоёр газар — .env-ээс текст ирдэг тул.
app/database.py:
python
# app/database.py
from sqlmodel import SQLModel, Session, create_engine
from app.config import DATABASE_URL
engine = create_engine(DATABASE_URL)
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield sessionХадгална.
Тэмдэглэлийн API v3-тэй яг ижил. Энэ файл төслөөс төсөлд бараг өөрчлөгддөггүй — стандарт хэсэг.
Одоо хамгийн чухал хэсэг. Энэ файл нь таны API-ийн бүх өгөгдлийн хэлбэрийг тодорхойлно.
app/models.py:
python
# app/models.py
from datetime import date, datetime
from sqlmodel import SQLModel, Field
# ─────────────────────────────────────────────
# НОМ
# ─────────────────────────────────────────────
class BookBase(SQLModel):
title: str
author: str
year: int
isbn: str = ""
total_copies: int = 1
class Book(BookBase, table=True):
id: int | None = Field(default=None, primary_key=True)
available_copies: int = 1
class BookCreate(BookBase):
pass
class BookPublic(BookBase):
id: int
available_copies: int
# ─────────────────────────────────────────────
# ГИШҮҮН
# ─────────────────────────────────────────────
class MemberBase(SQLModel):
name: str
email: str
is_active: bool = True
class Member(MemberBase, table=True):
id: int | None = Field(default=None, primary_key=True)
joined_at: date = Field(default_factory=date.today)
class MemberCreate(MemberBase):
pass
class MemberPublic(MemberBase):
id: int
joined_at: date
# ─────────────────────────────────────────────
# ЗЭЭЛ
# ─────────────────────────────────────────────
class Loan(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
book_id: int = Field(foreign_key="book.id")
member_id: int = Field(foreign_key="member.id")
borrowed_at: datetime = Field(default_factory=datetime.now)
returned_at: datetime | None = None
class LoanPublic(SQLModel):
id: int
book_id: int
member_id: int
borrowed_at: datetime
returned_at: datetime | None
class BorrowRequest(SQLModel):
book_id: int
member_id: int
class ReturnRequest(SQLModel):
loan_id: int
# ─────────────────────────────────────────────
# ЖАГСААЛТЫН БООДОЛ
# ─────────────────────────────────────────────
class BookList(SQLModel):
items: list[BookPublic]
total: int
skip: int
limit: int
has_more: bool
class MemberList(SQLModel):
items: list[MemberPublic]
total: int
skip: int
limit: int
has_more: bool
class LoanList(SQLModel):
items: list[LoanPublic]
total: int
skip: int
limit: int
has_more: bool
# ─────────────────────────────────────────────
# ЖАГСААЛТЫН ПАРАМЕТР
# ─────────────────────────────────────────────
class ListParams(SQLModel):
skip: int
limit: int
search: strХадгална. Энэ бол урт файл — доор хэсэг хэсгээр нь тайлбарлана.
Хамгийн түрүүнд нэг шинэ зүйл:
python
class BookBase(SQLModel):
title: str
author: str
year: int
isbn: str = ""
total_copies: int = 1
class Book(BookBase, table=True):
id: int | None = Field(default=None, primary_key=True)
available_copies: int = 1
class BookCreate(BookBase):
pass
class BookPublic(BookBase):
id: int
available_copies: intТэмдэглэлийн API-д бид гурван model бичихдээ талбаруудыг гурван удаа давтаж байсан:
python
class NoteCreate(SQLModel):
text: str
priority: str = "энгийн"
done: bool = False
class Note(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
text: str # давталт
priority: str = "энгийн" # давталт
done: bool = False # давталт
class NotePublic(SQLModel):
id: int
text: str # давталт
priority: str # давталт
done: bool # давталтГурван талбар, гурван удаа. Тав, зургаан талбартай model-д энэ нь тэвчихийн аргагүй болно.
Шийдэл: inheritance. Курс 2-ын Бүлэг 7-оос танил.
BookBase нь нийтлэг талбаруудыг агуулна. Бусад гурав нь түүнээс өвлөж, зөвхөн өөрийн онцлогийг нэмнэ.
class Book(BookBase, table=True) — BookBase-ийн бүх талбар + id + available_copies. Хүснэгт.
class BookCreate(BookBase): pass — BookBase-тэй яг ижил, нэмэлт юу ч байхгүй. pass нь "энд юу ч байхгүй" гэсэн Python түлхүүр үг.
class BookPublic(BookBase) — BookBase + id + available_copies. Гаралт.
Талбар нэг л удаа бичигдсэн. Хэрэв ирээдүйд publisher талбар нэмэх бол — зөвхөн BookBase-д нэмнэ, гурвуулаа автоматаар авна.
Энэ бол SQLModel-ийн албан ёсны баримт бичигт зөвлөдөг загвар бөгөөд бодит төслүүдэд түгээмэл.
Яагаад available_copies нь Base-д байхгүй вэ
Анзаараарай: available_copies нь BookBase-д байхгүй, зөвхөн Book болон BookPublic-д байна.
Учир нь хэрэглэгч түүнийг илгээх ёсгүй. Ном нэмэхэд "нийт хэдэн хувь" (total_copies) гэдгийг л хэлнэ; "хэдэн хувь боломжтой" гэдэг нь сервер тооцоолох зүйл.
Тиймээс BookCreate-д тэр талбар байхгүй — хэрэглэгч түүнийг тохируулж чадахгүй. BookPublic-д байна — хэрэглэгч уншиж чадна.
Энэ бол Бүлэг 5-6-т сурсан зарчим: хэрэглэгч юу өгөх ёстой, сервер юу нэмэх ёстой гэдгийг ялгах.
python
class Member(MemberBase, table=True):
id: int | None = Field(default=None, primary_key=True)
joined_at: date = Field(default_factory=date.today)joined_at: date — шинэ төрөл. Python-ы datetime модулийн date — зөвхөн огноо (цаггүй).
Field(default_factory=date.today) — Бүлэг 4-ийн Хичээл 4-т дурдсан default_factory. Яагаад энгийн = date.today() биш вэ?
Учир нь = date.today() бол нэг удаа, class зарлагдах үед ажиллана. Бүх гишүүн ижил огноотой болно — серверийг эхлүүлсэн өдөр.
default_factory=date.today бол функцийг зааж байна. Объект үүсэх бүрд тэр функц дуудагдана. Гишүүн бүр өөрийн бүртгүүлсэн огноотой болно.
Хаалт байхгүйг анзаараарай: date.today (функц), date.today() (үр дүн) биш. Depends-тэй ижил зарчим.
python
class Loan(SQLModel, table=True):
borrowed_at: datetime = Field(default_factory=datetime.now)
returned_at: datetime | None = Nonedatetime — огноо ба цаг. Зээл хэдэн цагт авсныг мэдэх нь ашигтай.
returned_at: datetime | None = None — буцаагаагүй бол None. Энэ бол чухал загвар: None нь "хараахан болоогүй" гэсэн утга илэрхийлж байна.
Дараагийн хичээлүүдэд бид returned_at is None гэсэн шалгалтаар "идэвхтэй зээл"-ийг олно.
FastAPI нь date болон datetime-ыг автоматаар JSON-д хөрвүүлнэ (Бүлэг 3-ын Хичээл 1-т дурдсан) — ISO форматын текст болно: "2026-07-16" эсвэл "2026-07-16T14:30:00".
Одоо хамгийн чухал шинэ ойлголт:
python
class Loan(SQLModel, table=True):
book_id: int = Field(foreign_key="book.id")
member_id: int = Field(foreign_key="member.id")foreign_key="book.id" — гадаад түлхүүр. Энэ нь "энэ багана нь book хүснэгтийн id баганыг заана" гэсэн үг.
Бүлэг 10-ын Хичээл 1-т бид энэ асуудлыг дурдсан: файлаар бол хоёр жагсаалтыг гараар нийлүүлэх ёстой, өнчин өгөгдөл үүсэх эрсдэлтэй.
Foreign key нь тэр асуудлыг шийднэ. Өгөгдлийн сан одоо дүрэм мэднэ: loan.book_id нь заавал байгаа номын id байх ёстой.
Хэрэв та байхгүй номын id-тай зээл үүсгэхийг оролдвол өгөгдлийн сан татгалзана:
sqlite3.IntegrityError: FOREIGN KEY constraint failedХүснэгтийн нэр жижиг үсгээр байгааг анзаараарай: "book.id", "Book.id" биш. Бүлэг 11-ийн Хичээл 2-т сурсанчлан class нь Book, хүснэгт нь book.
Холбоог зурагаар
book loan member
──── ──── ──────
id: 1 ◄──────── book_id: 1
title: ... member_id: 5 ────────► id: 5
borrowed_at: ... name: ...
returned_at: nullloan хүснэгт нь дунд байрлаж, хоёр талыг холбож байна. Энэ бол олон-олон харилцаа (many-to-many): нэг ном олон гишүүнд зээлдэж болно (өөр өөр цагт), нэг гишүүн олон ном авч болно.
Зээлийн хүснэгт нь тэр харилцааны бичлэг юм.
Юуг хийхгүй байгаа вэ
SQLModel-д Relationship гэсэн илүү хүчирхэг хэрэгсэл байдаг. Түүгээр loan.book гэж бичихэд номын бүтэн объект автоматаар ирдэг.
Бид түүнийг ашиглахгүй. Учир нь тэр нь нэмэлт төвөг (lazy loading, session-ийн амьдралын мөчлөг) авчирдаг бөгөөд энэ курсын хүрээнээс гадуур.
Оронд нь бид book_id-г ашиглан гараар номыг олно: session.get(Book, loan.book_id). Энэ нь илүү тодорхой, ойлгомжтой.
Бүлэг 15-т Relationship-ыг цаашдын зам болгон нэрлэнэ.
python
class BorrowRequest(SQLModel):
book_id: int
member_id: int
class ReturnRequest(SQLModel):
loan_id: intЭдгээр нь үйлдлийн model. Хүснэгт биш, гаралт биш — зөвхөн хүсэлтийн бие.
Зээлэхэд хэрэглэгч аль ном, аль гишүүн гэдгийг хэлнэ. Буцаахад аль зээл гэдгийг хэлнэ.
Энэ нь чухал загвар: бүх model нь хүснэгттэй тохирох албагүй. Заримдаа зөвхөн үйлдлийн параметрийг илэрхийлэх model хэрэгтэй болдог.
python
class BookList(SQLModel):
items: list[BookPublic]
total: int
skip: int
limit: int
has_more: boolБүлэг 13-ын Хичээл 3-ын загвар. Гурван нөөц бүрд нэг боодол.
Бүлэг 13-т ярьсан сургамжийг санаарай: эхнээсээ боодолтой хийх нь хамгийн зөв. Тэмдэглэлийн API-д бид дунд нь өөрчилсөн (breaking change). Энд эхнээсээ зөв.
page болон total_pages талбарыг оруулаагүйг анзаараарай — энгийн байлгах гэж. Хүсвэл нэмж болно (сонголтот өргөтгөл).
app/dependencies.py:
python
# app/dependencies.py
from fastapi import Header, HTTPException
from app.config import API_KEY, MAX_PAGE_SIZE
from app.models import ListParams
def verify_api_key(x_api_key: str = Header()):
if x_api_key != API_KEY:
raise HTTPException(status_code=403, detail="Түлхүүр буруу байна")
return x_api_key
def list_params(
skip: int = 0,
limit: int = 10,
search: str = "",
) -> ListParams:
if limit > MAX_PAGE_SIZE:
limit = MAX_PAGE_SIZE
if limit < 1:
limit = 1
if skip < 0:
skip = 0
return ListParams(skip=skip, limit=limit, search=search)Хадгална.
Тэмдэглэлийн API-тай бараг ижил. priority параметр байхгүй (номын санд хамаарахгүй).
Энэ dependency гурван router-т хоёуланд нь ашиглагдана — ном, гишүүн, зээл. Бүлэг 8-ын Хичээл 2-т ярьсан дахин ашиглалт.
app/errors.py — Тэмдэглэлийн API v3-ээс яг хуулж авна:
python
# app/errors.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
def register_error_handlers(app: FastAPI) -> None:
@app.exception_handler(HTTPException)
def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": True,
"status": exc.status_code,
"message": exc.detail,
"path": str(request.url.path),
},
)
@app.exception_handler(RequestValidationError)
def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = exc.errors()
first = errors[0] if errors else {}
field = first.get("loc", ["талбар"])[-1]
return JSONResponse(
status_code=422,
content={
"error": True,
"status": 422,
"message": f"'{field}' талбар буруу байна",
"path": str(request.url.path),
"details": errors,
},
)
@app.exception_handler(Exception)
def general_exception_handler(request: Request, exc: Exception):
print(f"АЛДАА: {request.url.path} — {type(exc).__name__}: {exc}")
return JSONResponse(
status_code=500,
content={
"error": True,
"status": 500,
"message": "Серверт алдаа гарлаа. Дараа дахин оролдоно уу.",
"path": str(request.url.path),
},
)Хадгална.
Энэ файл огт өөрчлөгдөөгүй. Тэмдэглэлийн API-аас Номын сангийн API руу шууд хуулагдсан.
Энэ бол сайн бүтцийн ашгийн бас нэг илрэл: зарим хэсэг нь төслөөс хамааралгүй тул дахин ашиглагдана. Алдаа боловсруулах логик нь та ном, тэмдэглэл, эсвэл огт өөр зүйл хадгалж байгаагаас хамаарахгүй.
Router-уудыг хараахан бичээгүй тул түр зуурын main.py бичье:
python
# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import ALLOWED_ORIGINS, APP_NAME, APP_VERSION
from app.database import create_db_and_tables
from app.errors import register_error_handlers
app = FastAPI(
title=APP_NAME,
version=APP_VERSION,
description="Номын сангийн ном, гишүүн, зээлийн бүртгэлийн API",
)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
register_error_handlers(app)
@app.on_event("startup")
def on_startup():
create_db_and_tables()
@app.get("/")
def root():
return {
"name": APP_NAME,
"version": APP_VERSION,
"docs": "/docs",
}Хадгална.
Router-ууд байхгүй — тэдгээрийг Хичээл 2-т нэмнэ. Одоохондоо суурь ажиллаж байгааг батлах гэж.
Терминал төслийн үндсэн хавтаст (library-api/) байгаа эсэхийг шалгаад:
fastapi dev main.pyШалгах цэг 1: сервер асав уу
Терминал дээр:
Server started at http://127.0.0.1:8000
Documentation at http://127.0.0.1:8000/docsШалгах цэг 2: өгөгдлийн сан үүсэв үү
Файлын жагсаалтаа хараарай. library.db файл гарч ирсэн байх ёстой.
Хэрэв гараагүй бол Explorer-ийг refresh хийнэ.
Шалгах цэг 3: root endpoint
http://127.0.0.1:8000/Хариу:
json
{
"name": "Номын сангийн API",
"version": "1.0.0",
"docs": "/docs"
}.env-ээс уншсан утгууд.
Шалгах цэг 4: /docs
http://127.0.0.1:8000/docsДээд талд Номын сангийн API 1.0.0, доор нь тайлбар. Ганц endpoint (GET /).
Endpoint цөөн байгаа нь хэвийн — router-уудыг хараахан нэмээгүй.
Шалгах цэг 5: хүснэгтүүд үүсэв үү
Хүснэгтүүд үүссэн эсэхийг батлахын тулд түр зуурын скрипт бичье. Төслийн хавтаст check_db.py:
python
# check_db.py
import sqlite3
connection = sqlite3.connect("library.db")
cursor = connection.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = cursor.fetchall()
print("Хүснэгтүүд:")
for table in tables:
print(" ", table[0])
connection.close()Шинэ терминал нээж (сервер ажиллаж байх ёстой, эсвэл зогсоосон ч болно), ажиллуулна:
python check_db.pyГаралт:
Хүснэгтүүд:
book
member
loanГурван хүснэгт үүссэн. SQLModel.metadata.create_all нь table=True бүхий гурван class-ыг олж, тус бүрд хүснэгт үүсгэсэн.
Хүснэгтийн нэр жижиг үсгээр байгааг анзаараарай — class нь Book, Member, Loan.
Батласны дараа check_db.py-г устгаж болно.
Та юу барив?
Ажиллаж байгаа сервер — /docs-тай, CORS тохируулагдсан, алдааны handler-тэй.
Гурван хүснэгттэй өгөгдлийн сан — book, member, loan, foreign key-тэй.
Бүрэн model-ын багц — base model inheritance-тай, огнооны талбартай, боодолтой хариунууд.
Тохиргоо — .env-д, кодоос салсан.
Dependency — API key болон жагсаалтын параметр.
Endpoint байхгүй, гэхдээ суурь бүрэн бэлэн. Дараагийн хичээлд бид зөвхөн endpoint бичихэд анхаарлаа төвлөрүүлнэ — бүтцийн ажил дууссан.
Энэ бол бодит төслийн ажиллах хэлбэр: эхлээд суурь, дараа нь чадвар. Суурь сайн бол чадвар нэмэх нь хурдан байдаг.
Base model-ыг table=True болгох
python
class BookBase(SQLModel, table=True): # буруу!
title: strТэгвэл BookBase ч хүснэгт болно — хоёр хүснэгт үүсч, будлиан гарна.
Зөвхөн нэг class table=True байх ёстой — жинхэнэ хүснэгтийнх.
foreign_key-д класын нэр бичих
python
book_id: int = Field(foreign_key="Book.id") # том үсэг — буруу
book_id: int = Field(foreign_key="book.id") # жижиг үсэг — зөвХүснэгтийн нэр жижиг үсгээр. Буруу бол:
sqlalchemy.exc.NoReferencedTableError: Foreign key ... could not find table 'Book'default_factory-д хаалт
python
joined_at: date = Field(default_factory=date.today()) # хаалт — буруу
joined_at: date = Field(default_factory=date.today) # зөвХаалттай бол функц нэг удаа дуудагдаж, бүх объект ижил огноотой болно.
datetime болон date андуурах
python
from datetime import date, datetimedate — зөвхөн огноо (2026-07-16). datetime — огноо ба цаг (2026-07-16T14:30:00).
Хоёуланг import хийх ёстой. Аль нэгийг мартвал NameError.
venv-ыг шинэ төсөлд идэвхжүүлэхгүй
Шинэ хавтаст шинэ venv үүсгэсэн тул түүнийг идэвхжүүлэх ёстой. Хуучин төслийн venv энд ажиллахгүй.
Хэрэв ModuleNotFoundError гарвал — venv идэвхтэй эсэхээ шалгаарай.
Capstone нь Курс 2-ын номын сангийн систем-ийг вэб үйлчилгээ болгож дахин төрүүлнэ.
Гурван нөөц: Ном, Гишүүн, Зээл. Гурав дахь нь хоёрыг холбоно.
Хамгаалалтын шийдвэр: номын жагсаалт нээлттэй, гишүүний мэдээлэл хаалттай, бүх бичих үйлдэл хаалттай.
Base model + inheritance — талбарыг нэг л удаа бичнэ: BookBase -> Book, BookCreate, BookPublic.
available_copies нь BookBase-д байхгүй — хэрэглэгч илгээх ёсгүй, сервер тооцоолно.
Field(default_factory=date.today) — хаалтгүй; хаалттай бол бүх объект ижил огноотой болно.
Field(foreign_key="book.id") — гадаад түлхүүр, хүснэгтийн нэр жижиг үсгээр. Өгөгдлийн сан холбоог хамгаална.
returned_at: datetime | None = None — None нь "хараахан болоогүй" гэсэн утга.
errors.py нь өмнөх төслөөс өөрчлөлтгүй хуулагдсан — сайн бүтцийн ашиг.
MAX_LOANS_PER_MEMBER нь бизнесийн дүрэм, тохиргоонд байх ёстой.
Гурван хүснэгт (book, member, loan) үүссэнийг батлав.
Суурь бэлэн боллоо. Дараагийн хичээлд бид Ном болон Гишүүний бүрэн CRUD бичнэ — хоёр router, pagination, хайлт, хамгаалалт. Тэнд та Түвшин 1-3-т сурсан бүх зүйлээ хэрэглэх бөгөөд ижил загварыг хоёр удаа хэрэглэх нь тэдгээрийг бататгах болно. Дараа нь Хичээл 3-т хамгийн сонирхолтой хэсэг — зээлийн логик, бодит дүрмүүдтэй.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.