Ачааллж байна...
Өмнөх хоёр хичээлд бид CRUD бичсэн — үүсгэх, унших, засах, устгах. Тэдгээр нь давтагдах загвар: ижил хэлбэр, ижил алхмууд, зөвхөн model өөр.
Энэ хичээл өөр. Энд бид бизнесийн логик бичнэ.
Ялгаа нь юу вэ? CRUD нь өгөгдлийн бүтцээс гардаг — ном бол ном, түүнийг хадгалж, уншиж, засна. Бизнесийн логик нь бодит ертөнцийн дүрмээс гардаг: хэн ном авч болох вэ, хэдийг авч болох вэ, хэзээ болохгүй вэ.
Тэдгээр дүрмийг өгөгдлийн бүтэц хэлж өгдөггүй. Тэдгээрийг номын сангийн бодлого хэлдэг. Программист түүнийг ойлгож, кодонд хөрвүүлэх ёстой.
Энэ бол программ хангамжийн жинхэнэ ажил юм. Мөн энэ бол таны бүх сургалтын оргил.
Эхлээд дүрмээ тодорхойлъё — кодоос өмнө. Энэ бол бодит төслийн зөв дараалал.
Зээлэхэд шалгах зүйлс
1. Ном байгаа юу? Байхгүй бол 404.
2. Гишүүн байгаа юу? Байхгүй бол 404.
3. Гишүүн идэвхтэй юу? Идэвхгүй гишүүн ном авч чадахгүй. Тэр нь торгуультай, эсвэл гишүүнчлэл дууссан байж болно.
4. Боломжтой хувь байна уу? Бүх хувь зээлэгдсэн бол хүлээх ёстой.
5. Гишүүн хязгаараа хэтрүүлээгүй юу? MAX_LOANS_PER_MEMBER — нэг зэрэг гурваас илүү ном авч болохгүй.
6. Гишүүн энэ номыг аль хэдийн аваагүй юу? Нэг хүн нэг номын хоёр хувь зэрэг авах шаардлагагүй.
Зургаан дүрэм. Тав нь бизнесийн шийдвэр, зөвхөн эхний хоёр нь техникийн.
Буцаахад шалгах зүйлс
1. Зээл байгаа юу? Байхгүй бол 404.
2. Аль хэдийн буцаагаагүй юу? Хоёр удаа буцаах боломжгүй.
Хажуугийн үр дагавар
Зээлэх үед: available_copies нэгээр багасна.
Буцаах үед: available_copies нэгээр нэмэгдэнэ, returned_at тохирогдоно.
Эдгээр нь чухал. Хэрэв мартвал тоо аажмаар алдагдаж, номын сан байхгүй ном зээлдүүлж эхэлнэ.
Бодит номын санчийг төсөөлье. Хэн нэгэн ном авах гэж ирлээ.
Номын санч дараалан шалгана. "Энэ ном бидэнд байна уу?" Каталогоос хайна. "Та гишүүн үү?" Үнэмлэх шалгана. "Таны гишүүнчлэл идэвхтэй юу?" Торгууль байгаа эсэхийг хардаг. "Тавиур дээр хувь байна уу?" Шалгана. "Та хэдэн ном авсан байна?" Бүртгэлээ хардаг.
Аль нэг шалгалт унавал тэр даруй зогсоно — цаашид явахгүй. "Уучлаарай, та аль хэдийн гурван ном авсан байна."
Бүх шалгалт өнгөрвөл л ном өгнө. Дараа нь бүртгэлдээ бичнэ: хэн, юуг, хэзээ. Мөн тавиурын тоог өөрчилнө.
Энэ бол яг таны бичих кодны хэлбэр юм. Guard clause загвар — Бүлэг 3-ын Хичээл 3-т сурсан: эхэнд бүх шалгалт, эцэст нь жинхэнэ ажил.
app/routers хавтаст loans.py файл үүсгэнэ:
python
# app/routers/loans.py
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, func, select
from app.config import MAX_LOANS_PER_MEMBER
from app.database import get_session
from app.dependencies import verify_api_key
from app.models import (
Book,
BorrowRequest,
Loan,
LoanList,
LoanPublic,
Member,
ReturnRequest,
)
router = APIRouter(
prefix="/loans",
tags=["Зээл"],
dependencies=[Depends(verify_api_key)],
)
@router.post("/borrow", status_code=201, response_model=LoanPublic)
def borrow_book(request: BorrowRequest, session: Session = Depends(get_session)):
# 1. Ном байгаа эсэх
book = session.get(Book, request.book_id)
if book is None:
raise HTTPException(status_code=404, detail="Ном олдсонгүй")
# 2. Гишүүн байгаа эсэх
member = session.get(Member, request.member_id)
if member is None:
raise HTTPException(status_code=404, detail="Гишүүн олдсонгүй")
# 3. Гишүүн идэвхтэй эсэх
if not member.is_active:
raise HTTPException(
status_code=400,
detail="Гишүүний бүртгэл идэвхгүй байна",
)
# 4. Боломжтой хувь байгаа эсэх
if book.available_copies < 1:
raise HTTPException(
status_code=400,
detail=f"'{book.title}' номын бүх хувь зээлэгдсэн байна",
)
# 5. Гишүүний зээлийн хязгаар
active_count = session.exec(
select(func.count())
.select_from(Loan)
.where(Loan.member_id == member.id, Loan.returned_at == None)
).one()
if active_count >= MAX_LOANS_PER_MEMBER:
raise HTTPException(
status_code=400,
detail=f"Нэг гишүүн зэрэг {MAX_LOANS_PER_MEMBER} номоос "
f"илүүг зээлж болохгүй",
)
# 6. Энэ номыг аль хэдийн аваагүй эсэх
existing = session.exec(
select(Loan).where(
Loan.member_id == member.id,
Loan.book_id == book.id,
Loan.returned_at == None,
)
).first()
if existing is not None:
raise HTTPException(
status_code=409,
detail="Энэ гишүүн уг номыг аль хэдийн зээлсэн байна",
)
# Бүх шалгалт өнгөрлөө — зээл үүсгэнэ
loan = Loan(book_id=book.id, member_id=member.id)
book.available_copies -= 1
session.add(loan)
session.commit()
session.refresh(loan)
return loan
@router.post("/return", response_model=LoanPublic)
def return_book(request: ReturnRequest, session: Session = Depends(get_session)):
# 1. Зээл байгаа эсэх
loan = session.get(Loan, request.loan_id)
if loan is None:
raise HTTPException(status_code=404, detail="Зээлийн бичлэг олдсонгүй")
# 2. Аль хэдийн буцаагаагүй эсэх
if loan.returned_at is not None:
raise HTTPException(
status_code=400,
detail="Энэ ном аль хэдийн буцаагдсан байна",
)
# Буцаалт бүртгэнэ
loan.returned_at = datetime.now()
book = session.get(Book, loan.book_id)
if book is not None:
book.available_copies += 1
session.commit()
session.refresh(loan)
return loan
@router.get("", response_model=LoanList)
def list_loans(
member_id: int | None = None,
book_id: int | None = None,
active_only: bool = False,
skip: int = 0,
limit: int = 10,
session: Session = Depends(get_session),
):
if limit > 50:
limit = 50
if limit < 1:
limit = 1
if skip < 0:
skip = 0
filters = []
if member_id is not None:
filters.append(Loan.member_id == member_id)
if book_id is not None:
filters.append(Loan.book_id == book_id)
if active_only:
filters.append(Loan.returned_at == None)
count_statement = select(func.count()).select_from(Loan)
for condition in filters:
count_statement = count_statement.where(condition)
total = session.exec(count_statement).one()
statement = select(Loan)
for condition in filters:
statement = statement.where(condition)
statement = statement.order_by(Loan.borrowed_at.desc())
statement = statement.offset(skip).limit(limit)
items = session.exec(statement).all()
return LoanList(
items=items,
total=total,
skip=skip,
limit=limit,
has_more=skip + len(items) < total,
)Хадгална.
Энэ бол таны бичсэн хамгийн урт, хамгийн логиктой endpoint. Мөр мөрөөр нь харцгаая.
Бүтцийг эхлээд харах
1. Шалга -> буруу бол raise
2. Шалга -> буруу бол raise
3. Шалга -> буруу бол raise
4. Шалга -> буруу бол raise
5. Шалга -> буруу бол raise
6. Шалга -> буруу бол raise
Бүх шалгалт өнгөрлөө -> жинхэнэ ажилХавтгай, дараалсан, уншихад амархан. Гүн үүрлэсэн if байхгүй.
Бүлэг 3-ын Хичээл 3-т ярьсан guard clause загвар. Тэнд би "энэ бол таны цаашид бичих бүх endpoint-ийн загвар" гэж хэлсэн. Одоо та түүний бүрэн хэлбэрийг харж байна.
Хэрэв if/else-ээр бичсэн бол зургаан түвшин гүн болж, уншихад бараг боломжгүй болох байсан.
Шалгалт 3: идэвхтэй эсэх
python
if not member.is_active:
raise HTTPException(
status_code=400,
detail="Гишүүний бүртгэл идэвхгүй байна",
)not member.is_active — энд Python-ы ердийн not ажиллана, учир нь энэ нь where дотор биш, харин ердийн Python объект дээр. Тэр ялгааг анзаараарай: where дотор SQLModel-ийн дүрэм, гадна нь Python-ы дүрэм.
400 ашиглаж байна — бизнесийн логикийн алдаа. Гишүүн байгаа (404 биш), хүсэлт зөв бүтэцтэй (422 биш), гэхдээ дүрмээр болохгүй.
Шалгалт 5: хязгаар тоолох
python
active_count = session.exec(
select(func.count())
.select_from(Loan)
.where(Loan.member_id == member.id, Loan.returned_at == None)
).one()Бүлэг 13-ын Хичээл 3-ын func.count() энд дахин ажиллаж байна — гэхдээ өөр зорилгоор. Тэнд pagination-д нийт тоог мэдэх гэж; энд дүрэм шалгах гэж.
Яагаад бүх зээлийг татаж len() хийхгүй вэ? Учир нь бидэнд зөвхөн тоо хэрэгтэй. Гишүүн зуун ном авсан байсан ч бид зуун бичлэгийг санах ойд авчрах шаардлагагүй — өгөгдлийн сан тоолж, нэг тоо буцаана.
Хоёр нөхцөл: тэр гишүүнийх ба буцаагаагүй. returned_at == None нь "идэвхтэй зээл" гэсэн утга.
python
if active_count >= MAX_LOANS_PER_MEMBER:>= ашигласныг анзаараарай, > биш. Хэрэв хязгаар нь 3 бол гурван ном авсан гишүүн дөрөв дэхийг авч болохгүй. active_count == 3 үед зогсоох ёстой.
MAX_LOANS_PER_MEMBER нь .env-ээс ирж байна. Номын сан бодлогоо өөрчилвөл — кодод гар хүрэхгүй.
Шалгалт 6: давхардал
python
existing = session.exec(
select(Loan).where(
Loan.member_id == member.id,
Loan.book_id == book.id,
Loan.returned_at == None,
)
).first()Гурван нөхцөл: тэр гишүүн, тэр ном, буцаагаагүй.
Хэрэв ийм зээл олдвол — гишүүн уг номыг аль хэдийн авсан байна. 409 (Conflict) — өмнөх хичээлээс танил.
Яагаад энэ дүрэм хэрэгтэй вэ? Нэг хүн нэг номын хоёр хувь зэрэг авах нь утгагүй. Мөн буцаахад аль зээл гэдгийг ялгахад төвөгтэй болно.
.first() — эхний тохирлыг авна, байхгүй бол None.
Жинхэнэ ажил
python
loan = Loan(book_id=book.id, member_id=member.id)
book.available_copies -= 1
session.add(loan)
session.commit()
session.refresh(loan)Дөрвөн мөрөнд хоёр өөрчлөлт байна.
Зээлийн бичлэг үүсэж байна — шинэ объект тул session.add хэрэгтэй.
Номын боломжтой хувь багасч байна — book нь session-аас авсан объект тул add хэрэггүй. Session түүний өөрчлөлтийг өөрөө мөрддөг (Бүлэг 11-ийн Хичээл 4).
session.commit() нь хоёуланг нэг дор хадгална. Энэ бол transaction-ийн ашиг: INSERT болон UPDATE хоёулаа болно, эсвэл хоёулаа болохгүй.
Энэ нь чухал. Хэрэв зээл үүсээд номын тоо буурахгүй бол — номын сан байхгүй ном зээлдүүлж эхэлнэ. Хэрэв тоо буураад зээл үүсэхгүй бол — ном "алга болно". Transaction нь хоёулаа хамт болохыг баталгаажуулна.
Бүлэг 10-ын Хичээл 1-т "transaction: бүгд болно, эсвэл юу ч болохгүй" гэж ярьсан. Энэ бол түүний бодит хэрэглээ.
loan.borrowed_at тохируулаагүйг анзаараарай — default_factory=datetime.now түүнийг автоматаар хийнэ.
Богино боловч чухал.
python
if loan.returned_at is not None:
raise HTTPException(
status_code=400,
detail="Энэ ном аль хэдийн буцаагдсан байна",
)Хоёр удаа буцаахаас сэргийлж байна.
Энэ дүрэм яагаад чухал вэ? Хэрэв байхгүй бол: хэн нэгэн ижил зээлийг хоёр удаа буцаавал available_copies хоёр удаа нэмэгдэнэ. Номын сан байхгүй хувь бүртгэнэ. Гурван хувьтай ном дөрвөн хувьтай болно.
Энэ бол idempotency-тэй холбоотой (Бүлэг 5-ын Хичээл 4). POST нь idempotent биш тул давтагдахаас гараар хамгаалах ёстой.
Энд is not None ажиллана — учир нь where дотор биш, ердийн Python шалгалт.
python
loan.returned_at = datetime.now()
book = session.get(Book, loan.book_id)
if book is not None:
book.available_copies += 1Хоёр өөрчлөлт: зээл дуусав, номын хувь нэмэгдэв.
if book is not None: — хамгаалалтын шалгалт. Ном устсан байх ёсгүй (бид устгахыг хориглосон), гэхдээ хамгаалалт нэмэх нь хор хөнөөлгүй. Хэрэв ямар нэг байдлаар устсан бол зээл нь буцаагдсан гэж тэмдэглэгдэнэ, программ унахгүй.
Энэ бол хамгаалалтын программчлал — "энэ болох ёсгүй, гэхдээ болвол зохих ёсоор ажиллана" гэсэн хандлага.
python
def list_loans(
member_id: int | None = None,
book_id: int | None = None,
active_only: bool = False,
skip: int = 0,
limit: int = 10,
session: Session = Depends(get_session),
):Энд list_params dependency ашиглаагүйг анзаараарай. Яагаад?
Учир нь зээлийн шүүлт нь өөр: текст хайлт биш, харин id-гаар шүүх. ListParams model-д search талбар байна, гэхдээ member_id байхгүй.
Бид өөр dependency үүсгэж болох байсан (LoanParams), гэхдээ ганц endpoint-д ашиглагдах тул шууд бичих нь илүү энгийн. Бүлэг 8-ын дүрэм: хоёроос дээш газар давтагдвал dependency болго; ганц газар бол шууд бич.
skip/limit шалгалтыг гараар бичсэн — list_params-д байсан шиг. Энэ бол бага зэрэг давхардал, гэхдээ ганц газар тул тэвчихийн аргатай.
python
statement = statement.order_by(Loan.borrowed_at.desc())Хамгийн сүүлийн зээл эхэнд. Зээлийн түүхийг харахад хамгийн шинэ нь эхэнд байх нь байгалийн.
Бүлэг 11-ийн Хичээл 3-ын order_by энд ажиллаж байна.
python
if active_only:
filters.append(Loan.returned_at == None)active_only=true — зөвхөн буцаагаагүй зээл. Номын санчид "одоо хэн ямар ном барьж байна" гэдгийг харах гол хэрэгсэл.
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
from app.routers import books, loans, members
tags_metadata = [
{"name": "Ном", "description": "Номын бүртгэл, хайлт, засвар."},
{"name": "Гишүүн", "description": "Гишүүний бүртгэл. API key шаардлагатай."},
{"name": "Зээл", "description": "Ном зээлэх, буцаах, түүх харах. API key шаардлагатай."},
]
app = FastAPI(
title=APP_NAME,
version=APP_VERSION,
description="Номын сангийн ном, гишүүн, зээлийн бүртгэлийн API",
openapi_tags=tags_metadata,
)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
register_error_handlers(app)
app.include_router(books.router)
app.include_router(members.router)
app.include_router(loans.router)
@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 нэмэгдлээ. main.py нь одоо ч зөвхөн угсарч байна — арван таван мөр орчим.
Одоо бид API-гаа бодит хувилбараар туршина. Энэ бол зөвхөн endpoint шалгах биш — номын сангийн нэг өдрийг дүрслэх явдал.
Сервер ажиллуулна. Хэрэв өмнөх хичээлээс өгөгдөл байгаа бол ашиглаж болно; цэвэр эхлэхийг хүсвэл library.db-г устгаад дахин асаана.
Бүх зээлийн endpoint API key шаардана — /docs-оор туршина.
Бэлтгэл: ном ба гишүүн
Хэрэв өгөгдөл байхгүй бол хоёр ном нэмнэ (POST /books, түлхүүртэй):
json
{"title": "Цог хийморь", "author": "Ч. Лодойдамба", "year": 1962, "total_copies": 2}json
{"title": "Цагаан хэрэм", "author": "С. Эрдэнэ", "year": 1971, "total_copies": 1}Хоёр гишүүн нэмнэ (POST /members):
json
{"name": "Дулгуун", "email": "dulguun@example.com"}json
{"name": "Бат", "email": "bat@example.com"}Одоо: ном 1 (2 хувь), ном 2 (1 хувь), гишүүн 1, гишүүн 2.
1. Эхний зээл — амжилттай
/docs -> POST /loans/borrow, түлхүүр зөв, body:
json
{"book_id": 1, "member_id": 1}Хариу (статус 201):
json
{
"id": 1,
"book_id": 1,
"member_id": 1,
"borrowed_at": "2026-07-16T14:23:45.123456",
"returned_at": null
}Зээл үүслээ. borrowed_at нь автоматаар тохирсон. returned_at нь null — хараахан буцаагаагүй.
2. Номын хувь буурсныг батлах
http://127.0.0.1:8000/books/1Хариу:
json
{
"title": "Цог хийморь",
...,
"total_copies": 2,
"available_copies": 1
}available_copies нь 2-оос 1 болов. Хажуугийн үр дагавар ажиллаж байна.
3. Ижил гишүүн, ижил ном — 409
Дахин ижил хүсэлт:
json
{"book_id": 1, "member_id": 1}Хариу (статус 409):
json
{
"error": true,
"status": 409,
"message": "Энэ гишүүн уг номыг аль хэдийн зээлсэн байна",
"path": "/loans/borrow"
}Дүрэм 6 ажиллаж байна.
4. Өөр гишүүн, ижил ном — амжилттай
json
{"book_id": 1, "member_id": 2}Хариу (статус 201): зээл 2 үүснэ.
Номыг шалгана: available_copies нь 0 болсон. Хоёр хувь хоёр гишүүнд.
5. Бүх хувь дууссан — 400
Гурав дахь гишүүн байхгүй тул гишүүн 1-ээр дахин оролдъё... гэхдээ тэр 409 өгнө (дүрэм 6).
Оронд нь гурав дахь гишүүн нэмээд (POST /members), дараа нь:
json
{"book_id": 1, "member_id": 3}Хариу (статус 400):
json
{
"error": true,
"status": 400,
"message": "'Цог хийморь' номын бүх хувь зээлэгдсэн байна",
"path": "/loans/borrow"
}Дүрэм 4 ажиллаж байна. Мессежид номын нэр орсон — f-string ашигласны ач холбогдол.
6. Байхгүй ном — 404
json
{"book_id": 999, "member_id": 1}Хариу (статус 404):
json
{
"error": true,
"status": 404,
"message": "Ном олдсонгүй",
"path": "/loans/borrow"
}7. Идэвхгүй гишүүн — 400
Эхлээд гишүүн 2-ыг идэвхгүй болгоно. /docs -> PUT /members/2:
json
{"name": "Бат", "email": "bat@example.com", "is_active": false}Дараа нь тэр гишүүнээр зээлэхийг оролдъё:
json
{"book_id": 2, "member_id": 2}Хариу (статус 400):
json
{
"error": true,
"status": 400,
"message": "Гишүүний бүртгэл идэвхгүй байна",
"path": "/loans/borrow"
}Дүрэм 3 ажиллаж байна.
Гишүүн 2-ыг буцааж идэвхжүүлээрэй (is_active: true).
8. Зээлийн хязгаар — 400
MAX_LOANS_PER_MEMBER=3 тул гишүүн 1-д гурван ном өгье. Тэр аль хэдийн нэгийг авсан.
Хоёр ном нэмнэ (POST /books), дараа нь гишүүн 1-ээр хоёуланг зээлнэ. Одоо гишүүн 1-д гурван идэвхтэй зээл байна.
Дөрөв дэх номыг нэмээд, зээлэхийг оролдъё:
json
{"book_id": 5, "member_id": 1}Хариу (статус 400):
json
{
"error": true,
"status": 400,
"message": "Нэг гишүүн зэрэг 3 номоос илүүг зээлж болохгүй",
"path": "/loans/borrow"
}Дүрэм 5 ажиллаж байна. Тоо нь .env-ээс ирсэн.
Хүсвэл .env-д MAX_LOANS_PER_MEMBER=5 болгож, серверээ дахин асаагаад дахин туршаарай — одоо ажиллана. Бизнесийн дүрэм тохиргоонд байгаагийн ач холбогдол.
9. Зээлийн жагсаалт
/docs -> GET /loansтүлхүүртэй, параметргүй.
Хариу:
json
{
"items": [
{"id":3,"book_id":4,"member_id":1,"borrowed_at":"...","returned_at":null},
{"id":2,"book_id":1,"member_id":2,"borrowed_at":"...","returned_at":null},
{"id":1,"book_id":1,"member_id":1,"borrowed_at":"...","returned_at":null}
],
"total": 3,
"skip": 0,
"limit": 10,
"has_more": false
}Хамгийн сүүлийн зээл эхэнд — order_by(...desc()) ажиллаж байна.
Шүүлт туршина: member_id=1 -> зөвхөн тэр гишүүний зээлүүд. active_only=true -> зөвхөн буцаагаагүй.
10. Буцаах — амжилттай
/docs -> POST /loans/return, түлхүүр зөв, body:
json
{"loan_id": 1}Хариу (статус 200):
json
{
"id": 1,
"book_id": 1,
"member_id": 1,
"borrowed_at": "2026-07-16T14:23:45.123456",
"returned_at": "2026-07-16T15:10:22.654321"
}returned_at тохирогдлоо.
Номыг шалгана:
http://127.0.0.1:8000/books/1available_copies нь 0-оос 1 болов. Ном тавиур дээр буцаж ирлээ.
11. Дахин буцаах — 400
Ижил хүсэлт:
json
{"loan_id": 1}Хариу (статус 400):
json
{
"error": true,
"status": 400,
"message": "Энэ ном аль хэдийн буцаагдсан байна",
"path": "/loans/return"
}Хамгаалалт ажиллаж байна. Хэрэв энэ шалгалт байхгүй бол available_copies дахин нэмэгдэж, номын сан байхгүй хувь бүртгэх байсан.
12. Буцаасны дараа дахин зээлэх — амжилттай
Одоо гишүүн 1 тэр номыг дахин авч чадах уу?
json
{"book_id": 1, "member_id": 1}Хариу (статус 201): шинэ зээл үүснэ.
Яагаад 409 гарахгүй байна вэ? Учир нь дүрэм 6 нь зөвхөн буцаагаагүй зээлийг шалгадаг (returned_at == None). Хуучин зээл буцаагдсан тул тоологдохгүй.
Энэ бол зөв зан төлөв: ном буцаасны дараа дахин авч болно.
13. Зээлэгдсэн ном устгах — 400
/docs -> DELETE /books/1, түлхүүр зөв.
Хариу (статус 400):
json
{
"error": true,
"status": 400,
"message": "Зээлэгдсэн хувьтай номыг устгах боломжгүй",
"path": "/books/1"
}Өмнөх хичээлд бичсэн хамгаалалт ажиллаж байна.
14. Буцаагаагүй номтой гишүүн устгах — 400
/docs -> DELETE /members/1, түлхүүр зөв.
Хариу (статус 400):
json
{
"error": true,
"status": 400,
"message": "Гишүүнд 3 буцаагаагүй ном байна",
"path": "/members/1"
}Өгөгдлийн бүрэн бүтэн байдал хамгаалагдлаа. Өнчин зээл үүсэхгүй.
Одоо суугаад бүтэн API-гаа хараарай.
http://127.0.0.1:8000/docsДээд талд: Номын сангийн API 1.0.0, тайлбартай.
Доор нь гурван бүлэг, арван гурван endpoint:
Ном
Номын бүртгэл, хайлт, засвар.
GET /books
POST /books
GET /books/{book_id}
PUT /books/{book_id}
DELETE /books/{book_id}
Гишүүн
Гишүүний бүртгэл. API key шаардлагатай.
GET /members
POST /members
GET /members/{member_id}
PUT /members/{member_id}
DELETE /members/{member_id}
Зээл
Ном зээлэх, буцаах, түүх харах. API key шаардлагатай.
POST /loans/borrow
POST /loans/return
GET /loansEndpoint бүр дээр товшвол: параметрүүд, request body-ийн бүтэц, хариуны schema, боломжит status code.
GET /books дээр x-api-key байхгүй — нээлттэй.
POST /loans/borrow дээр x-api-key байна — хаалттай.
Одоо энэ бүхнийг та бичээгүй гэдгийг санаарай.
Та ганц мөр HTML бичээгүй. Ганц мөр баримт бичиг бичээгүй. Ганц мөр JSON schema бичээгүй.
Та зөвхөн Python бичсэн — type hint-тэй, model-той, decorator-той. Бусад бүхэн түүнээс автоматаар үүссэн.
Бүлэг 1-ийн Хичээл 3-т би /docs-ыг таны лаборатори гэж нэрлэсэн. Тэр нь сургалтын турш үнэн байсан. Одоо тэр нь өөр зүйл болов: таны бүтээсэн бүтээгдэхүүний баримт бичиг.
Хэрэв та энэ API-г хэн нэгэнд өгвөл, тэр хүн /docs-ыг нээгээд бүх зүйлийг ойлгоно — ямар endpoint байна, юу илгээх ёстой, юу буцна, аль нь хамгаалагдсан. Тайлбар хэрэггүй.
Энэ бол мэргэжлийн программ хангамжийн шинж юм.
Хэсэг зогсоод, юу бүтээснээ жагсаая.
Гурван нөөц — ном, гишүүн, зээл. Хоорондоо foreign key-ээр холбогдсон.
Арван гурван endpoint — гурван router-т цэгцтэй тархсан.
Бүрэн CRUD — хоёр нөөцөд.
Бодит бизнесийн логик — зургаан дүрэм зээлэхэд, хоёр буцаахад, хамгаалалтын дүрмүүд устгахад.
Өгөгдлийн сан — SQLite, гурван хүснэгт, транзакцтай.
Хамгаалалт — API key, нээлттэй/хаалттай зөв ялгасан.
Pagination — гурван жагсаалтын endpoint, тоолол, шүүлттэй.
Алдааны боловсруулалт — тогтвортой бүтэц, монгол мессеж, дотоод мэдээлэл нууцалсан.
Тохиргоо — .env-д, кодоос салсан, бизнесийн дүрэм тохируулж болдог.
CORS — frontend залгах хаалга нээлттэй.
Автомат баримт бичиг — бүрэн, үнэн, кодтой үргэлж синхрон.
Найман файл, хоёр хавтас — цэгцтэй бүтэц, өсөх бэлтгэлтэй.
Энэ бол бодит программ хангамж юм. Сургалтын дасгал биш.
Курс 2-ын номын сангийн системийг санаарай. Тэр нь:
Терминал дээр ажилладаг байсан
Ганц хүн ашигладаг байсан
JSON файлд хадгалдаг байсан
Хамгаалалтгүй байсан
Зөвхөн таны компьютер дээр байсан
Одоогийн хувилбар:
Вэб API — ямар ч программ хандаж чадна
Олон хэрэглэгч — зэрэг ажиллана
Өгөгдлийн сан — транзакц, index, найдвартай байдал
Хамгаалалттай — API key
Баримтжуулагдсан — /docs
Интернэтэд гаргах боломжтой (deployment хийвэл)
Ижил санаа, огт өөр түвшин.
Энэ бол Курс 3-ын бүхэл зорилго байсан: Python мэддэг хүнийг backend хөгжүүлэгч болгох.
available_copies-ыг өөрчлөхгүй
python
loan = Loan(book_id=book.id, member_id=member.id)
session.add(loan)
session.commit() # book.available_copies -= 1 мартсанЗээл үүснэ, гэхдээ номын тоо буурахгүй. Хязгааргүй зээлдэх боломжтой болно.
Хажуугийн үр дагаврыг мартах нь бизнесийн логикийн хамгийн түгээмэл алдаа.
Хоёр өөрчлөлтийг тусад нь commit хийх
python
session.add(loan)
session.commit()
book.available_copies -= 1
session.commit() # хоёр дахь commitАжиллана, гэхдээ transaction эвдэрсэн. Хэрэв хоёр commit-ийн хооронд алдаа гарвал зээл үүсээд номын тоо буурахгүй.
Нэг үйлдлийн бүх өөрчлөлтийг нэг commit-д.
where дотор Python-ы is
python
.where(Loan.returned_at is None) # буруу
.where(Loan.returned_at == None) # зөвХоёр дахь удаа дурдаж байна, учир нь энэ нь үнэхээр төөрөгддөг. where дотор SQLModel-ийн дүрэм; гадна Python-ы дүрэм (is not None зөв).
Хязгаарт > ашиглах
python
if active_count > MAX_LOANS_PER_MEMBER: # буруу
if active_count >= MAX_LOANS_PER_MEMBER: # зөв> бол гишүүнд дөрөв дэх номыг өгнө (гурав нь хязгаараас их биш). >= зөв.
Давхардлыг буцаагдсан зээлтэй шалгах
python
select(Loan).where(
Loan.member_id == member.id,
Loan.book_id == book.id,
# returned_at шалгаагүй
)Тэгвэл гишүүн нэг удаа авч буцаасан номоо хэзээ ч дахин авч чадахгүй. returned_at == None заавал.
Нэгдүгээр өргөтгөл: буцаах хугацаа. Loan model-д due_date: date нэмээд, зээлэхэд date.today() + timedelta(days=14) тохируулна. Дараа нь GET /loans?overdue=true гэсэн шүүлт нэмж, хугацаа хэтэрсэн зээлийг харуулна. Санамж: model өөрчлөх бүрд .db файлыг устгана.
Хоёрдугаар өргөтгөл: гишүүний зээлийн түүх. GET /members/{id}/loans endpoint нэмж, тэр гишүүний бүх зээлийг буцаана. Санамж: members.py-д нэмэх бол Loan model import хэрэгтэй.
Гуравдугаар өргөтгөл: статистик. GET /stats endpoint нэмж, нийт ном, нийт гишүүн, идэвхтэй зээлийн тоо, хамгийн их зээлэгдсэн номыг буцаана.
Дөрөвдүгээр өргөтгөл: номын дэлгэрэнгүй. GET /books/{id} хариунд тухайн номыг одоо хэн зээлсэн байгааг нэмнэ (гишүүний нэрсийн жагсаалт).
Бизнесийн логик нь CRUD-аас өөр — өгөгдлийн бүтцээс биш, бодит ертөнцийн дүрмээс гардаг.
Дүрмээ кодоос өмнө тодорхойл — зургаан шалгалт зээлэхэд, хоёр буцаахад.
Guard clause загвар — эхэнд бүх шалгалт (raise), эцэст нь жинхэнэ ажил. Хавтгай, уншихад амархан.
Хажуугийн үр дагаврыг мартаж болохгүй — available_copies -= 1 зээлэхэд, += 1 буцаахад.
Нэг үйлдлийн бүх өөрчлөлтийг нэг commit-д — transaction нь бүгд болох, эсвэл юу ч болохгүйг баталгаажуулна.
Шинэ объектод add; session-аас авсан объектод add хэрэггүй.
func.count() нь дүрэм шалгахад ч ашигтай — бүх бичлэгийг татахгүйгээр тоолно.
Хоёр удаа буцаахаас хамгаалах нь заавал — эс бөгөөс байхгүй хувь бүртгэгдэнэ.
Status code-ууд: 404 (олдсонгүй), 400 (бизнесийн дүрэм), 409 (зөрчил/давхардал), 422 (төрөл).
Устгахын өмнө холбоотой өгөгдөл шалгах — өнчин бичлэгээс сэргийлнэ.
/docs нь одоо таны бүтээгдэхүүний баримт бичиг — та түүнийг бичээгүй, Python-оос автоматаар үүссэн.
Capstone дууслаа. Та бодит, ажиллагаатай, баримтжуулагдсан, хамгаалалттай, өгөгдлийн сантай вэб API бүтээв.
Бүлэг 15-т бид гурван зүйл хийнэ. Эхлээд deployment гэж юу болохыг ойлголтын түвшинд авч үзнэ — таны API-г интернэтэд гаргах нь юу гэсэн үг вэ. Дараа нь цаашдын замыг зурна: жинхэнэ хэрэглэгчийн нэвтрэлт, PostgreSQL, migration, тест, background task, frontend — тус бүр хаанаас эхлэхийг зааж өгнө. Эцэст нь бид гурван сургалтын аяныг дүгнэнэ: print("Сайн байна уу")-аас өгөгдлийн сантай вэб API хүртэлх зам.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.