| | 1 | = DatabaseProgramming = |
| | 2 | |
| | 3 | == Опис == |
| | 4 | |
| | 5 | Покрај основната структура на базата, имплементирана е и дополнителна PostgreSQL логика со функции, процедури и тригери. |
| | 6 | |
| | 7 | Оваа логика се користи за: |
| | 8 | |
| | 9 | * проверка на валидноста на податоците |
| | 10 | * извршување на почести операции над повеќе табели |
| | 11 | * автоматско одржување на историја и timestamps |
| | 12 | * зачувување на интегритетот на податоците |
| | 13 | * спроведување на дел од бизнис правилата на системот |
| | 14 | |
| | 15 | Кодот е поделен во: |
| | 16 | |
| | 17 | * [attachment:functions.sql functions.sql] |
| | 18 | * [attachment:procedures.sql procedures.sql] |
| | 19 | * [attachment:triggers.sql triggers.sql] |
| | 20 | |
| | 21 | == Функции == |
| | 22 | |
| | 23 | Функциите главно се користат како помошни операции кои потоа се повикуваат од процедурите и тригерите. |
| | 24 | |
| | 25 | Едноставен пример е функцијата за добивање на целосното име на корисник: |
| | 26 | |
| | 27 | {{{#!sql |
| | 28 | CREATE OR REPLACE FUNCTION fn_get_full_name(p_user_id int4) |
| | 29 | RETURNS text LANGUAGE sql STABLE AS $$ |
| | 30 | SELECT first_name || ' ' || last_name |
| | 31 | FROM "User" |
| | 32 | WHERE user_id = p_user_id; |
| | 33 | |
| | 34 | $$$; |
| | 35 | }}} |
| | 36 | |
| | 37 | За добивање на vendor-от поврзан со даден проект се користи: |
| | 38 | |
| | 39 | {{{#!sql |
| | 40 | CREATE OR REPLACE FUNCTION fn_get_vendor_id_for_project(p_project_id int4) |
| | 41 | RETURNS int4 LANGUAGE sql STABLE AS $$ |
| | 42 | SELECT cvc.vendor_id |
| | 43 | FROM Project p |
| | 44 | JOIN Client_Vendor_Contract cvc |
| | 45 | ON cvc.contract_id = p.contract_id |
| | 46 | WHERE p.project_id = p_project_id; |
| | 47 | $$; |
| | 48 | }}} |
| | 49 | |
| | 50 | Поголемиот дел од функциите се validation функции. На пример: |
| | 51 | |
| | 52 | {{{#!sql |
| | 53 | CREATE OR REPLACE FUNCTION fn_assert_project_exists(p_project_id int4) |
| | 54 | RETURNS void LANGUAGE plpgsql AS $$ |
| | 55 | BEGIN |
| | 56 | IF NOT EXISTS ( |
| | 57 | SELECT 1 |
| | 58 | FROM Project |
| | 59 | WHERE project_id = p_project_id |
| | 60 | ) THEN |
| | 61 | RAISE EXCEPTION 'Project % does not exist', p_project_id; |
| | 62 | END IF; |
| | 63 | END; |
| | 64 | $$; |
| | 65 | }}} |
| | 66 | |
| | 67 | На истиот принцип се имплементирани проверки за `Vendor`, `Client`, `Review`, `Project_Status`, `Subscription_Tier`, корисничките типови и `Rating_Dimension`. |
| | 68 | |
| | 69 | == Процедури == |
| | 70 | |
| | 71 | Процедурите ги имплементираат главните операции кои менуваат податоци во повеќе поврзани табели. |
| | 72 | |
| | 73 | === Регистрација на корисник === |
| | 74 | |
| | 75 | `sp_register_user` прво креира запис во `"User"`, а потоа го додава корисникот во соодветната subtype табела. |
| | 76 | |
| | 77 | {{{#!sql |
| | 78 | INSERT INTO "User" ( |
| | 79 | type, first_name, last_name, email, password_hash |
| | 80 | ) |
| | 81 | VALUES ( |
| | 82 | p_type, p_first_name, p_last_name, |
| | 83 | p_email, p_password_hash |
| | 84 | ) |
| | 85 | RETURNING user_id INTO p_user_id; |
| | 86 | |
| | 87 | IF p_type = 'client' THEN |
| | 88 | INSERT INTO Client_User (user_id, client_id) |
| | 89 | VALUES (p_user_id, p_client_id); |
| | 90 | |
| | 91 | ELSIF p_type = 'vendor' THEN |
| | 92 | INSERT INTO Vendor_User (user_id, vendor_id) |
| | 93 | VALUES (p_user_id, p_vendor_id); |
| | 94 | |
| | 95 | ELSIF p_type = 'management' THEN |
| | 96 | INSERT INTO Management_User (user_id, role_id) |
| | 97 | VALUES (p_user_id, p_role_id); |
| | 98 | END IF; |
| | 99 | }}} |
| | 100 | |
| | 101 | Пред внесувањето се проверуваат типот на корисникот, задолжителните параметри и референците кон останатите табели. |
| | 102 | |
| | 103 | === Креирање договор и проект === |
| | 104 | |
| | 105 | `sp_create_contract_with_project` овозможува со една операција да се креира договор и неговиот почетен проект: |
| | 106 | |
| | 107 | {{{#!sql |
| | 108 | INSERT INTO Client_Vendor_Contract ( |
| | 109 | client_id, vendor_id, contract_title, |
| | 110 | contract_number, start_date, end_date, |
| | 111 | total_value, currency_code, terms_summary |
| | 112 | ) |
| | 113 | VALUES ( |
| | 114 | p_client_id, p_vendor_id, p_contract_title, |
| | 115 | p_contract_number, p_cvc_start_date, p_cvc_end_date, |
| | 116 | p_total_value, p_currency_code, p_terms_summary |
| | 117 | ) |
| | 118 | RETURNING contract_id INTO p_contract_id; |
| | 119 | |
| | 120 | INSERT INTO Project ( |
| | 121 | contract_id, status_id, project_name, |
| | 122 | start_date, end_date, budget |
| | 123 | ) |
| | 124 | VALUES ( |
| | 125 | p_contract_id, p_status_id, p_project_name, |
| | 126 | p_project_start_date, p_project_end_date, p_budget |
| | 127 | ) |
| | 128 | RETURNING project_id INTO p_project_id; |
| | 129 | }}} |
| | 130 | |
| | 131 | Пред креирањето се проверуваат client, vendor и project status, како и буџетот и потребните текстуални полиња. |
| | 132 | |
| | 133 | === Промена на статус на проект === |
| | 134 | |
| | 135 | `sp_update_project_status` го менува тековниот статус и истовремено додава запис во историјата: |
| | 136 | |
| | 137 | {{{#!sql |
| | 138 | UPDATE Project |
| | 139 | SET status_id = p_new_status_id |
| | 140 | WHERE project_id = p_project_id; |
| | 141 | |
| | 142 | INSERT INTO Project_Status_History ( |
| | 143 | project_id, |
| | 144 | status_id, |
| | 145 | vendor_user_id, |
| | 146 | management_user_id, |
| | 147 | comment |
| | 148 | ) |
| | 149 | VALUES ( |
| | 150 | p_project_id, |
| | 151 | p_new_status_id, |
| | 152 | p_vendor_user_id, |
| | 153 | p_management_user_id, |
| | 154 | p_comment |
| | 155 | ); |
| | 156 | }}} |
| | 157 | |
| | 158 | Со ова секоја промена на статусот останува евидентирана. |
| | 159 | |
| | 160 | === Внесување review === |
| | 161 | |
| | 162 | `sp_submit_review` креира review и неговите оценки во една операција. |
| | 163 | |
| | 164 | Оценките се предаваат како JSONB низа: |
| | 165 | |
| | 166 | {{{#!sql |
| | 167 | FOR v_score IN |
| | 168 | SELECT * FROM jsonb_array_elements(p_scores) |
| | 169 | LOOP |
| | 170 | PERFORM fn_assert_dimension_exists( |
| | 171 | (v_score->>'dimension_id')::int4 |
| | 172 | ); |
| | 173 | |
| | 174 | IF ((v_score->>'score_value')::int4 < 1) |
| | 175 | OR ((v_score->>'score_value')::int4 > 5) THEN |
| | 176 | RAISE EXCEPTION |
| | 177 | 'Score value must be between 1 and 5'; |
| | 178 | END IF; |
| | 179 | |
| | 180 | INSERT INTO Review_Score ( |
| | 181 | review_id, |
| | 182 | dimension_id, |
| | 183 | score_value |
| | 184 | ) |
| | 185 | VALUES ( |
| | 186 | p_review_id, |
| | 187 | (v_score->>'dimension_id')::int4, |
| | 188 | (v_score->>'score_value')::int4 |
| | 189 | ); |
| | 190 | END LOOP; |
| | 191 | }}} |
| | 192 | |
| | 193 | Процедурата дополнително проверува дали корисникот припаѓа на клиентот на проектот и дали за проектот веќе постои review. |
| | 194 | |
| | 195 | Останатите процедури се користат за: |
| | 196 | |
| | 197 | * деактивирање на корисник |
| | 198 | * промена на project budget |
| | 199 | * отворање dispute ticket |
| | 200 | * разрешување dispute ticket |
| | 201 | * објавување review |
| | 202 | * обновување vendor subscription |
| | 203 | |
| | 204 | == Тригери == |
| | 205 | |
| | 206 | Тригерите автоматски извршуваат проверки или дополнителни операции при `INSERT` и `UPDATE`. |
| | 207 | |
| | 208 | === User subtype проверка === |
| | 209 | |
| | 210 | За `Client_User`, `Vendor_User` и `Management_User` се користи заедничка trigger функција која спречува еден корисник да припаѓа на повеќе subtype табели. |
| | 211 | |
| | 212 | {{{#!sql |
| | 213 | CREATE TRIGGER trg_client_user_subtype |
| | 214 | BEFORE INSERT ON Client_User |
| | 215 | FOR EACH ROW |
| | 216 | EXECUTE FUNCTION trg_enforce_user_subtype(); |
| | 217 | |
| | 218 | CREATE TRIGGER trg_vendor_user_subtype |
| | 219 | BEFORE INSERT ON Vendor_User |
| | 220 | FOR EACH ROW |
| | 221 | EXECUTE FUNCTION trg_enforce_user_subtype(); |
| | 222 | |
| | 223 | CREATE TRIGGER trg_management_user_subtype |
| | 224 | BEFORE INSERT ON Management_User |
| | 225 | FOR EACH ROW |
| | 226 | EXECUTE FUNCTION trg_enforce_user_subtype(); |
| | 227 | }}} |
| | 228 | |
| | 229 | Дополнително се проверува дали вредноста на `"User".type` одговара на subtype табелата. |
| | 230 | |
| | 231 | === Автоматско ажурирање на updated_at === |
| | 232 | |
| | 233 | За табелите кои имаат `updated_at` е дефинирана заедничка trigger функција: |
| | 234 | |
| | 235 | {{{#!sql |
| | 236 | CREATE OR REPLACE FUNCTION trg_set_updated_at() |
| | 237 | RETURNS TRIGGER LANGUAGE plpgsql AS $$ |
| | 238 | BEGIN |
| | 239 | NEW.updated_at := NOW(); |
| | 240 | RETURN NEW; |
| | 241 | END; |
| | 242 | $$; |
| | 243 | }}} |
| | 244 | |
| | 245 | На пример: |
| | 246 | |
| | 247 | {{{#!sql |
| | 248 | CREATE TRIGGER trg_project_updated_at |
| | 249 | BEFORE UPDATE ON Project |
| | 250 | FOR EACH ROW |
| | 251 | EXECUTE FUNCTION trg_set_updated_at(); |
| | 252 | }}} |
| | 253 | |
| | 254 | Истиот механизам се користи и за договори, subscriptions, reviews, dispute tickets и корисници. |
| | 255 | |
| | 256 | === Историја на промена на буџет === |
| | 257 | |
| | 258 | При промена на буџетот на проект, автоматски се креира audit запис: |
| | 259 | |
| | 260 | {{{#!sql |
| | 261 | CREATE OR REPLACE FUNCTION trg_capture_budget_change() |
| | 262 | RETURNS TRIGGER LANGUAGE plpgsql AS $$ |
| | 263 | BEGIN |
| | 264 | IF NEW.budget IS DISTINCT FROM OLD.budget THEN |
| | 265 | INSERT INTO Project_Budget_Audit ( |
| | 266 | project_id, |
| | 267 | old_budget, |
| | 268 | new_budget |
| | 269 | ) |
| | 270 | VALUES ( |
| | 271 | OLD.project_id, |
| | 272 | OLD.budget, |
| | 273 | NEW.budget |
| | 274 | ); |
| | 275 | END IF; |
| | 276 | |
| | 277 | RETURN NEW; |
| | 278 | END; |
| | 279 | $$; |
| | 280 | }}} |
| | 281 | |
| | 282 | Тригерот се активира после промена на `Project`: |
| | 283 | |
| | 284 | {{{#!sql |
| | 285 | CREATE TRIGGER trg_project_budget_audit |
| | 286 | AFTER UPDATE ON Project |
| | 287 | FOR EACH ROW |
| | 288 | EXECUTE FUNCTION trg_capture_budget_change(); |
| | 289 | }}} |
| | 290 | |
| | 291 | === Проверка на dispute и review === |
| | 292 | |
| | 293 | При разрешување на dispute се проверува дали е назначен management корисник. Доколку `resolved_at` не е поставен, автоматски се поставува тековното време. |
| | 294 | |
| | 295 | {{{#!sql |
| | 296 | IF NEW.is_resolved = true AND OLD.is_resolved = false THEN |
| | 297 | IF NEW.assigned_management_user_id IS NULL THEN |
| | 298 | RAISE EXCEPTION |
| | 299 | 'Dispute ticket % must have an assigned management user before it can be resolved', |
| | 300 | OLD.ticket_id; |
| | 301 | END IF; |
| | 302 | |
| | 303 | IF NEW.resolved_at IS NULL THEN |
| | 304 | NEW.resolved_at := NOW(); |
| | 305 | END IF; |
| | 306 | END IF; |
| | 307 | }}} |
| | 308 | |
| | 309 | Дополнително, review не може да биде објавен додека има неразрешен dispute: |
| | 310 | |
| | 311 | {{{#!sql |
| | 312 | IF EXISTS ( |
| | 313 | SELECT 1 |
| | 314 | FROM Dispute_Ticket |
| | 315 | WHERE review_id = NEW.review_id |
| | 316 | AND is_resolved = false |
| | 317 | ) THEN |
| | 318 | RAISE EXCEPTION |
| | 319 | 'Review % cannot be published while it has unresolved dispute tickets', |
| | 320 | NEW.review_id; |
| | 321 | END IF; |
| | 322 | }}} |
| | 323 | |
| | 324 | == Заклучок == |
| | 325 | |
| | 326 | Функциите се користат за повторно употребливи проверки и помошни операции, процедурите ги имплементираат главните операции над податоците, а тригерите автоматски ги применуваат правилата кои треба да важат независно од начинот на кој се менуваат податоците. |
| | 327 | |
| | 328 | Со ова дел од апликациската и validation логиката е имплементирана директно во PostgreSQL базата. |
| | 329 | $$$ |