Ачааллж байна...
Өмнөх хичээлд бид POST яагаад оршдог, GET-ээс юугаараа ялгаатайг ойлгосон. Body-г хэд хэдэн удаа ашигласан ч гүехэн харсан. Энэ хичээлд бид түүнийг гүнзгий эзэмшинэ.
Бид Pydantic model-ыг параметр болгон ашиглах бүрэн механизмыг судална: FastAPI хүлээж авсан JSON body-г хэрхэн задалж, шалгаж, объект болгодог. Дараа нь echo-style биш жинхэнэ үүсгэх endpoint бичнэ — хэрэглэгчийн өгсөн өгөгдлийг хадгалж, серверийн зүгээс id, огноо зэрэг нэмэлт мэдээлэл нэмж, бүрэн бүтээгдэхүүн буцаах endpoint.
Энэ бол Түвшин 2-ын бүтээн байгуулалт руу хийх шууд бэлтгэл юм.
Бүлэг 4-т та book: Book гэж бичихэд ямар ид шид болсныг харсан. Одоо тэр ид шидийг задалж, алхам алхмаар ойлгоё.
Хэрэглэгч POST хүсэлт илгээхэд түүний body-д JSON текст явдаг. Тэр текст сүлжээгээр дамжиж таны серверт ирнэ. Тэр мөчид тэр нь зүгээр л текст — бүтэцгүй, шалгагдаагүй тэмдэгтүүдийн урсгал:
{"title":"Цог хийморь","author":"Ч. Лодойдамба","year":1962}FastAPI таны функцийн параметрийг хардаг: book: Book. Type hint нь Pydantic model. Энэ мөчид FastAPI дараах алхмуудыг хийдэг.
Алхам 1: JSON текстийг задлах. Текстийг Python объект (dictionary) болгож хувиргана. Курс 2-ын Бүлэг 6-ын json.loads()-тэй ижил ажил — гэхдээ FastAPI өөрөө хийнэ.
Алхам 2: Pydantic-д дамжуулах. Тэр dictionary-г Book model-руу өгнө: "үүнээс Book объект үүсгэж чадах уу?"
Алхам 3: Pydantic шалгах. Bүх талбар зөв төрөлтэй эсэх, заавал талбар бүгд байгаа эсэхийг шалгана. Буруу бол ValidationError шидэж, FastAPI түүнийг 422 болгоно — функц огт дуудагдахгүй.
Алхам 4: Объект дамжуулах. Бүх зүйл зөв бол Book объект үүсэж, таны функцэд book нэрээр дамжина.
Таны функц ажиллаж эхлэх үед book нь бүрэн бэлэн, шалгагдсан Python объект. Та book.title, book.year гэж хандаж, юу ч шалгалгүйгээр ашиглана.
Энэ дөрвөн алхмыг та гараар хийвэл олон арван мөр код болно. FastAPI + Pydantic хамтдаа түүнийг ганц type hint-ээр хийж өгч байна.
Нэг чухал зүйлийг тодруулъя. book нь dictionary биш — Pydantic объект. Ялгааг харъя.
python
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="Номын API")
class Book(BaseModel):
title: str
author: str
year: int
@app.post("/books", status_code=201)
def create_book(book: Book):
# book бол Pydantic объект — цэгээр хандана
return {
"message": f"'{book.title}' ном нэмэгдлээ",
"title": book.title,
"author": book.author,
"year": book.year,
"century": (book.year // 100) + 1,
}Хадгална.
Кодын задаргаа
book.title, book.author, book.year — Pydantic объект тул цэгээр хандаж байна. Хэрэв энэ dictionary байсан бол book["title"] гэж хандах байсан. Гэхдээ Pydantic model нь class учир цэг ашиглана — Курс 2-ын класс, dataclass-тай адил.
book.year // 100 + 1 — жинхэнэ логик. Оноос зууныг тооцоолж байна (1962 -> 20-р зуун). Курс 1-ийн бүхэл хуваалт (//). Энэ нь чухал санааг харуулж байна: book дотор өгөгдөл нь зөв төрөлтэй тул та түүн дээр шууд тооцоолол хийж чадна. book.year нь баталгаатай тоо — текст биш — тул // 100 ажиллана. Хэрэв Pydantic шалгаагүй байсан бол book.year нь "1962 он" байж магадгүй, тэгвэл // 100 унах байсан.
Туршина
/docs -> POST /books -> Try it out:
json
{
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962
}Server response (статус 201):
json
{
"message": "'Цог хийморь' ном нэмэгдлээ",
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962,
"century": 20
}century талбар нь 20 — таны функц book.year-ээс тооцоолсон. Хэрэглэгч илгээгээгүй, сервер үүсгэсэн. Энэ нь чухал санааг харуулж байна: хариу нь оролттой яг ижил байх албагүй. Сервер өөрийн мэдлэгийг нэмж болно.
Одоог хүртэл бидний ихэнх POST endpoint зүгээр л хүлээж авсан өгөгдлөө буцааж байсан (echo). Бодит үүсгэх endpoint үүнээс илүү зүйл хийдэг. Гурван алхам байдаг.
Нэгд, серверийн мэдлэг нэмэх. id, үүсгэсэн огноо гэх мэт хэрэглэгч мэдэхгүй, зөвхөн сервер мэддэг мэдээлэл.
Хоёрт, хадгалах. Өгөгдлийг жагсаалт (эсвэл Түвшин 4-т өгөгдлийн сан)-д хийх.
Гуравт, үр дүнг буцаах. Хэрэглэгчид "энэ бол таны үүсгэсэн зүйл, ийм id-тай боллоо" гэж хэлэх.
Эдгээрийг нэг нэгээр нь хийе.
main.py файлаа дараах кодоор солино:
python
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="Номын API")
books = []
next_id = 1
class Book(BaseModel):
title: str
author: str
year: int
description: str | None = None
@app.get("/books")
def get_books():
return books
@app.post("/books", status_code=201)
def create_book(book: Book):
global next_id
new_book = book.model_dump()
new_book["id"] = next_id
books.append(new_book)
next_id += 1
return new_bookХадгална.
Кодын задаргаа
books = [] болон next_id = 1 — жагсаалт болон дараагийн id-г хянадаг тоолуур. Хоёулаа файлын дээд түвшинд, функцээс гадуур.
book.model_dump() — энэ бол гол мөр. Бүлэг 4-т сурсанчлан Pydantic объектыг Python dictionary болгож байна. Яагаад? Учир нь бид id талбар нэмэх гэж байгаа, гэхдээ Book model-д id талбар байхгүй. Model-ыг dictionary болгосны дараа шинэ түлхүүр нэмэхэд хялбар.
Дараалалыг ажиглаарай: book (Pydantic объект, зөвхөн хэрэглэгчийн талбарууд) -> new_book (dictionary, id нэмсэн). Хоёр өөр зүйл. book бол оролт, new_book бол хадгалагдах бүрэн бичлэг.
new_book["id"] = next_id — dictionary-д id нэмлээ.
books.append(new_book) — жагсаалтад хадгаллаа.
next_id += 1 — дараагийн номд зориулж тоолуурыг нэмэгдүүллээ.
global next_id — Курс 2-ын Бүлэг 3-аас. Функцийн гаднах next_id-г өөрчилж байгаа тул global зарлах ёстой. Хэрэв энэ мөр байхгүй бол Python next_id-г функцийн дотоод шинэ хувьсагч гэж үзэж, next_id += 1 дээр алдаа өгнө.
Яагаад id серверээс үүсдэг вэ
Нэг чухал зарчмыг тодруулъя. id-г хэрэглэгч биш, сервер үүсгэдэг.
Яагаад? Учир нь id нь давтагдашгүй байх ёстой. Хэрэв хэрэглэгч бүр өөрийн id сонгодог байсан бол хоёр хэрэглэгч ижил id сонгож, зөрчил үүсгэх байсан. Сервер төвлөрсөн байдлаар id өгснөөр давхардал гарахгүй.
Тиймээс Book model-д id талбар байхгүй — хэрэглэгч id илгээх ёсгүй. Хэрэглэгч зөвхөн title, author, year, description илгээнэ. id-г сервер нэмнэ. Энэ бол API зохион бүтээхэд чухал загвар: хэрэглэгч юу өгөх ёстой, сервер юу нэмэх ёстой гэдгийг ялгах.
Туршина
/docs -> POST /books -> Try it out. Хоёр ном дараалан нэмнэ.
Эхнийх:
json
{
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962
}Server response (статус 201):
json
{
"title": "Цог хийморь",
"author": "Ч. Лодойдамба",
"year": 1962,
"description": null,
"id": 1
}id: 1 нэмэгдсэн — хэрэглэгч илгээгээгүй, сервер өгсөн. description: null — хэрэглэгч өгөөгүй тул default.
Хоёр дахь:
json
{
"title": "Цагаан хэрэм",
"author": "С. Эрдэнэ",
"year": 1971,
"description": "Сонгодог роман"
}Server response (статус 201):
json
{
"title": "Цагаан хэрэм",
"author": "С. Эрдэнэ",
"year": 1971,
"description": "Сонгодог роман",
"id": 2
}id: 2 — тоолуур нэмэгдсэн.
GET-ээр батлах
http://127.0.0.1:8000/booksХариу:
json
[
{"title":"Цог хийморь","author":"Ч. Лодойдамба","year":1962,"description":null,"id":1},
{"title":"Цагаан хэрэм","author":"С. Эрдэнэ","year":1971,"description":"Сонгодог роман","id":2}
]Хоёр ном хадгалагдсан, тус бүр id-тай. POST үүсгэсэн, GET уншиж байна. CRUD-ийн эхний хоёр үсэг ажиллаж байна.
Заримдаа танд body болон path parameter хамт хэрэгтэй болно. Жишээ нь тодорхой зохиолчид ном нэмэх:
python
# main.py (нэмэлт)
@app.post("/authors/{author_name}/books", status_code=201)
def add_book_to_author(author_name: str, book: Book):
global next_id
new_book = book.model_dump()
new_book["id"] = next_id
new_book["author"] = author_name # path-аас авсан нэрээр дарж бичих
books.append(new_book)
next_id += 1
return new_bookЭнд хоёр эх сурвалж нэгдэж байна. author_name нь path-аас ({author_name}). book нь body-оос (Pydantic model). FastAPI хоёрыг автоматаар ялгадаг — Бүлэг 4-т сурсан дүрэм: нэр замын дөрвөлжин хаалтад байвал path, Pydantic model бол body.
Туршина
/docs -> POST /authors/{author_name}/books. Path талбарт Д. Нацагдорж, body-д:
json
{
"title": "Учиртай гурван толгой",
"author": "тоохгүй",
"year": 1934
}Server response (статус 201):
json
{
"title": "Учиртай гурван толгой",
"author": "Д. Нацагдорж",
"year": 1934,
"description": null,
"id": 3
}author талбар нь "тоохгүй" биш "Д. Нацагдорж" болсон — path-аас ирсэн нэр body доторхийг дарж бичсэн (new_book["author"] = author_name). Энэ нь харуулж байна: та body болон path-ийн өгөгдлийг чөлөөтэй хослуулж, аль нэгийг нь давуу болгож болно.
Body нь ганц объект байх албагүй — жагсаалт ч байж болно. Жишээ нь олон ном нэг дор нэмэх:
python
# main.py (нэмэлт)
@app.post("/books/bulk", status_code=201)
def create_books(new_books: list[Book]):
global next_id
created = []
for book in new_books:
record = book.model_dump()
record["id"] = next_id
books.append(record)
created.append(record)
next_id += 1
return {"created_count": len(created), "books": created}new_books: list[Book] — энэ бол шинэ зүйл. Type hint нь "Book объектуудын жагсаалт". FastAPI body-г Book объектуудын жагсаалт гэж хүлээж, тус бүрийг шалгана.
Туршина
/docs -> POST /books/bulk -> Try it out:
json
[
{"title": "Ном 1", "author": "Зохиолч А", "year": 2000},
{"title": "Ном 2", "author": "Зохиолч Б", "year": 2010}
]Server response (статус 201):
json
{
"created_count": 2,
"books": [
{"title":"Ном 1","author":"Зохиолч А","year":2000,"description":null,"id":4},
{"title":"Ном 2","author":"Зохиолч Б","year":2010,"description":null,"id":5}
]
}Хоёр ном нэг хүсэлтээр нэмэгдсэн. list[Book] дотор нэг ном буруу байвал юу болох вэ? Туршаад үзээрэй — Pydantic жагсаалтын аль элемент буруу байгааг loc талбарт индексээр (["body", 0, "year"] гэх мэт) заана.
/docs дээр POST /books мөрийг задалж, Request body хэсгийг хараарай. FastAPI таны Book model-оос жишээ JSON үүсгэсэн:
json
{
"title": "string",
"author": "string",
"year": 0,
"description": "string"
}Schema таб дээр товшвол талбар бүрийн төрөл, заавал эсэх харагдана. title, author, year дээр од (*); description дээр од байхгүй (заавал бус).
POST /books/bulk дээр бол жишээ нь жагсаалт хэлбэртэй байх болно — FastAPI list[Book] гэдгийг ойлгож, тохирох жишээ үүсгэсэн.
Дахин хэлэхэд: та энэ баримт бичгийг бичээгүй. Model-оос автоматаар үүссэн.
model_dump-гүйгээр id нэмэх гэж
python
@app.post("/books")
def create_book(book: Book):
book["id"] = next_id # book бол dictionary биш!
return bookГаралт:
TypeError: 'Book' object does not support item assignmentbook нь Pydantic объект, dictionary биш. book["id"] = ... гэсэн dictionary-ийн синтакс ажиллахгүй. Эхлээд book.model_dump()-аар dictionary болгож, дараа нь ["id"] нэмнэ.
global мартах
python
@app.post("/books")
def create_book(book: Book):
new_book = book.model_dump()
new_book["id"] = next_id
books.append(new_book)
next_id += 1 # global байхгүй!
return new_bookГаралт:
UnboundLocalError: cannot access local variable 'next_id' where it is not associated with a valuenext_id += 1 гэж бичихэд Python next_id-г функцийн дотоод хувьсагч гэж үзнэ, гэтэл түүнд утга оноогоогүй тул алдаа. Засвар: функцийн эхэнд global next_id.
(Анзаараарай: books.append(...) нь global-гүйгээр ажилладаг — учир нь бид books-г өөрчилж байгаа болохоос дахин оноож байгаа биш. next_id += 1 бол дахин оноолт тул global хэрэгтэй. Энэ ялгаа Курс 2-ын Бүлэг 3-т тайлбарлагдсан.)
id-г model-д оруулах
python
class Book(BaseModel):
id: int # хэрэглэгч id илгээх ёсгүй!
title: str
author: strЭнэ нь id-г заавал болгоно — хэрэглэгч id илгээх ёстой болно. Гэтэл id-г сервер үүсгэх ёстой. Хэрэглэгч id: 999 гэж илгээвэл, эсвэл хоёр хэрэглэгч ижил id илгээвэл зөрчил үүснэ. id-г model-оос гаргах нь зөв. (Бүлэг 6-т бид оролтын model болон гаралтын model-ыг тусгаарлаж, энэ асуудлыг илүү цэвэрхэн шийднэ.)
Body-г dict гэж хүлээх
python
@app.post("/books")
def create_book(book: dict):
return bookЭнэ ажиллана, гэхдээ шалгалт байхгүй. dict гэдэг нь "ямар ч dictionary" — ямар талбар, ямар төрөл байхыг шалгахгүй. Хэрэглэгч юу ч илгээж болно, /docs юу ч харуулахгүй. Энэ бол Бүлэг 4-ийн эхэнд ярьсан гар аргын код руу буцах явдал. Үргэлж Pydantic model ашигла, dict биш.
Хүсвэл Book model-д genre: str = "уран зохиол" гэсэн талбар нэмээд, зарим номд genre өгч, заримд нь өгөлгүй туршаад үзээрэй. model_dump() дотор бүх талбар (default-ууд ч) орж байгааг ажиглаарай.
Сонирхвол create_book-д print(book) болон print(new_book) хоёуланг нэмээд, ялгааг терминал дээр хараарай. book нь Pydantic объектын хэлбэрээр, new_book нь dictionary хэлбэрээр (id-тай) харагдана. Хоёрын ялгааг гар дээрээ мэдрэх нь model_dump() яагаад хэрэгтэйг тодруулна.
FastAPI body-г дөрвөн алхмаар боловсруулна: JSON задлах -> Pydantic-д өгөх -> шалгах -> объект дамжуулах.
Функцийн доторх book нь Pydantic объект — book.title (цэгээр), book["title"] биш.
Өгөгдөл зөв төрөлтэй тул түүн дээр шууд тооцоолол хийж болно (book.year // 100).
Жинхэнэ үүсгэх = серверийн мэдлэг нэмэх (id) + хадгалах + буцаах. Echo биш.
book.model_dump()-аар dictionary болгож, дараа нь id нэмнэ.
id-г сервер үүсгэдэг, хэрэглэгч биш — тиймээс id талбар оролтын model-д байхгүй.
Body + path хамт ажиллана; list[Book]-аар олон объект нэг дор авна.
global нь дахин оноолтод (next_id += 1) хэрэгтэй; append зэрэг өөрчлөлтөд хэрэггүй.
Одоо та жинхэнэ үүсгэх endpoint бичиж чадна. Гэхдээ бид одоог хүртэл /docs-ийн гоо үзэсгэлэнг бүрэн ашиглаагүй. Дараагийн хичээл богино бөгөөд гардан дадлагын шинжтэй: бид /docs хуудсыг бүрэн туршилтын хэрэгсэл болгон ашиглаж, POST хүсэлтийг засварлаж, зөв ба буруу body илгээж, хоёр үр дүнг уншиж сурна. Postman гэх мэт тусдаа программ хэрэггүй — таны лаборатори аль хэдийн бэлэн.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.