Ачааллж байна...
Бүлэг 8-ын Хичээл 2-т бид skip болон limit параметрүүдийг dependency болгож бүтээсэн. Бүлэг 12-т тэдгээрийг өгөгдлийн санд залгасан. Тэр нь ажиллаж байна.
Гэхдээ дутуу. Хэрэглэгч ?skip=20&limit=10 гэж хүсэлт илгээгээд арван тэмдэглэл авна. Дараа нь тэр асууж болно:
Нийт хэдэн тэмдэглэл байна вэ?
Би хэддүгээр хуудсанд байна вэ?
Дараагийн хуудас байгаа юу?
Нийт хэдэн хуудас байна вэ?
Одоогийн API эдгээрийн аль нэгэнд ч хариулж чадахгүй. Хэрэглэгч зөвхөн арван тэмдэглэл хараад, цаашид юу байгааг мэдэхгүй.
Энэ хичээлд бид түүнийг засна. Мөн pagination яагаад сонголт биш, шаардлага болохыг ойлгоно.
Эхлээд нэг зүйлийг тодруулъя: pagination нь зөвхөн тав тухын зүйл биш, хамгаалалт юм.
Хэрэв таны API бүх өгөгдлийг буцаадаг байсан бол:
Санах ой. Сая тэмдэглэл байвал бүгдийг санах ойд ачаалах ёстой. Таны сервер унана.
Сүлжээ. Сая мөр JSON нь хэдэн зуун мегабайт. Хэрэглэгчийн интернэт удаашрана, зардал нэмэгдэнэ.
Хугацаа. Хэрэглэгч гучин секунд хүлээнэ. Тэр хугацаанд бусад хүсэлт удаашрана.
Халдлага. Хортой хэрэглэгч /notes руу давтан хандаж, серверийг унагаж чадна.
Тиймээс бодит API бүр pagination-тай байдаг. Twitter, GitHub, Google — бүгд. Хязгааргүй жагсаалт буцаадаг API нь эвдрэх нь цаг хугацааны асуудал юм.
Таны MAX_PAGE_SIZE=50 тохиргоо (Бүлэг 9) нь яг энэ хамгаалалт. Хэрэглэгч ?limit=999999 гэж хүссэн ч 50-аар хязгаарлагдана.
Мянган хуудастай номыг төсөөлье. Та түүнийг бүхэлд нь нэг дор уншдаггүй — хуудас хуудсаар нь уншдаг.
Хуудас бүрд гурван зүйл байдаг:
Агуулга — тэр хуудсан дээрх текст.
Хуудасны дугаар — та хаана байгаа.
Нийт хуудас — ном хэр урт.
Хэрэв номонд хуудасны дугаар байхгүй байсан бол та төөрөх байсан. "Би хаана байна? Хэр их үлдсэн бэ? Дараа нь юу байна?"
Одоогийн таны API нь дугааргүй ном юм. Агуулга өгдөг, гэхдээ байршил өгдөггүй.
Энэ хичээлд бид дугаар нэмнэ.
Pagination-д хоёр үндсэн загвар байдаг. Хоёуланг товч мэдэх нь зүйтэй.
Offset pagination — "20 мөр алгасаад, 10 өгөөч". Энэ бол таны одоо ашиглаж байгаа загвар (skip/limit). Ойлгомжтой, хэрэгжүүлэхэд хялбар, хуудасны дугаартай сайн ажилладаг.
Сул тал: маш том өгөгдөлд удаан болдог. OFFSET 1000000 гэвэл өгөгдлийн сан сая мөрийг алгасахын тулд тэдгээрийг тоолох ёстой.
Cursor pagination — "энэ id-гээс хойших 10-ыг өгөөч". Илүү хурдан, том өгөгдөлд илүү тохиромжтой. Гэхдээ хуудасны дугаар өгч чаддаггүй ("та 5-р хуудсанд байна" гэж хэлэх боломжгүй).
Бид offset загварыг ашиглана, учир нь тэр нь илүү энгийн, илүү түгээмэл, суралцахад тохиромжтой. Cursor нь маш том системд хэрэгтэй болдог бөгөөд энэ курсын хүрээнээс гадуур.
Одоогийн get_notes endpoint ийм байна:
python
@router.get("", response_model=list[NotePublic])
def get_notes(
params: ListParams = Depends(list_params),
session: Session = Depends(get_session),
):
statement = select(Note)
if params.search:
statement = statement.where(Note.text.contains(params.search))
if params.priority:
statement = statement.where(Note.priority == params.priority)
statement = statement.offset(params.skip).limit(params.limit)
return session.exec(statement).all()Хариу нь энгийн жагсаалт:
json
[
{"id":1,"text":"Сүү авах","priority":"чухал","done":false},
{"id":2,"text":"Номоо буцаах","priority":"энгийн","done":false}
]Мэдээлэл байхгүй. Зөвхөн өгөгдөл.
Мэргэжлийн API-ууд жагсаалтыг боодолтой (wrapped) хариунд буцаадаг:
json
{
"items": [
{"id":1,"text":"Сүү авах","priority":"чухал","done":false},
{"id":2,"text":"Номоо буцаах","priority":"энгийн","done":false}
],
"total": 47,
"skip": 0,
"limit": 2,
"has_more": true
}Одоо хэрэглэгч бүх зүйлийг мэднэ: өгөгдөл, нийт тоо, байршил, цаашид байгаа эсэх.
Энэ бол бүтцийн өөрчлөлт — хариу нь жагсаалт биш, объект болно. Тиймээс response_model ч өөрчлөгдөнө.
Эхлээд техникийн асуудал: нийт тоог яаж мэдэх вэ?
Гэнэн шийдэл нь бүх мөрийг татаад тоолох:
python
all_notes = session.exec(select(Note)).all()
total = len(all_notes)Энэ бол буруу. Бид яг л зайлсхийхийг хүсэж байсан зүйлээ хийж байна — бүх өгөгдлийг санах ойд авчирч байна. Сая мөртэй бол сервер унана.
Зөв шийдэл нь өгөгдлийн санд тоолуулах. Бүлэг 10-ын Хичээл 3-т сурсан COUNT(*):
sql
SELECT COUNT(*) FROM noteӨгөгдлийн сан тоолж, зөвхөн нэг тоо буцаана. Сая мөр ч хөдлөхгүй.
SQLModel-д үүнийг ингэж бичнэ:
python
from sqlmodel import func, select
count_statement = select(func.count()).select_from(Note)
total = session.exec(count_statement).one()func.count() — SQL-ын COUNT(*). func нь SQL-ын функцүүдийн цуглуулга.
.select_from(Note) — аль хүснэгтээс тоолохыг заана.
.one() — яг нэг үр дүн хүлээж байна (.all() биш, учир нь тоолол нь үргэлж нэг тоо).
Энд нэг нарийн зүйл байна, түүнийг сайтар анзаарах хэрэгтэй.
Хэрэв хэрэглэгч ?search=бичих гэж хайсан бол нийт тоо нь юу байх ёстой вэ?
Бүх тэмдэглэлийн тоо (47)? Үгүй. Хайлтад тохирсон тэмдэглэлийн тоо (3).
Тиймээс шүүлтийг хоёр асуултад хоёуланд нь хэрэглэх ёстой: өгөгдөл авах асуултад болон тоолох асуултад.
Хэрэв мартвал хэрэглэгч "нийт 47 байна" гэж харна, гэтэл гурав л ирнэ — төөрөгдөнө.
Энэ нь код давхардуулах эрсдэл үүсгэнэ. Шийдэл нь: шүүлтийг нэг газар бичиж, хоёр асуултад хэрэглэх.
Эхлээд боодлын model үүсгэе. app/models.py-д нэмнэ:
python
# app/models.py (нэмэлт)
class NoteList(SQLModel):
items: list[NotePublic]
total: int
skip: int
limit: int
has_more: boolХадгална.
NoteList — жагсаалтын боодол. items нь NotePublic объектуудын жагсаалт (Бүлэг 6-ын model доторх model). Бусад нь мета мэдээлэл.
table=True байхгүй — энэ бол зөвхөн хариуны хэлбэр, хүснэгт биш.
app/routers/notes.py-ийн get_notes функцийг солино. Мөн import-д func болон NoteList нэмнэ:
python
# app/routers/notes.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, Note, NoteCreate, NoteList, NotePublicДараа нь get_notes-ыг солино:
python
# app/routers/notes.py (get_notes-ыг солино)
@router.get("", response_model=NoteList)
def get_notes(
params: ListParams = Depends(list_params),
session: Session = Depends(get_session),
):
filters = []
if params.search:
filters.append(Note.text.contains(params.search))
if params.priority:
filters.append(Note.priority == params.priority)
# Нийт тоог тоолох (шүүлттэй)
count_statement = select(func.count()).select_from(Note)
for condition in filters:
count_statement = count_statement.where(condition)
total = session.exec(count_statement).one()
# Өгөгдлийг авах (ижил шүүлттэй)
statement = select(Note)
for condition in filters:
statement = statement.where(condition)
statement = statement.offset(params.skip).limit(params.limit)
items = session.exec(statement).all()
return NoteList(
items=items,
total=total,
skip=params.skip,
limit=params.limit,
has_more=params.skip + len(items) < total,
)Хадгална.
Кодын задаргаа
python
filters = []
if params.search:
filters.append(Note.text.contains(params.search))
if params.priority:
filters.append(Note.priority == params.priority)Шүүлтийн нөхцөлүүдийг жагсаалтад цуглуулж байна. Тэдгээрийг хараахан асуултад хэрэглээгүй.
Энэ бол чухал загвар. Нөхцөлийг нэг газар тодорхойлж, дараа нь хоёр асуултад хэрэглэнэ. Давхардал байхгүй, зөрчилдөх боломжгүй.
Курс 2-ын Бүлэг 3-т сурсан "функц бол утга" санаа энд ажиллаж байна: Note.text.contains(...) нь тэр дороо ажилладаггүй, харин дараа ашиглах нөхцөлийн объект үүсгэдэг. Тиймээс түүнийг жагсаалтад хийж, зөөж, хожим хэрэглэж болно.
python
count_statement = select(func.count()).select_from(Note)
for condition in filters:
count_statement = count_statement.where(condition)
total = session.exec(count_statement).one()Тоолох асуулт. Шүүлт бүрийг давталтаар нэмж байна. statement = statement.where(...) гэсэн оноолт заавал — Бүлэг 12-оос танил.
python
statement = select(Note)
for condition in filters:
statement = statement.where(condition)
statement = statement.offset(params.skip).limit(params.limit)
items = session.exec(statement).all()Өгөгдлийн асуулт. Ижил шүүлт, дараа нь хуудаслалт.
Анзаараарай: offset/limit нь зөвхөн энд байна, тоолох асуултад байхгүй. Учир нь нийт тоо нь хуудаслалтаас хамаарахгүй — тэр нь бүх тохирох мөрийн тоо.
python
return NoteList(
items=items,
total=total,
skip=params.skip,
limit=params.limit,
has_more=params.skip + len(items) < total,
)Боодлыг угсарч байна.
has_more — "цаашид өгөгдөл байна уу?". Логик: одоогийн байрлал (skip) дээр авсан тоог (len(items)) нэмээд, нийт тооноос бага бол цаашид байна.
Жишээ: skip=0, 10 авсан, нийт 47 -> 0 + 10 = 10 < 47 -> үнэн, цаашид байна.
Жишээ: skip=40, 7 авсан, нийт 47 -> 40 + 7 = 47 < 47 -> худал, дууссан.
Сервер ажиллуулна. Хэрэв тэмдэглэл цөөн бол хэдэн ширхэг нэмнэ (POST, түлхүүртэй).
Үндсэн хүсэлт
http://127.0.0.1:8000/notesХариу:
json
{
"items": [
{"id":1,"text":"Сүү ба талх авах","priority":"чухал","done":true},
{"id":3,"text":"Тайлан бичих","priority":"яаралтай","done":false}
],
"total": 2,
"skip": 0,
"limit": 10,
"has_more": false
}Одоо хэрэглэгч бүх зүйлийг мэднэ: хоёр тэмдэглэл байна, эхнээс нь харж байна, цаашид байхгүй.
Хуудаслалттай
Илүү сайн турших гэж хэдэн тэмдэглэл нэмээрэй — таваас доошгүй. Дараа нь:
http://127.0.0.1:8000/notes?limit=2Хариу:
json
{
"items": [ ... хоёр тэмдэглэл ... ],
"total": 6,
"skip": 0,
"limit": 2,
"has_more": true
}has_more: true — цаашид байна.
http://127.0.0.1:8000/notes?skip=2&limit=2Хариу:
json
{
"items": [ ... дараагийн хоёр ... ],
"total": 6,
"skip": 2,
"limit": 2,
"has_more": true
}http://127.0.0.1:8000/notes?skip=4&limit=2Хариу:
json
{
"items": [ ... сүүлийн хоёр ... ],
"total": 6,
"skip": 4,
"limit": 2,
"has_more": false
}has_more: false — сүүлийн хуудас.
Хэрэглэгч одоо хуудсаар шилжиж чадна. Frontend нь has_more талбарыг хараад "Дараах" товчийг идэвхжүүлэх эсэхийг шийднэ.
Шүүлттэй тоолол
http://127.0.0.1:8000/notes?priority=чухалХариу:
json
{
"items": [ ... чухал тэмдэглэлүүд ... ],
"total": 2,
"skip": 0,
"limit": 10,
"has_more": false
}total нь бүх тэмдэглэлийн тоо биш, чухал тэмдэглэлийн тоо. Шүүлт тоололд ч хэрэглэгдсэн.
Энэ бол дээр ярьсан нарийн зүйлийн бодит илрэл. Хэрэв шүүлтийг тоололд хэрэглээгүй бол total нь 6 гэж гарах байсан — худал.
SQL-ыг харах
echo=True асаавал энэ хүсэлт хоёр SQL үүсгэнэ:
sql
SELECT count(*) AS count_1 FROM note WHERE (note.priority = ?)sql
SELECT note.id, note.text, note.priority, note.done
FROM note WHERE (note.priority = ?)
LIMIT ? OFFSET ?Эхнийх нь тоолно, хоёр дахь нь өгөгдөл авна. Хоёулаа ижил WHERE нөхцөлтэй.
Хоёр асуулт нь нэгээс удаан юу? Бага зэрэг. Гэхдээ COUNT нь маш хурдан (өгөгдөл татдаггүй, зөвхөн тоолдог) тул зардал бага. Хэрэглэгчид өгч байгаа мэдээлэл нь тэр зардлыг зөвтгөнө.
Одоо нэг тав тухыг нэмье. skip/limit нь техникийн — хэрэглэгч "хэддүгээр хуудас" гэж бодохыг илүүд үздэг.
NoteList model-д хоёр талбар нэмнэ:
python
# app/models.py (NoteList-ыг солино)
class NoteList(SQLModel):
items: list[NotePublic]
total: int
skip: int
limit: int
has_more: bool
page: int
total_pages: intRouter-т тооцоолол нэмнэ:
python
# app/routers/notes.py (return хэсгийг солино)
page = (params.skip // params.limit) + 1
total_pages = (total + params.limit - 1) // params.limit
return NoteList(
items=items,
total=total,
skip=params.skip,
limit=params.limit,
has_more=params.skip + len(items) < total,
page=page,
total_pages=total_pages,
)Хадгална.
Тооцооллыг ойлгох
python
page = (params.skip // params.limit) + 1// — бүхэл хуваалт (Курс 1). skip=0, limit=10 -> 0 // 10 = 0 -> хуудас 1. skip=20, limit=10 -> 20 // 10 = 2 -> хуудас 3.
+1 нь хүн 1-ээс тоолдог тул (программ 0-ээс тоолдог).
python
total_pages = (total + params.limit - 1) // params.limitЭнэ нь дээш бөөрөнхийлсөн хуваалт. Яагаад ийм хачирхалтай томьёо вэ?
Жишээ: нийт 47, хуудсанд 10. 47 // 10 = 4 — гэхдээ 4 хуудсанд зөвхөн 40 багтана. Үлдсэн 7-д тав дахь хуудас хэрэгтэй. Тиймээс 5 байх ёстой.
Томьёо: (47 + 10 - 1) // 10 = 56 // 10 = 5. Зөв.
Хэрэв яг тэнцүү хуваагдвал: нийт 40, хуудсанд 10. (40 + 10 - 1) // 10 = 49 // 10 = 4. Зөв — нэмэлт хуудас үүсгээгүй.
Энэ бол программчлалын түгээмэл заль мэх: дээш бөөрөнхийлөх = (a + b - 1) // b.
Туршина
http://127.0.0.1:8000/notes?limit=2Хариу:
json
{
"items": [ ... ],
"total": 6,
"skip": 0,
"limit": 2,
"has_more": true,
"page": 1,
"total_pages": 3
}"1-р хуудас, нийт 3 хуудас." Хүн ойлгомжтой.
http://127.0.0.1:8000/notes?skip=2&limit=2page: 2, total_pages: 3.
http://127.0.0.1:8000/notes?skip=4&limit=2page: 3, total_pages: 3, has_more: false.
Одоо чухал зүйлийг тэмдэглэе. Бид хариуны бүтцийг өөрчилсөн.
Өмнө нь:
json
[{"id":1,...}, {"id":2,...}]Одоо:
json
{"items": [{"id":1,...}], "total": 6, ...}Хэрэв хэн нэгэн таны API-г аль хэдийн ашиглаж байсан бол тэдний код эвдэрнэ. Тэд жагсаалт хүлээж байсан, объект ирлээ.
Энэ бол breaking change — таны API-ийн гэрээг зөрчсөн өөрчлөлт.
Бодит систем үүнийг хэрхэн зохицуулдаг вэ? Хэд хэдэн арга бий:
Хувилбарлах. /v1/notes (хуучин) болон /v2/notes (шинэ) хоёуланг ажиллуулах. Хуучин хэрэглэгчид үргэлжлүүлж, шинэ нь шинэчлэгдэнэ.
Урьдчилан мэдэгдэх. "Гурван сарын дараа өөрчлөгдөнө" гэж зарлаж, хэрэглэгчдэд бэлдэх хугацаа өгөх.
Эхнээс нь зөв хийх. Хамгийн сайн арга: API-г эхнээсээ боодолтой бүтэцтэй хийх. Тэгвэл хожим өөрчлөх шаардлагагүй.
Бид capstone-д яг тэгнэ — жагсаалт буцаадаг endpoint бүр эхнээсээ боодолтой байна.
Энэ бол API зохион бүтээхэд суралцах чухал сургамж: ирээдүйд өргөжих боломжтой бүтэц сонго. Бүлэг 3-ын Хичээл 1-т "үргэлж dictionary буцаа, ганц утга биш" гэж хэлсэн зарчмын том хувилбар.
Одоо шүүлтийг нэмэх нь хэр хялбар болсныг харъя. done шүүлт нэмье.
app/models.py-д ListParams-ыг өөрчилнө:
python
class ListParams(SQLModel):
skip: int
limit: int
search: str
priority: str
done: bool | None = Noneapp/dependencies.py-д:
python
def list_params(
skip: int = 0,
limit: int = 10,
search: str = "",
priority: str = "",
done: bool | None = None,
) -> 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,
priority=priority,
done=done,
)app/routers/notes.py-д, filters жагсаалтад нэмнэ:
python
if params.done is not None:
filters.append(Note.done == params.done)Хадгална.
Гурван файлд бага зэрэг өөрчлөлт, тоолол болон өгөгдөл хоёулаа автоматаар шинэ шүүлтийг авлаа.
Учир нь бид filters жагсаалтын загварыг ашигласан. Нөхцөл нэмэхэд хоёр асуултад хоёуланд нь хэрэглэгдэнэ. Хэрэв шүүлтийг хоёр газар гараар бичсэн бол хоёр газар засах ёстой байсан, нэгийг нь мартах эрсдэлтэй.
Туршина
http://127.0.0.1:8000/notes?done=trueХариу: дууссан тэмдэглэлүүд, total нь тэдгээрийн тоо.
http://127.0.0.1:8000/notes?done=false&priority=чухалХоёр шүүлт хамт, тоолол ч тохирсон.
if params.done is not None: — False бол хүчинтэй шүүлт (Бүлэг 6, 12-оос танил).
Шүүлтийг тоололд хэрэглэхгүй
python
count_statement = select(func.count()).select_from(Note)
total = session.exec(count_statement).one() # шүүлтгүй!total нь үргэлж бүх мөрийн тоо байна — шүүлтээс хамаарахгүй. Хэрэглэгч "нийт 47" гэж харна, гэтэл 3 ирнэ.
Энэ бол хамгийн түгээмэл pagination алдаа. Шүүлт хоёуланд нь.
Тоолохын тулд бүгдийг татах
python
total = len(session.exec(select(Note)).all())Ажиллана, гэхдээ бүх өгөгдлийг санах ойд авчирна — pagination-ийн зорилгыг устгана.
Засвар: func.count().
offset/limit-ыг тоололд хэрэглэх
python
count_statement = select(func.count()).select_from(Note).limit(params.limit)total нь limit-ээс хэтрэхгүй болно — утгагүй. Нийт тоо нь хуудаслалтаас хамаарахгүй.
.one() биш .all()
python
total = session.exec(count_statement).all() # жагсаалт буцнаtotal нь тоо биш, нэг элементтэй жагсаалт болно. NoteList-ийн total: int тул validation алдаа гарна.
Засвар: .one().
has_more-ыг буруу тооцоолох
python
has_more = params.limit == len(items) # буруу логикХэрэв яг сүүлийн хуудсанд limit тоотой тэнцүү мөр байвал has_more нь худлаа true гэж гарна.
Зөв: params.skip + len(items) < total.
Хуудасны дугаарыг доош бөөрөнхийлөх
python
total_pages = total // params.limit # бурууНийт 47, хуудсанд 10 -> 4 гэж гарна, гэтэл 5 байх ёстой. Сүүлийн 7 мөр "алга болно".
Зөв: (total + limit - 1) // limit.
Хүсвэл арав орчим тэмдэглэл нэмээд, ?limit=3 гэж хуудсаар шилжиж үзээрэй. page, total_pages, has_more талбарууд хэрхэн өөрчлөгдөхийг ажиглаарай. Сүүлийн хуудсанд has_more нь false болохыг батлаарай.
Сонирхвол echo=True асааж, нэг хүсэлтэд хоёр SQL үүсэхийг хараарай. COUNT асуулт болон өгөгдлийн асуулт хоёулаа ижил WHERE нөхцөлтэй байгааг ажиглах нь filters загварын утгыг бататгана.
Pagination бол сонголт биш, хамгаалалт — санах ой, сүлжээ, хугацаа, халдлагаас сэргийлнэ.
Хоёр загвар: offset (skip/limit, энгийн, хуудасны дугаартай) ба cursor (том өгөгдөлд хурдан, дугааргүй). Бид offset ашиглана.
Энгийн жагсаалт буцаах нь дутуу — хэрэглэгч нийт тоо, байршлаа мэдэхгүй.
Шийдэл: боодолтой хариу — items, total, skip, limit, has_more, page, total_pages.
Нийт тоог func.count()-оор өгөгдлийн санд тоолуулна, бүгдийг татаж len() хийхгүй.
Шүүлтийг хоёр асуултад хоёуланд нь хэрэглэнэ — өгөгдөл болон тоололд. filters жагсаалтын загвар давхардлаас сэргийлнэ.
offset/limit нь зөвхөн өгөгдлийн асуултад; тоололд байхгүй.
has_more = skip + len(items) < total. page = skip // limit + 1. total_pages = (total + limit - 1) // limit (дээш бөөрөнхийлөх).
Бүтэц өөрчлөх нь breaking change — хуучин хэрэглэгчид эвдэрнэ. Эхнээсээ боодолтой хийх нь хамгийн зөв.
Бүлэг 13 дууслаа. Түвшин 5-ын нэмэлт чадварууд таны гарт: async-ийн зөв ойлголт, CORS, мэргэжлийн pagination.
Дараагийн бүлэг бол capstone — гурван хичээлээр бүтээх Номын сангийн API. Курс 2-ын Түвшин 3-ын бүтээн байгуулалт — Book, Member, зээлэх, буцаах бүхий номын сангийн систем — санаж байна уу? Тэр программ жинхэнэ вэб үйлчилгээ болж дахин төрнө: өгөгдлийн сантай, олон router-тэй, хамгаалалттай, pagination-тай, зээлийн бодит логиктой. Энэ бол таны бүтээх хамгийн том, хамгийн бүрэн программ бөгөөд гурван сургалтын аяны оргил юм.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.