Ачааллж байна...
Өмнөх хичээлд бид суурь тавьсан: model-ууд, өгөгдлийн сан, тохиргоо, dependency. Endpoint байхгүй байсан.
Энэ хичээлд бид арван endpoint бичнэ — Ном болон Гишүүний бүрэн CRUD. Хоёр router файл, pagination, хайлт, хамгаалалт.
Нэг чухал зүйл: хоёр router нь бараг ижил бүтэцтэй байна. Тэр нь санамсаргүй биш — CRUD бол давтагдах загвар. Ижил загварыг хоёр удаа бичих нь түүнийг бататгах хамгийн сайн арга юм. Хоёр дахь удаад та түүнийг цээжээр мэдэж эхэлнэ.
Эхлээд хоёрын ялгааг тодруулъя, учир нь энэ нь дизайны шийдвэр юм.
Ном — олон нийтийн мэдээлэл. Хэн ч номын санд ямар ном байгааг харж болно. Тиймээс унших нээлттэй, бичих хаалттай.
Гишүүн — хувийн мэдээлэл. Нэр, имэйл. Хэн ч бусад гишүүдийн жагсаалтыг харах ёсгүй. Тиймээс бүх үйлдэл хаалттай.
Энэ бол бодит шийдвэр. Хэрэв та бүх зүйлийг ижилхэн хамгаалсан бол — эсвэл хэт хаалттай (ном хайх боломжгүй), эсвэл хэт нээлттэй (хувийн мэдээлэл ил) болно.
Дараагийн хичээлүүдэд та энэ ялгааг кодонд харна.
app/routers хавтаст books.py файл үүсгэнэ:
python
# app/routers/books.py
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, func, select
from app.database import get_session
from app.dependencies import list_params, verify_api_key
from app.models import (
Book,
BookCreate,
BookList,
BookPublic,
ListParams,
)
router = APIRouter(prefix="/books", tags=["Ном"])
def find_book(book_id: int, session: Session) -> Book:
book = session.get(Book, book_id)
if book is None:
raise HTTPException(status_code=404, detail="Ном олдсонгүй")
return book
@router.get("", response_model=BookList)
def list_books(
params: ListParams = Depends(list_params),
session: Session = Depends(get_session),
):
filters = []
if params.search:
filters.append(Book.title.contains(params.search))
count_statement = select(func.count()).select_from(Book)
for condition in filters:
count_statement = count_statement.where(condition)
total = session.exec(count_statement).one()
statement = select(Book)
for condition in filters:
statement = statement.where(condition)
statement = statement.offset(params.skip).limit(params.limit)
items = session.exec(statement).all()
return BookList(
items=items,
total=total,
skip=params.skip,
limit=params.limit,
has_more=params.skip + len(items) < total,
)
@router.get("/{book_id}", response_model=BookPublic)
def get_book(book_id: int, session: Session = Depends(get_session)):
return find_book(book_id, session)
@router.post(
"",
status_code=201,
response_model=BookPublic,
dependencies=[Depends(verify_api_key)],
)
def create_book(book: BookCreate, session: Session = Depends(get_session)):
db_book = Book.model_validate(book)
db_book.available_copies = book.total_copies
session.add(db_book)
session.commit()
session.refresh(db_book)
return db_book
@router.put(
"/{book_id}",
response_model=BookPublic,
dependencies=[Depends(verify_api_key)],
)
def update_book(
book_id: int,
book: BookCreate,
session: Session = Depends(get_session),
):
db_book = find_book(book_id, session)
borrowed = db_book.total_copies - db_book.available_copies
if book.total_copies < borrowed:
raise HTTPException(
status_code=400,
detail=f"Одоогоор {borrowed} хувь зээлэгдсэн байна. "
f"Нийт хувийг {borrowed}-аас бага болгож болохгүй.",
)
db_book.title = book.title
db_book.author = book.author
db_book.year = book.year
db_book.isbn = book.isbn
db_book.total_copies = book.total_copies
db_book.available_copies = book.total_copies - borrowed
session.commit()
session.refresh(db_book)
return db_book
@router.delete(
"/{book_id}",
status_code=204,
dependencies=[Depends(verify_api_key)],
)
def delete_book(book_id: int, session: Session = Depends(get_session)):
db_book = find_book(book_id, session)
if db_book.available_copies < db_book.total_copies:
raise HTTPException(
status_code=400,
detail="Зээлэгдсэн хувьтай номыг устгах боломжгүй",
)
session.delete(db_book)
session.commit()
return NoneХадгална.
find_book — танил загвар
python
def find_book(book_id: int, session: Session) -> Book:
book = session.get(Book, book_id)
if book is None:
raise HTTPException(status_code=404, detail="Ном олдсонгүй")
return bookТэмдэглэлийн API v3-ийн find_note-тэй яг ижил. Ол, олдохгүй бол 404.
Dependency биш — ердийн туслах функц, session-ыг параметрээр авдаг.
list_books — pagination
Бүлэг 13-ын Хичээл 3-ын загвар яг хэвээр: filters жагсаалт, хоёр асуулт (тоолол болон өгөгдөл), боодолтой хариу.
Ганц шүүлт — гарчгаар хайх. Хүсвэл зохиолчоор хайх нэмж болно (сонголтот өргөтгөл).
create_book — сонирхолтой мөр
python
db_book = Book.model_validate(book)
db_book.available_copies = book.total_copiesЭхний мөр танил: BookCreate -> Book хөрвүүлэлт.
Хоёр дахь мөр нь шинэ. Яагаад хэрэгтэй вэ?
BookCreate model-д available_copies байхгүй (өмнөх хичээлд ярьсанчлан — хэрэглэгч түүнийг тохируулах ёсгүй). Тиймээс model_validate түүнийг тохируулж чадахгүй; default (1) хэвээр үлдэнэ.
Гэтэл шинэ ном нэмэхэд бүх хувь боломжтой байх ёстой. Тэмдэглэл нэмэхэд гурван хувь оруулбал гурвуулаа зээлдэх боломжтой.
Тиймээс бид түүнийг гараар тохируулж байна: available_copies = total_copies.
Энэ бол бизнесийн логик — өгөгдлийн бүтцээс биш, номын сангийн дүрмээс гарч байна.
update_book — хамгийн нарийн endpoint
Энэ бол энэ файлын хамгийн сонирхолтой хэсэг. Сайтар харцгаая.
python
borrowed = db_book.total_copies - db_book.available_copiesХэдэн хувь одоогоор зээлэгдсэн бэ? Нийт хувиас боломжтой хувийг хасна.
Жишээ: нийт 5, боломжтой 2 -> 3 зээлэгдсэн.
python
if book.total_copies < borrowed:
raise HTTPException(
status_code=400,
detail=f"Одоогоор {borrowed} хувь зээлэгдсэн байна. ...",
)Дүрэм: нийт хувийг зээлэгдсэн хувиас бага болгож болохгүй.
Яагаад? Жишээ: нийт 5, гурав зээлэгдсэн. Хэрэв админ "нийт 2 хувь" гэж засвал — гурав зээлэгдсэн байхад нийт хоёр хувьтай болно. Утгагүй. Боломжтой хувь сөрөг болно.
Тиймээс бид түүнийг 400 алдаагаар зогсоож байна. Бүлэг 3-ын Хичээл 3-т сурсан ялгаа: 422 бол төрлийн алдаа, 400 бол утгын алдаа. total_copies=2 нь хүчинтэй бүхэл тоо (422 биш), гэхдээ бизнесийн логикийн хувьд буруу (400).
python
db_book.available_copies = book.total_copies - borrowedБоломжтой хувийг дахин тооцоолж байна.
Жишээ: нийт 5 -> 8 болгож нэмэгдүүлэв, гурав зээлэгдсэн. Шинэ боломжтой = 8 - 3 = 5. Зөв.
Хэрэв бид зүгээр л available_copies = total_copies гэж бичсэн бол — зээлэгдсэн гурав "мартагдаж", боломжтой 8 болох байсан. Тэгвэл номын сан байхгүй ном зээлдүүлэх болно.
Энэ бол бодит программ хангамжийн нарийн ширийн зүйл. Хялбар харагдах endpoint дотор ийм тооцоолол нуугдаж байдаг. Тэдгээрийг мартвал өгөгдөл аажмаар алдагдаж, хожим "яагаад тоо таарахгүй байна вэ?" гэсэн асуулт гарна.
delete_book — хамгаалалтын дүрэм
python
if db_book.available_copies < db_book.total_copies:
raise HTTPException(
status_code=400,
detail="Зээлэгдсэн хувьтай номыг устгах боломжгүй",
)Дүрэм: зээлэгдсэн ном устгаж болохгүй.
Яагаад? Учир нь loan хүснэгтэд тэр номыг заасан бичлэгүүд байна. Хэрэв номыг устгавал тэдгээр зээл өнчин болно — байхгүй ном руу заана.
Foreign key нь үнэндээ үүнээс сэргийлдэг (өгөгдлийн сан татгалзана), гэхдээ тэр үед IntegrityError гарч, 500 алдаа болно. Хэрэглэгч "сервер эвдэрлээ" гэж ойлгоно.
Бид түүнийг урьдчилан барьж, 400 болон тодорхой мессеж өгч байна. Хэрэглэгч яагаад болохгүйг ойлгоно.
Энэ бол сайн API-ийн шинж: алдааг эрт барьж, ойлгомжтой тайлбарла.
app/routers хавтаст members.py:
python
# app/routers/members.py
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, func, select
from app.database import get_session
from app.dependencies import list_params, verify_api_key
from app.models import (
ListParams,
Loan,
Member,
MemberCreate,
MemberList,
MemberPublic,
)
router = APIRouter(
prefix="/members",
tags=["Гишүүн"],
dependencies=[Depends(verify_api_key)],
)
def find_member(member_id: int, session: Session) -> Member:
member = session.get(Member, member_id)
if member is None:
raise HTTPException(status_code=404, detail="Гишүүн олдсонгүй")
return member
@router.get("", response_model=MemberList)
def list_members(
params: ListParams = Depends(list_params),
session: Session = Depends(get_session),
):
filters = []
if params.search:
filters.append(Member.name.contains(params.search))
count_statement = select(func.count()).select_from(Member)
for condition in filters:
count_statement = count_statement.where(condition)
total = session.exec(count_statement).one()
statement = select(Member)
for condition in filters:
statement = statement.where(condition)
statement = statement.offset(params.skip).limit(params.limit)
items = session.exec(statement).all()
return MemberList(
items=items,
total=total,
skip=params.skip,
limit=params.limit,
has_more=params.skip + len(items) < total,
)
@router.get("/{member_id}", response_model=MemberPublic)
def get_member(member_id: int, session: Session = Depends(get_session)):
return find_member(member_id, session)
@router.post("", status_code=201, response_model=MemberPublic)
def create_member(member: MemberCreate, session: Session = Depends(get_session)):
existing = session.exec(
select(Member).where(Member.email == member.email)
).first()
if existing is not None:
raise HTTPException(
status_code=409,
detail="Энэ имэйлтэй гишүүн аль хэдийн бүртгэгдсэн байна",
)
db_member = Member.model_validate(member)
session.add(db_member)
session.commit()
session.refresh(db_member)
return db_member
@router.put("/{member_id}", response_model=MemberPublic)
def update_member(
member_id: int,
member: MemberCreate,
session: Session = Depends(get_session),
):
db_member = find_member(member_id, session)
db_member.name = member.name
db_member.email = member.email
db_member.is_active = member.is_active
session.commit()
session.refresh(db_member)
return db_member
@router.delete("/{member_id}", status_code=204)
def delete_member(member_id: int, session: Session = Depends(get_session)):
db_member = find_member(member_id, session)
active_loans = session.exec(
select(Loan).where(
Loan.member_id == member_id,
Loan.returned_at == None,
)
).all()
if active_loans:
raise HTTPException(
status_code=400,
detail=f"Гишүүнд {len(active_loans)} буцаагаагүй ном байна",
)
session.delete(db_member)
session.commit()
return NoneХадгална.
Router түвшний хамгаалалт
python
router = APIRouter(
prefix="/members",
tags=["Гишүүн"],
dependencies=[Depends(verify_api_key)],
)dependencies нь router дээр байна, endpoint дээр биш.
Бүлэг 8-ын Хичээл 3-т дурдсан загвар: router бүхэлдээ хамгаалагдана. Бүх endpoint автоматаар API key шаардана.
Endpoint-үүдийг хараарай — тэдгээрт dependencies=[...] байхгүй. Router дээр нэг удаа бичсэн тул хангалттай.
Энэ бол номын router-ээс ялгаатай. Тэнд зарим endpoint нээлттэй тул тус бүрд нь тодорхой бичих ёстой байсан. Энд бүгд хаалттай тул router дээр нэг удаа.
Ашиг: ирээдүйд шинэ endpoint нэмэхэд автоматаар хамгаалагдана. Хамгаалалт нэмэхээ мартах эрсдэлгүй.
Давхардал шалгах — 409
python
existing = session.exec(
select(Member).where(Member.email == member.email)
).first()
if existing is not None:
raise HTTPException(
status_code=409,
detail="Энэ имэйлтэй гишүүн аль хэдийн бүртгэгдсэн байна",
)Шинэ status code: 409 (Conflict).
Энэ нь "таны хүсэлт зөв, гэхдээ одоогийн байдалтай зөрчилдөж байна" гэсэн утгатай. Давхардсан бүртгэлд яг тохирно.
Яагаад 400 биш вэ? 400 нь "хүсэлт буруу"; 409 нь "хүсэлт зөв, гэхдээ боломжгүй". Ялгаа нарийн, гэхдээ 409 нь илүү тодорхой.
.first() — Бүлэг 11-ийн Хичээл 3-аас. Эхний тохирлыг буцаана, байхгүй бол None.
Тэмдэглэл: энэ шалгалт нь өгөгдлийн сангийн unique хязгаарлалтаас өөр. SQLModel-д Field(unique=True) гэж зарлаж болно, тэгвэл өгөгдлийн сан өөрөө татгалзана. Гэхдээ тэр үед IntegrityError -> 500 гарна. Гараар шалгах нь илүү ойлгомжтой мессеж өгдөг.
Бодит системд хоёуланг хийдэг: гараар шалгаж сайхан мессеж өгөх, өгөгдлийн санд хязгаарлалт тавьж баталгаа болгох.
Идэвхтэй зээл шалгах
python
active_loans = session.exec(
select(Loan).where(
Loan.member_id == member_id,
Loan.returned_at == None,
)
).all()Гишүүнийг устгахаас өмнө буцаагаагүй ном байгаа эсэхийг шалгаж байна.
Loan.returned_at == None — энд нэг онцлог байна. Python-д ихэвчлэн is None гэж бичдэг, == None биш. Гэхдээ SQLModel-ийн where дотор == None заавал — учир нь SQLModel нь харьцуулалтыг SQL болгож хувиргадаг (IS NULL), Python-ы is түүнийг ойлгодоггүй.
Бүлэг 11-ийн Хичээл 3-т ярьсан "not ажиллахгүй" онцлогийн бас нэг илрэл. where дотор SQLModel-ийн дүрэм үйлчилнэ, Python-ынх биш.
Хоёр нөхцөл таслалаар — SQL-д AND болно.
if active_loans: — хоосон биш жагсаалт нь Python-д үнэн. Бүлэг 1-ээс танил.
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, members
tags_metadata = [
{"name": "Ном", "description": "Номын бүртгэл, хайлт, засвар."},
{"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.on_event("startup")
def on_startup():
create_db_and_tables()
@app.get("/")
def root():
return {
"name": APP_NAME,
"version": APP_VERSION,
"docs": "/docs",
}Хадгална.
tags_metadata — Бүлэг 7-ын Хичээл 3-аас. Bag бүрд тайлбар. Гишүүний tag-д "API key шаардлагатай" гэж бичсэн нь хэрэглэгчид тустай.
Хоёр include_router.
Сервер ажиллуулна:
fastapi dev main.py1. /docs — бүтэц
http://127.0.0.1:8000/docsОдоо арван нэг endpoint харагдана, хоёр бүлэгт:
Ном
GET /books
POST /books
GET /books/{book_id}
PUT /books/{book_id}
DELETE /books/{book_id}
Гишүүн
GET /members
POST /members
GET /members/{member_id}
PUT /members/{member_id}
DELETE /members/{member_id}Bag бүрийн дор тайлбар.
Ялгааг анзаараарай: GET /books дээр товшвол x-api-key талбар байхгүй (нээлттэй). GET /members дээр товшвол байна (хаалттай).
2. Ном нэмэх
/docs -> POST /books -> Try it out. x-api-key-д номын-сангийн-нууц-түлхүүр, body:
json
{
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962,
"isbn": "978-99929-0-123-4",
"total_copies": 3
}Хариу (статус 201):
json
{
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962,
"isbn": "978-99929-0-123-4",
"total_copies": 3,
"id": 1,
"available_copies": 3
}available_copies: 3 — та илгээгээгүй, сервер total_copies-оос тохируулсан.
Дахин хоёр ном нэмнэ:
json
{"title": "Цагаан хэрэм", "author": "С. Эрдэнэ", "year": 1971, "total_copies": 2}json
{"title": "Тунгалаг тамир", "author": "Ч. Лодойдамба", "year": 1962, "total_copies": 5}3. Номын жагсаалт — түлхүүргүй
Browser дээр (түлхүүр огт хэрэггүй):
http://127.0.0.1:8000/booksХариу:
json
{
"items": [
{"title":"Цог хийморь","author":"Ч. Лодойдамба","year":1962,"isbn":"978-99929-0-123-4","total_copies":3,"id":1,"available_copies":3},
{"title":"Цагаан хэрэм","author":"С. Эрдэнэ","year":1971,"isbn":"","total_copies":2,"id":2,"available_copies":2},
{"title":"Тунгалаг тамир","author":"Ч. Лодойдамба","year":1962,"isbn":"","total_copies":5,"id":3,"available_copies":5}
],
"total": 3,
"skip": 0,
"limit": 10,
"has_more": false
}Түлхүүргүй ажиллалаа — номын жагсаалт нээлттэй.
4. Хайлт ба pagination
http://127.0.0.1:8000/books?search=ЦагНэг ном (Цагаан хэрэм), total: 1.
http://127.0.0.1:8000/books?limit=2Хоёр ном, total: 3, has_more: true.
http://127.0.0.1:8000/books?skip=2&limit=2Нэг ном, has_more: false.
5. Гишүүн нэмэх
/docs -> POST /members, түлхүүр зөв, body:
json
{
"name": "Дулгуун",
"email": "dulguun@example.com"
}Хариу (статус 201):
json
{
"name": "Дулгуун",
"email": "dulguun@example.com",
"is_active": true,
"id": 1,
"joined_at": "2026-07-16"
}joined_at — та илгээгээгүй, default_factory=date.today тохируулсан. Огноо нь ISO форматын текст болж хувирсан.
Дахин хоёр гишүүн нэмнэ:
json
{"name": "Бат", "email": "bat@example.com"}json
{"name": "Сараа", "email": "saraa@example.com"}6. Давхардсан имэйл — 409
Дахин ижил имэйлээр:
json
{"name": "Өөр хүн", "email": "dulguun@example.com"}Хариу (статус 409):
json
{
"error": true,
"status": 409,
"message": "Энэ имэйлтэй гишүүн аль хэдийн бүртгэгдсэн байна",
"path": "/members"
}Таны алдааны handler ажиллаж, тогтвортой бүтэцтэй хариу өгч байна.
7. Гишүүдийн жагсаалт — түлхүүргүй
Browser дээр:
http://127.0.0.1:8000/membersХариу (статус 422):
json
{
"error": true,
"status": 422,
"message": "'x-api-key' талбар буруу байна",
"path": "/members",
"details": [...]
}Хаалттай. Хувийн мэдээлэл хамгаалагдсан.
/docs-оор түлхүүртэй туршвал жагсаалт ирнэ.
8. Номын нийт хувийг засах
/docs -> PUT /books/{book_id}. Path 1, түлхүүр зөв, body:
json
{
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962,
"isbn": "978-99929-0-123-4",
"total_copies": 5
}Хариу (статус 200):
json
{
...,
"total_copies": 5,
"available_copies": 5
}Гурваас тав болов, зээлэгдсэн нь тэг тул боломжтой нь ч тав.
(Зээлэгдсэн номтой үед энэ логик хэрхэн ажиллахыг Хичээл 3-т харна.)
9. Ном устгах
/docs -> DELETE /books/{book_id}. Path 3, түлхүүр зөв.
Хариу (статус 204): хоосон.
http://127.0.0.1:8000/booksХоёр ном үлдсэн.
10. 404 шалгах
http://127.0.0.1:8000/books/999Хариу (статус 404):
json
{
"error": true,
"status": 404,
"message": "Ном олдсонгүй",
"path": "/books/999"
}Одоо нэг зүйлийг ажиглая. Хоёр файлыг зэрэгцүүлж харвал:
books.py | members.py | |
|---|---|---|
|
|
|
Жагсаалт |
|
|
Нэг |
|
|
Үүсгэх |
|
|
Засах |
|
|
Устгах |
|
|
Бүтэц нь яг ижил. Ялгаа нь зөвхөн: model-ын нэр, хамгаалалтын байрлал, бизнесийн дүрмүүд.
Энэ бол CRUD-ийн мөн чанар. Та энэ загварыг нэг удаа эзэмшсэн бол ямар ч нөөцөд хэрэглэнэ: бүтээгдэхүүн, захиалга, хэрэглэгч, тэмдэглэл, ном.
Бүлэг 5-ын Хичээл 1-т би "CRUD бол бараг бүх API-ийн үндэс" гэж хэлсэн. Одоо та түүнийг гурав дахь удаагаа (тэмдэглэл, ном, гишүүн) бичээд, тэр үнэнийг гар дээрээ мэдэрч байна.
Давталтыг арилгаж болох уу
Шударга асуулт: хоёр файл ижил бол давхардлыг арилгаж болох уу?
Болно — "generic CRUD" гэсэн дэвшилтэт загвар байдаг. Нэг ерөнхий функц бичээд model-ыг параметр болгон дамжуулна.
Гэхдээ бид тэгэхгүй. Хоёр шалтгаанаар.
Нэгд, тодорхой байдал. Ижил кодыг хоёр удаа бичих нь уншихад амархан. Ерөнхий код нь богино боловч ойлгоход хэцүү.
Хоёрт, ялгаа гарч ирдэг. Одоо ижил харагдаж байгаа хоёр router нь маргааш өөр болно — ном нь ISBN шалгалт авна, гишүүн нь имэйл баталгаажуулалт авна. Ерөнхий код тэр үед задардаг.
Бодит төслүүдэд байнга хэлэгддэг зөвлөгөө: гурав дахь удаагаа давтахаас өмнө ерөнхийлж бүү эхэл. Хоёр давталт нь тэвчихийн аргатай; гурав дахь нь тэвчихийн аргагүй бол тэр үед ерөнхийл.
available_copies-ыг тохируулахгүй
python
db_book = Book.model_validate(book)
session.add(db_book) # available_copies тохируулаагүйНом нэмэхэд available_copies нь default (1) хэвээр үлдэнэ — хэдэн хувь оруулсан ч. Тэгвэл 5 хувьтай номноос зөвхөн нэг зээлдэх боломжтой болно.
Засвар: db_book.available_copies = book.total_copies.
where дотор is None
python
select(Loan).where(Loan.returned_at is None) # буруу
select(Loan).where(Loan.returned_at == None) # зөвPython-ы is нь SQLModel-ийн нөхцөлийн объектыг ойлгодоггүй. where дотор == None.
Router-ийн dependency-г endpoint дээр давтах
python
router = APIRouter(dependencies=[Depends(verify_api_key)])
@router.post("", dependencies=[Depends(verify_api_key)]) # давхардалАжиллана, гэхдээ илүүц — dependency хоёр удаа ажиллана. Router дээр байвал endpoint дээр хэрэггүй.
Model-уудыг буруу import хийх
python
from app.models import Book, BookCreate # BookList, BookPublic дутууАшиглаж байгаа бүх model-ыг import хийх ёстой. Дутуу бол NameError.
total_copies-ыг шалгахгүй засах
python
db_book.total_copies = book.total_copies
db_book.available_copies = book.total_copies # borrowed тооцоогүйЗээлэгдсэн хувь "мартагдана". Номын сан байхгүй ном зээлдүүлнэ.
Заавал: available_copies = total_copies - borrowed.
Нэгдүгээр өргөтгөл: зохиолчоор хайх. list_books-д нэмэлт шүүлт:
python
if params.search:
filters.append(Book.title.contains(params.search))Үүнийг гарчиг эсвэл зохиолчоор хайхаар өөрчилж болно. Санамж: or_ функц хэрэгтэй — from sqlmodel import or_.
Хоёрдугаар өргөтгөл: идэвхтэй гишүүдээр шүүх. ListParams-д is_active: bool | None = None нэмээд, members.py-д шүүлт нэмнэ.
Гуравдугаар өргөтгөл: хуудасны дугаар. Бүлэг 13-ын page болон total_pages талбарыг гурван *List model-д нэмнэ.
Арван endpoint, хоёр router — ном (хэсэгчлэн нээлттэй), гишүүн (бүрэн хаалттай).
Хамгаалалтын шийдвэр: олон нийтийн мэдээлэл нээлттэй, хувийн мэдээлэл хаалттай.
Номын router: endpoint тус бүрд dependencies=[...]. Гишүүний router: router дээр нэг удаа — бүх endpoint автоматаар хамгаалагдана.
create_book-д available_copies = total_copies гараар тохируулна — BookCreate-д тэр талбар байхгүй.
update_book-д borrowed тооцоолол заавал — зээлэгдсэн хувийг мартвал өгөгдөл алдагдана.
400 — бизнесийн логикийн алдаа (422-оос ялгаатай: тэр нь төрлийн алдаа).
409 (Conflict) — давхардсан бүртгэл. "Хүсэлт зөв, гэхдээ боломжгүй."
Устгахын өмнө холбоотой өгөгдөл шалгана — зээлэгдсэн ном, буцаагаагүй номтой гишүүн.
where дотор == None, is None биш — SQLModel-ийн дүрэм.
Хоёр router бараг ижил бүтэцтэй — CRUD бол давтагдах загвар. Гурав дахь давталтаас өмнө ерөнхийлж бүү эхэл.
Арван endpoint бэлэн боллоо. Таны API одоо ном болон гишүүнийг бүрэн удирдаж чадна.
Гэхдээ номын сан ажиллахгүй байна. Ном байна, гишүүн байна — гэхдээ хэн ч ном авч чадахгүй. Зээлийн систем дутуу.
Дараагийн хичээл бол capstone-ийн оргил: зээлэх, буцаах логик. Тэнд бид бодит дүрмүүдийг хэрэгжүүлнэ — боломжтой хувь байна уу, гишүүн идэвхтэй юу, хязгаараа хэтрүүлсэн үү, аль хэдийн буцаасан уу. Тэдгээр нь зөвхөн CRUD биш, харин жинхэнэ бизнесийн логик бөгөөд бодит программ хангамжийн зүрх юм. Дараа нь бид бүтэн API-гаараа аялж, гурван сургалтын аяны оргилыг тэмдэглэнэ.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.