| | 1 | = Функции, Процедури и Тригери = |
| | 2 | |
| | 3 | Во оваа фаза беа имплементирани три функции, три процедури и три тригери кои ја поддржуваат основната бизнис логика на системот IDontKnow. Тие автоматизираат процеси како пресметка на кредитно-пондериран просек и отворен долг, проверка на слободни места, регистрирање наплата со автоматска алокација кон обврски, упис на студент на курс, сериско генерирање обврски за школарина, како и валидација на оценки, ажурирање на отворената сума и контрола на капацитетот на курсевите преку тригери. |
| | 4 | |
| | 5 | Сите рутини се креирани во базата `advdb_202526l_prj_idontknow` (PostgreSQL 17.9). |
| | 6 | |
| | 7 | [attachment:13_routines.sql 13_routines.sql] |
| | 8 | |
| | 9 | |
| | 10 | == Функции == |
| | 11 | |
| | 12 | Функциите се користат за пресметување и враќање на вредности кои се потребни во повеќе делови од системот и може да се вградат директно во прашалник, поглед или друга рутина. |
| | 13 | |
| | 14 | === 1. fn_prosek_student === |
| | 15 | |
| | 16 | {{{ |
| | 17 | CREATE OR REPLACE FUNCTION fn_prosek_student( |
| | 18 | p_student_id INTEGER, |
| | 19 | p_sistem_ocenuvanje_id INTEGER DEFAULT NULL |
| | 20 | ) |
| | 21 | RETURNS NUMERIC |
| | 22 | LANGUAGE sql |
| | 23 | STABLE |
| | 24 | AS $$ |
| | 25 | SELECT ROUND(SUM(sov.ocena_vrednost::numeric * ak.krediti) |
| | 26 | / NULLIF(SUM(ak.krediti), 0), 2) |
| | 27 | FROM sm_student_ocena_zapis soz |
| | 28 | JOIN sm_student_upisan_kurs suk ON soz.student_upisan_kurs_id = suk.id |
| | 29 | JOIN sm_student_semestar_upis ssu ON suk.student_semestar_upis_id = ssu.id |
| | 30 | JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id |
| | 31 | JOIN sm_kurs_realizacija kr ON suk.kurs_realizacija_id = kr.id |
| | 32 | JOIN sm_akreditacija ak ON kr.akreditacija_id = ak.id |
| | 33 | JOIN sm_sistem_ocenuvanje_vrednost sov ON soz.sistem_ocenuvanje_vrednost_id = sov.id |
| | 34 | WHERE ss.student_id = p_student_id |
| | 35 | AND sov.polozitelna |
| | 36 | AND (p_sistem_ocenuvanje_id IS NULL OR sov.sistem_ocenuvanje_id = p_sistem_ocenuvanje_id); |
| | 37 | $$; |
| | 38 | }}} |
| | 39 | |
| | 40 | Функцијата `fn_prosek_student` го пресметува кредитно-пондерираниот просек од положените оценки на даден студент. Како влезни параметри прима `p_student_id` и опционално `p_sistem_ocenuvanje_id`, со кој пресметката може да се ограничи на еден систем на оценување. Поминувајќи низ синџирот на табели од `sm_student_ocena_zapis` до `sm_akreditacija`, ја дели сумата на (вредност на оценка × кредити) со вкупните кредити и резултатот го заокружува на две децимали. |
| | 41 | |
| | 42 | Реализирана е како функција бидејќи враќа единечна вредност што директно се вградува во прашалници — на пример листа на студенти со просек над праг, услов за стипендија или уверение (`SELECT indeks, fn_prosek_student(s.id) FROM sm_studenti s`), што не е возможно со процедура. За масовни извештаи постои погледот `vw_studentski_oceni`; функцијата е за поединечна, параметризирана употреба во произволен контекст. |
| | 43 | |
| | 44 | === 2. fn_otvoren_dolg === |
| | 45 | |
| | 46 | {{{ |
| | 47 | CREATE OR REPLACE FUNCTION fn_otvoren_dolg( |
| | 48 | p_user_id INTEGER, |
| | 49 | p_vid_valuta_id INTEGER |
| | 50 | ) |
| | 51 | RETURNS NUMERIC |
| | 52 | LANGUAGE sql |
| | 53 | STABLE |
| | 54 | AS $$ |
| | 55 | SELECT COALESCE(SUM(fo.otvorena_suma), 0) |
| | 56 | FROM fin_finansiska_obvrska fo |
| | 57 | WHERE fo.user_id = p_user_id |
| | 58 | AND fo.vid_valuta_id = p_vid_valuta_id |
| | 59 | AND fo.otvorena_suma > 0 |
| | 60 | AND fo.status_finansiska_obvrska_id IN |
| | 61 | (SELECT id FROM fin_status_finansiska_obvrska WHERE ime IN ('Izdadena', 'Delumno platena')); |
| | 62 | $$; |
| | 63 | }}} |
| | 64 | |
| | 65 | Функцијата `fn_otvoren_dolg` ја враќа вкупната отворена сума на корисник во дадена валута. Прима `p_user_id` и `p_vid_valuta_id`, и собира `otvorena_suma` само од обврските во статус `Izdadena` или `Delumno platena`, враќајќи 0 ако нема такви. |
| | 66 | |
| | 67 | Истата проверка ја користат повеќе места — блокада при упис, опомени и работа на шалтер — па е издвоена како функција со повратна вредност што може да се вгради дури и во услов (`WHERE fn_otvoren_dolg(...) > 0`). Како процедура би морала да враќа преку OUT-параметар и не би можела да се користи на тој начин. |
| | 68 | |
| | 69 | === 3. fn_slobodni_mesta === |
| | 70 | |
| | 71 | {{{ |
| | 72 | CREATE OR REPLACE FUNCTION fn_slobodni_mesta(p_kurs_realizacija_id INTEGER) |
| | 73 | RETURNS INTEGER |
| | 74 | LANGUAGE sql |
| | 75 | STABLE |
| | 76 | AS $$ |
| | 77 | SELECT kr.kapacitet_studenti |
| | 78 | - (SELECT COUNT(*)::int |
| | 79 | FROM sm_student_upisan_kurs suk |
| | 80 | WHERE suk.kurs_realizacija_id = kr.id |
| | 81 | AND suk.status_slushanje = 'UPISAN') |
| | 82 | FROM sm_kurs_realizacija kr |
| | 83 | WHERE kr.id = p_kurs_realizacija_id; |
| | 84 | $$; |
| | 85 | }}} |
| | 86 | |
| | 87 | Функцијата `fn_slobodni_mesta` го враќа бројот на слободни места на дадена реализација на курс — `kapacitet_studenti` намалено за бројот на активни (`UPISAN`) уписи. Доколку капацитетот е `NULL`, тоа значи неограничен број места. |
| | 88 | |
| | 89 | Се прикажува при пребарување курсеви и се користи внатре во процедурата `pr_zapisi_student_na_kurs`, па затоа е функција што враќа вредност. Контролата на капацитетот ја спроведува тригерот `trg_kapacitet_kurs`; функцијата само информира, додека тригерот е тој што го гарантира ограничувањето. |
| | 90 | |
| | 91 | == Процедури == |
| | 92 | |
| | 93 | Процедурите се користат за извршување деловни операции врз базата кои опфаќаат повеќе чекори и табели, се повикуваат експлицитно со `CALL` и единствени смеат да користат `COMMIT` / `ROLLBACK` внатре. |
| | 94 | |
| | 95 | === 1. pr_registriraj_naplata === |
| | 96 | |
| | 97 | {{{ |
| | 98 | CREATE OR REPLACE PROCEDURE pr_registriraj_naplata( |
| | 99 | p_user_id INTEGER, |
| | 100 | p_suma NUMERIC, |
| | 101 | p_vid_valuta_id INTEGER, |
| | 102 | p_organizaciona_edinica_id INTEGER, |
| | 103 | p_referenca VARCHAR DEFAULT NULL, |
| | 104 | p_korisnik_naplata_id INTEGER DEFAULT NULL |
| | 105 | ) |
| | 106 | LANGUAGE plpgsql |
| | 107 | AS $$ |
| | 108 | DECLARE |
| | 109 | v_naplata_id INTEGER; |
| | 110 | v_smetka_id INTEGER; |
| | 111 | v_ostatok NUMERIC := p_suma; |
| | 112 | v_obvrska RECORD; |
| | 113 | v_alocirano NUMERIC; |
| | 114 | BEGIN |
| | 115 | IF p_suma <= 0 THEN |
| | 116 | RAISE EXCEPTION 'Сумата мора да биде позитивна (добиено: %)', p_suma; |
| | 117 | END IF; |
| | 118 | |
| | 119 | -- активна трансакциска сметка на организационата единица (приоритет: иста валута) |
| | 120 | SELECT oes.transakciska_smetka_id |
| | 121 | INTO v_smetka_id |
| | 122 | FROM or_organizaciona_edinica_smetki oes |
| | 123 | JOIN fin_transakciski_smetki ts ON oes.transakciska_smetka_id = ts.id |
| | 124 | WHERE oes.organizaciona_edinica_id = p_organizaciona_edinica_id |
| | 125 | AND oes.aktivna |
| | 126 | AND ts.aktivna |
| | 127 | ORDER BY (ts.vid_valuta_id = p_vid_valuta_id) DESC, ts.id |
| | 128 | LIMIT 1; |
| | 129 | |
| | 130 | IF v_smetka_id IS NULL THEN |
| | 131 | RAISE EXCEPTION 'Организационата единица % нема активна трансакциска сметка', |
| | 132 | p_organizaciona_edinica_id; |
| | 133 | END IF; |
| | 134 | |
| | 135 | INSERT INTO fin_naplati (user_id, korisnik_naplata_id, transakciska_smetka_id, |
| | 136 | organizaciona_edinica_id, naplatena_suma, vid_valuta_id, |
| | 137 | status_transakcija_id, nadvoresna_referenca, |
| | 138 | requested_at, confirmed_at, data) |
| | 139 | VALUES (p_user_id, COALESCE(p_korisnik_naplata_id, p_user_id), v_smetka_id, |
| | 140 | p_organizaciona_edinica_id, p_suma, p_vid_valuta_id, |
| | 141 | (SELECT id FROM fin_status_transakcija WHERE ime_status = 'ACCEPTED'), |
| | 142 | p_referenca, now(), now(), now()) |
| | 143 | RETURNING id INTO v_naplata_id; |
| | 144 | |
| | 145 | FOR v_obvrska IN |
| | 146 | SELECT fo.id, fo.otvorena_suma |
| | 147 | FROM fin_finansiska_obvrska fo |
| | 148 | WHERE fo.user_id = p_user_id |
| | 149 | AND fo.vid_valuta_id = p_vid_valuta_id |
| | 150 | AND fo.otvorena_suma > 0 |
| | 151 | ORDER BY fo.datum_dospevanje NULLS LAST, fo.id |
| | 152 | FOR UPDATE |
| | 153 | LOOP |
| | 154 | EXIT WHEN v_ostatok <= 0; |
| | 155 | v_alocirano := LEAST(v_ostatok, v_obvrska.otvorena_suma); |
| | 156 | INSERT INTO fin_naplata_alokacija (naplata_id, finansiska_obvrska_id, |
| | 157 | alocirana_suma, created_at) |
| | 158 | VALUES (v_naplata_id, v_obvrska.id, v_alocirano, now()); |
| | 159 | v_ostatok := v_ostatok - v_alocirano; |
| | 160 | END LOOP; |
| | 161 | |
| | 162 | IF v_ostatok > 0 THEN |
| | 163 | RAISE NOTICE 'Наплата %: % останува неалоцирано (преплата).', v_naplata_id, v_ostatok; |
| | 164 | END IF; |
| | 165 | END; |
| | 166 | $$; |
| | 167 | }}} |
| | 168 | |
| | 169 | Процедурата `pr_registriraj_naplata` регистрира наплата и автоматски ја алоцира кон отворените обврски на корисникот. Откако ќе валидира дека сумата е позитивна и ќе ја најде активната трансакциска сметка на организационата единица (со приоритет на иста валута), внесува запис во `fin_naplati`, па во циклус ги покрива обврските по редослед на најстаро доспевање, внесувајќи редови во `fin_naplata_alokacija`. Ако остане непокриен дел (преплата), тој останува неалоциран и се пријавува со `NOTICE`. |
| | 170 | |
| | 171 | Ова е деловна операција составена од еден INSERT во `fin_naplati` и повеќе INSERT-и во `fin_naplata_alokacija`, која се извршува атомски и не враќа вредност за прашалник — затоа е процедура, а не функција. Намерно не ја менува `otvorena_suma`: тоа го прави тригерот `trg_azuriraj_otvorena_suma`, така што изведената вредност останува конзистентна без оглед дали алокацијата ја внела оваа процедура, друга апликација или рачен SQL. Процедурата одлучува како се распределува наплатата, а тригерот гарантира книговодствена конзистентност. |
| | 172 | |
| | 173 | === 2. pr_zapisi_student_na_kurs === |
| | 174 | |
| | 175 | {{{ |
| | 176 | CREATE OR REPLACE PROCEDURE pr_zapisi_student_na_kurs( |
| | 177 | p_student_id INTEGER, |
| | 178 | p_kurs_realizacija_id INTEGER |
| | 179 | ) |
| | 180 | LANGUAGE plpgsql |
| | 181 | AS $$ |
| | 182 | DECLARE |
| | 183 | v_ssu_id INTEGER; |
| | 184 | v_user_id INTEGER; |
| | 185 | v_dolg NUMERIC; |
| | 186 | v_valuta INTEGER; |
| | 187 | v_slobodni INTEGER; |
| | 188 | BEGIN |
| | 189 | SELECT ssu.id |
| | 190 | INTO v_ssu_id |
| | 191 | FROM sm_student_semestar_upis ssu |
| | 192 | JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id |
| | 193 | JOIN sm_kurs_realizacija kr ON kr.id = p_kurs_realizacija_id |
| | 194 | WHERE ss.student_id = p_student_id |
| | 195 | AND ssu.akademski_semestar_id = kr.akademski_semestar_id |
| | 196 | ORDER BY ssu.datum_upis DESC |
| | 197 | LIMIT 1; |
| | 198 | |
| | 199 | IF v_ssu_id IS NULL THEN |
| | 200 | RAISE EXCEPTION 'Студентот % нема упис во семестарот на курсот %', |
| | 201 | p_student_id, p_kurs_realizacija_id; |
| | 202 | END IF; |
| | 203 | |
| | 204 | IF EXISTS (SELECT 1 |
| | 205 | FROM sm_student_upisan_kurs |
| | 206 | WHERE student_semestar_upis_id = v_ssu_id |
| | 207 | AND kurs_realizacija_id = p_kurs_realizacija_id) THEN |
| | 208 | RAISE EXCEPTION 'Студентот % е веќе запишан на курсот %', |
| | 209 | p_student_id, p_kurs_realizacija_id; |
| | 210 | END IF; |
| | 211 | |
| | 212 | SELECT s.user_id |
| | 213 | INTO v_user_id |
| | 214 | FROM sm_studenti s |
| | 215 | WHERE s.id = p_student_id; |
| | 216 | |
| | 217 | FOR v_valuta IN |
| | 218 | SELECT DISTINCT vid_valuta_id |
| | 219 | FROM fin_finansiska_obvrska |
| | 220 | WHERE user_id = v_user_id |
| | 221 | AND otvorena_suma > 0 |
| | 222 | LOOP |
| | 223 | v_dolg := fn_otvoren_dolg(v_user_id, v_valuta); |
| | 224 | IF v_dolg > 0 THEN |
| | 225 | RAISE EXCEPTION 'Студентот % има отворен долг % (валута %) — уписот е блокиран', |
| | 226 | p_student_id, v_dolg, v_valuta; |
| | 227 | END IF; |
| | 228 | END LOOP; |
| | 229 | |
| | 230 | v_slobodni := fn_slobodni_mesta(p_kurs_realizacija_id); |
| | 231 | IF v_slobodni IS NOT NULL AND v_slobodni <= 0 THEN |
| | 232 | RAISE EXCEPTION 'Нема слободни места на курсот %', p_kurs_realizacija_id; |
| | 233 | END IF; |
| | 234 | |
| | 235 | INSERT INTO sm_student_upisan_kurs (student_semestar_upis_id, kurs_realizacija_id, status_slushanje) |
| | 236 | VALUES (v_ssu_id, p_kurs_realizacija_id, 'UPISAN'); |
| | 237 | END; |
| | 238 | $$; |
| | 239 | }}} |
| | 240 | |
| | 241 | Процедурата `pr_zapisi_student_na_kurs` запишува студент на курс. Го наоѓа семестралниот упис на студентот за семестарот на курсот, одбива дупликат упис, одбива ако студентот има отворен долг (преку `fn_otvoren_dolg`) и одбива ако нема слободни места (преку `fn_slobodni_mesta`), а потоа внесува запис во `sm_student_upisan_kurs` со статус `UPISAN`. |
| | 242 | |
| | 243 | Повеќечекорната валидациона секвенца со завршно запишување, повикана експлицитно од акција на корисникот, ја прави процедура; таа воедно демонстрира компонирање бидејќи ги повикува функциите `fn_otvoren_dolg` и `fn_slobodni_mesta`. Проверката на долг е политика на уписот, а не интегритет на податоците (администратор може по исклучок да запише студент со долг преку директен INSERT), па затоа не е тригер; капацитетот, напротив, е физичко ограничување и е заштитен со тригер. |
| | 244 | |
| | 245 | === 3. pr_generiraj_obvrski_za_semestar === |
| | 246 | |
| | 247 | {{{ |
| | 248 | CREATE OR REPLACE PROCEDURE pr_generiraj_obvrski_za_semestar(p_akademski_semestar_id INTEGER) |
| | 249 | LANGUAGE plpgsql |
| | 250 | AS $$ |
| | 251 | DECLARE |
| | 252 | v_cenovnik RECORD; |
| | 253 | v_oe INTEGER; |
| | 254 | v_opis VARCHAR; |
| | 255 | v_student RECORD; |
| | 256 | v_brojac INTEGER := 0; |
| | 257 | BEGIN |
| | 258 | SELECT asem.organizaciona_edinica_id, |
| | 259 | c.id AS cenovnik_id, |
| | 260 | c.iznos, |
| | 261 | c.vid_valuta_id, |
| | 262 | asem.tip_naplata_organizaciona_edinica_id |
| | 263 | INTO v_cenovnik |
| | 264 | FROM sm_akademski_semestar asem |
| | 265 | JOIN fin_tip_naplati_cenovnik c |
| | 266 | ON c.tip_naplata_organizaciona_edinica_id = asem.tip_naplata_organizaciona_edinica_id |
| | 267 | AND c.aktivna |
| | 268 | WHERE asem.id = p_akademski_semestar_id |
| | 269 | ORDER BY c.prioritet |
| | 270 | LIMIT 1; |
| | 271 | |
| | 272 | IF v_cenovnik IS NULL THEN |
| | 273 | RAISE EXCEPTION 'Нема активен ценовник за семестар %', p_akademski_semestar_id; |
| | 274 | END IF; |
| | 275 | |
| | 276 | v_oe := v_cenovnik.organizaciona_edinica_id; |
| | 277 | v_opis := 'Школарина семестар ' || p_akademski_semestar_id; |
| | 278 | |
| | 279 | FOR v_student IN |
| | 280 | SELECT DISTINCT s.user_id |
| | 281 | FROM sm_student_semestar_upis ssu |
| | 282 | JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id |
| | 283 | JOIN sm_studenti s ON ss.student_id = s.id |
| | 284 | WHERE ssu.akademski_semestar_id = p_akademski_semestar_id |
| | 285 | AND NOT EXISTS (SELECT 1 |
| | 286 | FROM fin_finansiska_obvrska fo |
| | 287 | WHERE fo.user_id = s.user_id |
| | 288 | AND fo.opis = 'Школарина семестар ' || p_akademski_semestar_id) |
| | 289 | LOOP |
| | 290 | INSERT INTO fin_finansiska_obvrska (user_id, organizaciona_edinica_id, |
| | 291 | tip_naplata_organizaciona_edinica_id, |
| | 292 | tip_naplati_cenovnik_id, |
| | 293 | status_finansiska_obvrska_id, |
| | 294 | datum_kreiranje, datum_dospevanje, |
| | 295 | originalna_suma, otvorena_suma, |
| | 296 | vid_valuta_id, opis) |
| | 297 | VALUES (v_student.user_id, v_oe, v_cenovnik.tip_naplata_organizaciona_edinica_id, |
| | 298 | v_cenovnik.cenovnik_id, |
| | 299 | (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Izdadena'), |
| | 300 | now(), CURRENT_DATE + 30, v_cenovnik.iznos, v_cenovnik.iznos, |
| | 301 | v_cenovnik.vid_valuta_id, v_opis); |
| | 302 | v_brojac := v_brojac + 1; |
| | 303 | IF v_brojac % 1000 = 0 THEN |
| | 304 | COMMIT; -- дозволено само во процедура |
| | 305 | END IF; |
| | 306 | END LOOP; |
| | 307 | |
| | 308 | RAISE NOTICE 'Генерирани % обврски за семестар %', v_brojac, p_akademski_semestar_id; |
| | 309 | END; |
| | 310 | $$; |
| | 311 | }}} |
| | 312 | |
| | 313 | Процедурата `pr_generiraj_obvrski_za_semestar` сериски генерира обврски за школарина за сите студенти запишани во даден академски семестар, според активниот ценовник на семестарот. Прескокнува студенти кои веќе имаат таква обврска (идемпотентно според `opis`), а на секои 1000 внесени редови прави `COMMIT`. |
| | 314 | |
| | 315 | Ова е единствената рутина што мора да биде процедура: периодичниот `COMMIT` врз batch од десетици илјади редови е дозволен само во процедура — функциите и тригерите во PostgreSQL не смеат да завршуваат трансакции. |
| | 316 | |
| | 317 | == Тригери == |
| | 318 | |
| | 319 | Тригерите се користат за автоматско извршување логика при внесување или промена на податоци во табелите. Тие се единствениот механизам кој гарантира дека правилото важи без разлика кој и како пишува во табелата — апликација, процедура или рачен INSERT. |
| | 320 | |
| | 321 | === 1. trg_validna_ocena === |
| | 322 | |
| | 323 | {{{ |
| | 324 | CREATE OR REPLACE FUNCTION trgf_validna_ocena() |
| | 325 | RETURNS TRIGGER |
| | 326 | LANGUAGE plpgsql |
| | 327 | AS $$ |
| | 328 | DECLARE |
| | 329 | v_status VARCHAR; |
| | 330 | BEGIN |
| | 331 | SELECT status_slushanje |
| | 332 | INTO v_status |
| | 333 | FROM sm_student_upisan_kurs |
| | 334 | WHERE id = NEW.student_upisan_kurs_id; |
| | 335 | |
| | 336 | IF v_status IS DISTINCT FROM 'UPISAN' THEN |
| | 337 | RAISE EXCEPTION 'Оценка може да се внесе само за активен упис (статус: %)', v_status; |
| | 338 | END IF; |
| | 339 | |
| | 340 | IF NEW.osvoeni_poeni IS NOT NULL AND (NEW.osvoeni_poeni < 0 OR NEW.osvoeni_poeni > 100) THEN |
| | 341 | RAISE EXCEPTION 'Освоени поени мора да бидат 0–100 (добиено: %)', NEW.osvoeni_poeni; |
| | 342 | END IF; |
| | 343 | |
| | 344 | IF NEW.datum_zapis > now() THEN |
| | 345 | RAISE EXCEPTION 'Датумот на запис не смее да биде во иднина'; |
| | 346 | END IF; |
| | 347 | |
| | 348 | RETURN NEW; |
| | 349 | END; |
| | 350 | $$; |
| | 351 | |
| | 352 | DROP TRIGGER IF EXISTS trg_validna_ocena ON sm_student_ocena_zapis; |
| | 353 | CREATE TRIGGER trg_validna_ocena |
| | 354 | BEFORE INSERT ON sm_student_ocena_zapis |
| | 355 | FOR EACH ROW |
| | 356 | EXECUTE FUNCTION trgf_validna_ocena(); |
| | 357 | }}} |
| | 358 | |
| | 359 | Тригерот `trg_validna_ocena` се активира пред внесување оценка во `sm_student_ocena_zapis`. Тој одбива оценка ако уписот не е во статус `UPISAN`, ако освоените поени се надвор од опсегот 0–100, или ако датумот на запис е во иднина. |
| | 360 | |
| | 361 | Реализиран е како тригер бидејќи станува збор за интегритет на податоците — невалидна оценка е невалидна без оглед кој ја внесува (апликација, import или рачен INSERT), што процедура не би го гарантирала. CHECK-ограничување не е доволно бидејќи правилото за статусот бара поглед во друга табела (`sm_student_upisan_kurs`). |
| | 362 | |
| | 363 | === 2. trg_azuriraj_otvorena_suma === |
| | 364 | |
| | 365 | {{{ |
| | 366 | CREATE OR REPLACE FUNCTION trgf_azuriraj_otvorena_suma() |
| | 367 | RETURNS TRIGGER |
| | 368 | LANGUAGE plpgsql |
| | 369 | AS $$ |
| | 370 | DECLARE |
| | 371 | v_nova NUMERIC; |
| | 372 | BEGIN |
| | 373 | SELECT otvorena_suma - NEW.alocirana_suma |
| | 374 | INTO v_nova |
| | 375 | FROM fin_finansiska_obvrska |
| | 376 | WHERE id = NEW.finansiska_obvrska_id |
| | 377 | FOR UPDATE; |
| | 378 | |
| | 379 | IF v_nova < 0 THEN |
| | 380 | RAISE EXCEPTION 'Алокацијата (%) ја надминува отворената сума на обврската %', |
| | 381 | NEW.alocirana_suma, NEW.finansiska_obvrska_id; |
| | 382 | END IF; |
| | 383 | |
| | 384 | UPDATE fin_finansiska_obvrska |
| | 385 | SET otvorena_suma = v_nova, |
| | 386 | status_finansiska_obvrska_id = CASE |
| | 387 | WHEN v_nova = 0 THEN |
| | 388 | (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Platena') |
| | 389 | ELSE |
| | 390 | (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Delumno platena') |
| | 391 | END |
| | 392 | WHERE id = NEW.finansiska_obvrska_id; |
| | 393 | |
| | 394 | RETURN NEW; |
| | 395 | END; |
| | 396 | $$; |
| | 397 | |
| | 398 | DROP TRIGGER IF EXISTS trg_azuriraj_otvorena_suma ON fin_naplata_alokacija; |
| | 399 | CREATE TRIGGER trg_azuriraj_otvorena_suma |
| | 400 | AFTER INSERT ON fin_naplata_alokacija |
| | 401 | FOR EACH ROW |
| | 402 | EXECUTE FUNCTION trgf_azuriraj_otvorena_suma(); |
| | 403 | }}} |
| | 404 | |
| | 405 | Тригерот `trg_azuriraj_otvorena_suma` се активира по внесување алокација во `fin_naplata_alokacija`. Тој ја намалува `otvorena_suma` на соодветната обврска за алоцираната сума, одбива алокација што би ја однела сумата под нула, и автоматски го поставува статусот на `Platena` (кога ќе достигне нула) или `Delumno platena`. |
| | 406 | |
| | 407 | `otvorena_suma` е изведена вредност што мора да се ажурира секогаш кога ќе настане алокација, па е врзана за настанот преку тригер, а не за повикувачот. Кога би била дел од процедурата `pr_registriraj_naplata`, секој иден писател на алокации (на пр. модул за поврат или миграција) би морал да не заборави да ја ажурира. Тригерот е `AFTER INSERT` затоа што редот прво мора да постои како валиден, а ажурирањето на другата табела е последица. |
| | 408 | |
| | 409 | === 3. trg_kapacitet_kurs === |
| | 410 | |
| | 411 | {{{ |
| | 412 | CREATE OR REPLACE FUNCTION trgf_kapacitet_kurs() |
| | 413 | RETURNS TRIGGER |
| | 414 | LANGUAGE plpgsql |
| | 415 | AS $$ |
| | 416 | DECLARE |
| | 417 | v_kapacitet INTEGER; |
| | 418 | v_upisani INTEGER; |
| | 419 | BEGIN |
| | 420 | IF NEW.status_slushanje <> 'UPISAN' THEN |
| | 421 | RETURN NEW; |
| | 422 | END IF; |
| | 423 | |
| | 424 | SELECT kapacitet_studenti |
| | 425 | INTO v_kapacitet |
| | 426 | FROM sm_kurs_realizacija |
| | 427 | WHERE id = NEW.kurs_realizacija_id |
| | 428 | FOR UPDATE; -- сериализира конкурентни уписи на иста реализација |
| | 429 | |
| | 430 | IF v_kapacitet IS NULL THEN |
| | 431 | RETURN NEW; -- неограничен капацитет |
| | 432 | END IF; |
| | 433 | |
| | 434 | SELECT COUNT(*) |
| | 435 | INTO v_upisani |
| | 436 | FROM sm_student_upisan_kurs |
| | 437 | WHERE kurs_realizacija_id = NEW.kurs_realizacija_id |
| | 438 | AND status_slushanje = 'UPISAN'; |
| | 439 | |
| | 440 | IF v_upisani >= v_kapacitet THEN |
| | 441 | RAISE EXCEPTION 'Курсот % е полн (капацитет %)', NEW.kurs_realizacija_id, v_kapacitet; |
| | 442 | END IF; |
| | 443 | |
| | 444 | RETURN NEW; |
| | 445 | END; |
| | 446 | $$; |
| | 447 | |
| | 448 | DROP TRIGGER IF EXISTS trg_kapacitet_kurs ON sm_student_upisan_kurs; |
| | 449 | CREATE TRIGGER trg_kapacitet_kurs |
| | 450 | BEFORE INSERT ON sm_student_upisan_kurs |
| | 451 | FOR EACH ROW |
| | 452 | EXECUTE FUNCTION trgf_kapacitet_kurs(); |
| | 453 | }}} |
| | 454 | |
| | 455 | Тригерот `trg_kapacitet_kurs` се активира пред внесување упис во `sm_student_upisan_kurs`. За нов упис со статус `UPISAN` ја заклучува реализацијата (`FOR UPDATE`), го брои бројот на активни уписи и одбива ако е достигнат `kapacitet_studenti` (при што `NULL` значи неограничено). |
| | 456 | |
| | 457 | Капацитетот мора да издржи и конкурентни уписи — двајца студенти што истовремено го земаат последното место. Проверка само во процедура би била race-condition бидејќи двете трансакции би виделе слободно место; тригерот со заклучување на редот на реализацијата ги сериализира таквите инсерти. Процедурата `pr_zapisi_student_na_kurs` сепак повикува `fn_slobodni_mesta` однапред за љубезна порака, но тригерот е последната, непробивна одбрана. |