Ачааллж байна...
Өмнөх хичээлд бид session-ыг dependency болгож, гурван endpoint (жагсаалт, нэг тэмдэглэл, үүсгэх)-ыг өгөгдлийн санд шилжүүлсэн. Өгөгдөл эцэст нь үлдэж эхэлсэн.
Энэ хичээлд бид бүх endpoint-ыг шилжүүлнэ: PUT (засах), DELETE (устгах), мөн хайлт, шүүлт, хуудаслалт.
Хамгийн чухал нь: бид Python-ы санах ойн логик хэрхэн SQL болж хувирахыг мөр мөрөөр нь харна. [n for n in notes if ...] нь .where(...) болно. result[skip : skip + limit] нь .offset(skip).limit(limit) болно. Тэр ялгаа нь зөвхөн синтакс биш — сая мөртэй өгөгдлийн санд бүх зүйлийг шийддэг үндсэн ялгаа юм.
Өмнөх хичээлийн төгсгөлд бид нэг дутагдлыг тэмдэглэсэн: оролтод table model (Note) ашигласан тул хэрэглэгч id илгээж чадна.
Одоо түүнийг зөв болгоё. Бүлэг 6-ын оролт-гаралтын хос загвар одоо гуравт өргөжинө.
main.py файлаа дараах кодоор солино:
python
# main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlmodel import SQLModel, Field, Session, create_engine, select
app = FastAPI(title="Тэмдэглэлийн API")
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
engine = create_engine("sqlite:///notes.db")
def get_session():
with Session(engine) as session:
yield session
@app.on_event("startup")
def on_startup():
SQLModel.metadata.create_all(engine)Хадгална. (Endpoint-үүдийг доор нэмнэ.)
Гурван model-ын үүрэг
NoteCreate (table биш) — хэрэглэгч илгээх зүйл. id байхгүй — сервер өгнө. Энэ бол хаалганы харуул.
Note (table=True) — өгөгдлийн сангийн хүснэгт. id байна, primary_key-тэй. Энэ бол бодит хадгалагдах бүтэц.
NotePublic (table биш) — хэрэглэгч рүү буцах зүйл. id байна (заавал, | None биш). Энэ бол гарцын шүүлтүүр.
table=True зөвхөн дунд model-д байгааг анзаараарай. Бүлэг 11-ийн Хичээл 2-т сурсанчлан create_all нь зөвхөн table=True бүхий class-д хүснэгт үүсгэнэ. NoteCreate болон NotePublic нь зөвхөн Pydantic model — validation, /docs, response_model-д ашиглагдана, гэхдээ хүснэгт биш.
Мөн анзаараарай: гурвуулаа BaseModel-ээс биш, SQLModel-ээс өвлөж байна. SQLModel нь Pydantic-ийн BaseModel-ыг өргөтгөсөн тул SQLModel (table-гүй) нь Pydantic model шиг ажиллана. Тиймээс бүх зүйлийг нэг import-оор хийж болно.
Яагаад NotePublic-д id заавал вэ
Note-д id: int | None (хадгалахаас өмнө None байж болно). NotePublic-д id: int — заавал, None байж болохгүй.
Учир нь: хэрэглэгч рүү буцаж байгаа тэмдэглэл нь аль хэдийн хадгалагдсан тул id-тэй байх ёстой. Хэрэв ямар нэг байдлаар id-гүй объект буцаах гэвэл response_model нь алдаа өгнө (Бүлэг 6-ын Хичээл 1-т сурсан баталгаажуулалт).
Энэ бол сайн зохион байгуулалт: model бүр өөрийн үе шатанд байх ёстой байдлыг илэрхийлж байна.
Файлын төгсгөлд нэмнэ:
python
# main.py (нэмэлт)
@app.post("/notes", status_code=201, response_model=NotePublic)
def create_note(note: NoteCreate, session: Session = Depends(get_session)):
db_note = Note(
text=note.text,
priority=note.priority,
done=note.done,
)
session.add(db_note)
session.commit()
session.refresh(db_note)
return db_noteХадгална.
Кодын задаргаа
note: NoteCreate — оролт нь NoteCreate. Хэрэглэгч id илгээж чадахгүй.
response_model=NotePublic — гаралт нь NotePublic.
python
db_note = Note(
text=note.text,
priority=note.priority,
done=note.done,
)Оролтын объектоос хүснэгтийн объект үүсгэж байна. Талбар бүрийг гараар хуулж байна.
Яагаад шууд session.add(note) гэж болохгүй вэ? Учир нь note нь NoteCreate — тэр нь хүснэгт биш. Session нь зөвхөн table model-ыг хадгалж чадна.
Тиймээс хөрвүүлэлт хэрэгтэй: NoteCreate -> Note.
Илүү товч хувилбар
Талбар бүрийг гараар бичих нь уйтгартай, ялангуяа арван талбартай model-д. SQLModel-д илүү товч арга бий:
python
@app.post("/notes", status_code=201, response_model=NotePublic)
def create_note(note: NoteCreate, session: Session = Depends(get_session)):
db_note = Note.model_validate(note)
session.add(db_note)
session.commit()
session.refresh(db_note)
return db_noteNote.model_validate(note) — "note объектоос Note объект үүсгэ". Бүх тохирох талбарыг автоматаар хуулна.
Энэ нь Pydantic-ийн method бөгөөд Бүлэг 4-т сурсан validation-тай ижил механизм — зөвхөн одоо dictionary биш, өөр объектоос уншиж байна.
Бид энэ товч хувилбарыг ашиглана. Хадгална.
Санамж: Pydantic v1-т энэ нь Note.from_orm(note) эсвэл Note(**note.dict()) байсан. Хэрэв интернэтээс тийм код харвал — тэр нь хуучин бичлэг. v2-т model_validate.
Туршина
Сервер ажиллуулна. notes.db устгасан бол шинээр үүснэ.
/docs -> POST /notes -> Try it out.
Request body хэсгийг анзаараарай:
json
{
"text": "string",
"priority": "энгийн",
"done": false
}id байхгүй. NoteCreate model-д байхгүй тул /docs ч харуулахгүй. Хэрэглэгч id илгээх боломжгүй боллоо.
Гурван тэмдэглэл нэмнэ:
json
{"text": "Сүү авах", "priority": "чухал"}json
{"text": "Номоо буцаах"}json
{"text": "Тайлан бичих", "priority": "яаралтай"}Хариу (статус 201):
json
{"id":1,"text":"Сүү авах","priority":"чухал","done":false}id нь хариунд байна (NotePublic-д зарлагдсан).
Одоо хамгийн сонирхолтой хэсэг. Файлын төгсгөлд нэмнэ:
python
# main.py (нэмэлт)
@app.get("/notes", response_model=list[NotePublic])
def get_notes(
search: str = "",
priority: str = "",
done: bool | None = None,
skip: int = 0,
limit: int = 10,
session: Session = Depends(get_session),
):
statement = select(Note)
if search:
statement = statement.where(Note.text.contains(search))
if priority:
statement = statement.where(Note.priority == priority)
if done is not None:
statement = statement.where(Note.done == done)
statement = statement.offset(skip).limit(limit)
return session.exec(statement).all()Хадгална.
Кодын задаргаа — гол сургамж
Энэ бол энэ хичээлийн хамгийн чухал код. Түүнийг сайтар харцгаая.
python
statement = select(Note)Асуулт бэлдэж эхэлж байна. Одоохондоо "бүх тэмдэглэл".
python
if search:
statement = statement.where(Note.text.contains(search))Нөхцөл байвал асуултад where нэмж байна. statement нь дахин оноогдож байгааг анзаараарай — SQLModel-ийн асуулт нь өөрчлөгддөггүй; шинэ нөхцөл нэмэх бүрд шинэ асуулт үүсдэг.
Note.text.contains(search) — SQL-ын LIKE '%утга%' болно. "Энэ текст дотор тэр үг байна уу?"
python
statement = statement.offset(skip).limit(limit)Хуудаслалт нэмж байна.
python
return session.exec(statement).all()Бүх нөхцөл бүрдсэний дараа л асуулт ажиллана.
v2-той харьцуулах — үндсэн ялгаа
Одоо v2-ийн кодтой зэрэгцүүлж харъя:
v2 (санах ойд):
python
result = notes # бүх тэмдэглэл
if search:
result = [n for n in result if search.lower() in n["text"].lower()]
if priority:
result = [n for n in result if n["priority"] == priority]
return result[skip : skip + limit]v3 (өгөгдлийн санд):
python
statement = select(Note) # асуултын тодорхойлолт
if search:
statement = statement.where(Note.text.contains(search))
if priority:
statement = statement.where(Note.priority == priority)
return session.exec(statement.offset(skip).limit(limit)).all()Бүтэц нь бараг ижил. Дараалсан шүүлт, нөхцөл бүрд нэг блок, эцэст нь хязгаарлалт.
Гэхдээ утга нь бүрэн өөр.
v2-т result бол бодит жагсаалт — бүх тэмдэглэл санах ойд байна. Шүүлт бүр шинэ жагсаалт үүсгэж, санах ойд ажиллана. Хэрэв мянган тэмдэглэл байсан бол мянгуулаа санах ойд байна.
v3-т statement бол асуултын тодорхойлолт — өгөгдөл огт байхгүй. Тэр нь зөвхөн "юу асуухаа" тодорхойлж байна. session.exec(...) дуудагдах хүртэл өгөгдлийн сан руу юу ч явахгүй.
Дараа нь өгөгдлийн сан бүх шүүлт, хуудаслалтыг өөртөө хийж, зөвхөн эцсийн үр дүнг буцаана.
Тоогоор харуулбал
Сая тэмдэглэлтэй, ?limit=10 гэсэн хүсэлт ирлээ гэж бодъё.
v2: сая тэмдэглэлийг файлаас/санах ойгоос уншина -> сая элементийн жагсаалт үүснэ -> шүүлт хийж дахин жагсаалт үүснэ -> эцэст нь 10-ыг сонгоно. Сая мөр боловсруулагдсан, 10 хэрэглэгдсэн.
v3: өгөгдлийн санд SELECT ... LIMIT 10 OFFSET 0 явна -> өгөгдлийн сан index ашиглан 10 мөр олно -> 10 мөр Python руу ирнэ. Бусад 999,990 мөр хөдлөөгүй.
Энэ бол Бүлэг 10-ын Хичээл 1-т ярьсан бүх зүйлийн практик утга. Өгөгдлийн сан бол зөвхөн хадгалах газар биш — тэр нь ажлыг таны өмнөөс, илүү үр дүнтэй хийдэг систем юм.
SQL-ыг харах
Хэрэв та create_engine-д echo=True нэмбэл дараах хүсэлт:
http://127.0.0.1:8000/notes?search=бичих&limit=5ийм SQL үүсгэнэ:
sql
SELECT note.id, note.text, note.priority, note.done
FROM note
WHERE (note.text LIKE '%' || ? || '%')
LIMIT ? OFFSET ?Утгууд: ('бичих', 5, 0).
Бүлэг 10-ын Хичээл 3-т сурсан бүх зүйл энд байна: SELECT, WHERE, LIMIT, OFFSET. Таны Python код тэдгээр болж хувирсан.
LIKE '%' || ? || '%' — contains()-ийн SQL хувилбар. || нь SQL-д текст холбох тэмдэг; % нь "ямар ч зүйл" гэсэн орлуулагч. Тиймээс %бичих% нь "бичих гэсэн үг агуулсан ямар ч текст" гэсэн утгатай.
Туршина
http://127.0.0.1:8000/notesГурван тэмдэглэл.
http://127.0.0.1:8000/notes?search=бичихХариу:
json
[{"id":3,"text":"Тайлан бичих","priority":"яаралтай","done":false}]http://127.0.0.1:8000/notes?priority=чухалХариу:
json
[{"id":1,"text":"Сүү авах","priority":"чухал","done":false}]http://127.0.0.1:8000/notes?limit=2Хоёр тэмдэглэл.
http://127.0.0.1:8000/notes?skip=1&limit=2Хоёр дахь, гурав дахь тэмдэглэл.
Бүх шүүлт өгөгдлийн санд хийгдэж байна.
Том жижиг үсгийн тухай
Нэг практик тэмдэглэл. v2-т бид .lower() хоёр талд ашиглаж, том жижиг үсгийн ялгааг арилгаж байсан.
SQLite-д LIKE нь латин үсэгт том жижиг үсэг ялгадаггүй ("Hello" нь "hello"-той таарна). Гэхдээ кирилл үсэгт энэ баталгаа байхгүй — SQLite-ийн default тохиргоо нь зөвхөн ASCII-д ажилладаг.
Тиймээс ?search=Тайлан (том Т) болон ?search=тайлан (жижиг т) өөр үр дүн өгч болно.
Үүнийг зөв шийдэх нь нарийн (өгөгдлийн сангийн collation тохируулах, эсвэл жижиг үсгээр давхар багана хадгалах) бөгөөд энэ курсын хүрээнээс гадуур. Одоохондоо энэ хязгаарыг мэдэж байхад хангалттай. Хэрэглэгчид хайхдаа яг бичсэнээр нь хайх болно.
Файлын төгсгөлд нэмнэ:
python
# main.py (нэмэлт)
@app.get("/notes/{note_id}", response_model=NotePublic)
def get_note(note_id: int, session: Session = Depends(get_session)):
note = session.get(Note, note_id)
if note is None:
raise HTTPException(status_code=404, detail="Тэмдэглэл олдсонгүй")
return noteХадгална.
Өмнөх хичээлээс танил. session.get нь None буцаах боломжтой; шалгаж, 404 шидэнэ.
Бүлэг 11-ийн None шалгалт нь одоо HTTP 404 болж хувирлаа. Хоёр ертөнцийн холбоос.
Файлын төгсгөлд нэмнэ:
python
# main.py (нэмэлт)
@app.put("/notes/{note_id}", response_model=NotePublic)
def update_note(
note_id: int,
note: NoteCreate,
session: Session = Depends(get_session),
):
db_note = session.get(Note, note_id)
if db_note is None:
raise HTTPException(status_code=404, detail="Тэмдэглэл олдсонгүй")
db_note.text = note.text
db_note.priority = note.priority
db_note.done = note.done
session.commit()
session.refresh(db_note)
return db_noteХадгална.
Кодын задаргаа
note: NoteCreate — оролт нь ижил NoteCreate model. Засахад ч ижил талбарууд ирдэг.
db_note = session.get(Note, note_id) — өгөгдлийн сангаас объектыг олно.
if db_note is None: raise ... — байхгүй бол 404.
python
db_note.text = note.text
db_note.priority = note.priority
db_note.done = note.doneТалбаруудыг өөрчилж байна. Ердийн Python оноолт.
python
session.commit()session.add(db_note) байхгүйг анзаараарай. Бүлэг 11-ийн Хичээл 4-т сурсанчлан: session-аас авсан объект аль хэдийн бүртгэгдсэн, түүний өөрчлөлтийг session өөрөө мөрддөг. commit хийхэд UPDATE автоматаар үүснэ.
session.refresh(db_note) — шинэчлэгдсэн утгыг өгөгдлийн сангаас дахин уншина.
Туршина
/docs -> PUT /notes/{note_id}. Path 1, body:
json
{"text": "Сүү ба талх авах", "priority": "чухал", "done": true}Хариу (статус 200):
json
{"id":1,"text":"Сүү ба талх авах","priority":"чухал","done":true}Байхгүй id (PUT /notes/999) -> 404.
Одоо шүүлтээр батлана:
http://127.0.0.1:8000/notes?done=trueХариу: зөвхөн эхний тэмдэглэл (done=true болсон).
Файлын төгсгөлд нэмнэ:
python
# main.py (нэмэлт)
@app.delete("/notes/{note_id}", status_code=204)
def delete_note(note_id: int, session: Session = Depends(get_session)):
note = session.get(Note, note_id)
if note is None:
raise HTTPException(status_code=404, detail="Тэмдэглэл олдсонгүй")
session.delete(note)
session.commit()
return NoneХадгална.
Бүлэг 11-ийн Хичээл 4-ийн загвар: олох -> session.delete(объект) -> commit.
status_code=204 — No Content. response_model байхгүй, return None.
Туршина
/docs -> DELETE /notes/{note_id}. Path 2, Execute.
Хариу (статус 204): хоосон.
http://127.0.0.1:8000/notesХоёр тэмдэглэл үлдсэн (id 1 болон 3).
Дахин DELETE /notes/2 -> 404 (аль хэдийн устсан).
Энэ бол таны бүрэн main.py:
python
# main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlmodel import SQLModel, Field, Session, create_engine, select
app = FastAPI(title="Тэмдэглэлийн API")
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
engine = create_engine("sqlite:///notes.db")
def get_session():
with Session(engine) as session:
yield session
@app.on_event("startup")
def on_startup():
SQLModel.metadata.create_all(engine)
@app.post("/notes", status_code=201, response_model=NotePublic)
def create_note(note: NoteCreate, session: Session = Depends(get_session)):
db_note = Note.model_validate(note)
session.add(db_note)
session.commit()
session.refresh(db_note)
return db_note
@app.get("/notes", response_model=list[NotePublic])
def get_notes(
search: str = "",
priority: str = "",
done: bool | None = None,
skip: int = 0,
limit: int = 10,
session: Session = Depends(get_session),
):
statement = select(Note)
if search:
statement = statement.where(Note.text.contains(search))
if priority:
statement = statement.where(Note.priority == priority)
if done is not None:
statement = statement.where(Note.done == done)
statement = statement.offset(skip).limit(limit)
return session.exec(statement).all()
@app.get("/notes/{note_id}", response_model=NotePublic)
def get_note(note_id: int, session: Session = Depends(get_session)):
note = session.get(Note, note_id)
if note is None:
raise HTTPException(status_code=404, detail="Тэмдэглэл олдсонгүй")
return note
@app.put("/notes/{note_id}", response_model=NotePublic)
def update_note(
note_id: int,
note: NoteCreate,
session: Session = Depends(get_session),
):
db_note = session.get(Note, note_id)
if db_note is None:
raise HTTPException(status_code=404, detail="Тэмдэглэл олдсонгүй")
db_note.text = note.text
db_note.priority = note.priority
db_note.done = note.done
session.commit()
session.refresh(db_note)
return db_note
@app.delete("/notes/{note_id}", status_code=204)
def delete_note(note_id: int, session: Session = Depends(get_session)):
note = session.get(Note, note_id)
if note is None:
raise HTTPException(status_code=404, detail="Тэмдэглэл олдсонгүй")
session.delete(note)
session.commit()
return NoneЗуу орчим мөр. Бүрэн CRUD, өгөгдлийн сантай, хайлт, шүүлт, хуудаслалттай.
Серверээ унтраа (Ctrl+C), дахин асаа, дараа нь:
http://127.0.0.1:8000/notesТэмдэглэлүүд байрандаа. Устгасан нь устсан хэвээр, засварласан нь засварласан хэвээр.
Одоо бүтэн зургийг харъя. Endpoint бүр SQLModel-ийн тодорхой загвартай тохирч байна:
Endpoint | SQLModel | SQL |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Гурван давхарга, ижил дөрвөн үйлдэл. HTTP хүсэлт ирж, SQLModel код ажиллаж, SQL үүсэж, өгөгдлийн сан өөрчлөгдөнө.
Бүлэг 5-ын Хичээл 1-т би CRUD-ыг танилцуулж, "энэ бол бараг бүх API-ийн үндэс" гэж хэлсэн. Одоо та түүнийг дээрээс доош бүхэлд нь харж байна.
NoteCreate-ыг session.add-д өгөх
python
def create_note(note: NoteCreate, session: Session = Depends(get_session)):
session.add(note) # NoteCreate бол хүснэгт биш!Гаралт:
sqlalchemy.orm.exc.UnmappedInstanceError: Class 'NoteCreate' is not mappedSession нь зөвхөн table=True бүхий объектыг хадгална. Эхлээд Note.model_validate(note)-оор хөрвүүлнэ.
statement-ыг дахин оноохгүй
python
statement = select(Note)
if search:
statement.where(Note.text.contains(search)) # оноолт байхгүй!where нь шинэ асуулт буцаадаг, хуучныг өөрчилдөггүй. Оноолгүй бол нөхцөл алдагдана — шүүлт ажиллахгүй, алдаа ч гарахгүй.
Заавал: statement = statement.where(...).
response_model-д Note ашиглах
python
@app.get("/notes", response_model=list[Note]) # table modelЭнэ нь ажиллана, гэхдээ Note-д id: int | None тул /docs нь id-г "заавал биш" гэж харуулна — төөрөгдүүлнэ. Мөн ирээдүйд нуух талбар нэмэхэд асуудалтай.
Гаралтад үргэлж NotePublic.
done шүүлтэд if done:
python
if done: # буруу
statement = statement.where(Note.done == done)done=False нь хүчинтэй шүүлт (дуусаагүйг харах), гэтэл if done: түүнийг алгасана. Бүлэг 6-ын Хичээл 3-аас танил.
Заавал: if done is not None:.
commit мартах (PUT дээр)
python
db_note.text = note.text
session.refresh(db_note) # commit байхгүй!
return db_noterefresh нь өгөгдлийн сангаас дахин уншдаг тул таны өөрчлөлт устана — хуучин утга буцна. Алдаа гарахгүй, зөвхөн буруу үр дүн.
Дараалал заавал: commit -> refresh.
Хүсвэл create_engine-д echo=True нэмээд, endpoint бүрийг дуудаж, үүсэх SQL-ыг хараарай. GET /notes?search=...&limit=2 дуудахад бүх нөхцөл нэг SQL мөрөнд нэгдэж байгааг ажиглах нь энэ хичээлийн гол санааг бататгана.
Сонирхвол order_by нэмж үзээрэй — sort_desc: bool = False гэсэн query parameter нэмээд:
python
if sort_desc:
statement = statement.order_by(Note.id.desc())Шинэ нөхцөл нэмэх нь хэр амархан болохыг мэдрэх сайн дасгал.
Гурван model: NoteCreate (оролт, id-гүй), Note (table=True, хүснэгт), NotePublic (гаралт, id заавал).
table=True зөвхөн дунд model-д — бусад нь Pydantic model.
Note.model_validate(note) — оролтын объектоос хүснэгтийн объект үүсгэнэ (v1-ийн from_orm биш).
Шүүлтийг асуултад нэмнэ: statement = statement.where(...) — оноолт заавал, where шинэ асуулт буцаана.
Асуулт нь өгөгдөл биш — session.exec(...) дуудагдтал өгөгдлийн сан руу юу ч явахгүй.
Үндсэн ялгаа: v2 бүх өгөгдлийг санах ойд авчирч шүүдэг; v3 өгөгдлийн санд шүүж, зөвхөн хэрэгтэйг авчирдаг.
Note.text.contains(x) -> SQL LIKE '%x%'. .offset(skip).limit(limit) -> SQL LIMIT/OFFSET.
PUT-д add хэрэггүй — session өөрчлөлтийг мөрддөг. Дараалал: өөрчлөх -> commit -> refresh.
SQLite-ийн LIKE нь кирилл үсэгт том жижиг үсэг ялгадаг — мэдэж байх хязгаар.
if done is not None: — False бол хүчинтэй шүүлт.
Таны бүх endpoint одоо өгөгдлийн санд ажиллаж байна. Гэхдээ бүх код дахин нэг файлд буцаж хураагдсан — Түвшин 3-т бүтээсэн мэргэжлийн бүтэц алга болсон.
Дараагийн хичээлд бид хоёуланг нэгтгэнэ: Тэмдэглэлийн API v3. v2-ийн олон файлт бүтэц, API key хамгаалалт, .env тохиргоо, алдааны handler — бүгд хэвээр, гэхдээ санах ойн жагсаалтын оронд жинхэнэ өгөгдлийн сан. Тэр бол Тэмдэглэлийн API-ийн эцсийн хувилбар бөгөөд бүх сургалтын турш барьж ирсэн утас тэнд бүрэн тайлагдана. Бид тэр мөчийг зохих ёсоор тэмдэглэнэ.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.