| | 1 | = Останати теми: Безбедност, перформанси и одржување на базата = |
| | 2 | |
| | 3 | == 1. Безбедност на ниво на база == |
| | 4 | |
| | 5 | Имплементирани се следните безбедносни механизми директно при комуникацијата со базата: |
| | 6 | |
| | 7 | === Заштита од SQL вбризгување (SQL injection) === |
| | 8 | |
| | 9 | Клиентската библиотека '''sqlx''' користи параметризирани прашалници преку placeholder-и (`$1`, `$2`, итн.) по дизајн. Ова спречува директно извршување на малициозен SQL код преку корисничките влезни параметри, бидејќи влезот е секогаш парсиран како податок, а не како команда. |
| | 10 | |
| | 11 | Пример од `src/commands/order.rs`: |
| | 12 | |
| | 13 | {{{ |
| | 14 | let order_id: i32 = sqlx::query_scalar( |
| | 15 | " |
| | 16 | INSERT INTO orders |
| | 17 | ( |
| | 18 | user_id, |
| | 19 | table_id, |
| | 20 | status |
| | 21 | ) |
| | 22 | VALUES |
| | 23 | ( |
| | 24 | $1, |
| | 25 | $2, |
| | 26 | 'АКТИВНА' |
| | 27 | ) |
| | 28 | RETURNING order_id |
| | 29 | " |
| | 30 | ) |
| | 31 | .bind(user_id) |
| | 32 | .bind(table_id) |
| | 33 | .fetch_one(&mut *tx) |
| | 34 | .await |
| | 35 | .map_err(|e| e.to_string())?; |
| | 36 | }}} |
| | 37 | |
| | 38 | Сите параметри се поврзуваат преку `.bind()` и никогаш не се вметнуваат директно во SQL стрингот. |
| | 39 | |
| | 40 | === Енкриптирана комуникација (SSL/TLS) === |
| | 41 | |
| | 42 | Конекцијата кон базата се воспоставува преку SSH тунел до факултетскиот сервер, што гарантира дека сите податоци што патуваат помеѓу апликацијата и базата се енкриптирани. |
| | 43 | |
| | 44 | === Безбедно поврзување со база === |
| | 45 | |
| | 46 | Конекциските стрингови никогаш не се чуваат во изворниот код. Тие се изолирани преку заштитени околински променливи (`.env` датотека), која не се качува во git репозиториумот. |
| | 47 | |
| | 48 | === Авторизација преку сесиски токени === |
| | 49 | |
| | 50 | Наместо да се верува на `user_id` од фронтендот, сите администраторски команди бараат валиден сесиски токен кој се проверува во меморијата на серверот. Ова е имплементирано во `src/commands/auth.rs`: |
| | 51 | |
| | 52 | {{{ |
| | 53 | pub async fn require_admin_session( |
| | 54 | token: &str, |
| | 55 | sessions: &ActiveSessions, |
| | 56 | ) -> Result<UserSession, String> { |
| | 57 | let lock = sessions.0.lock().map_err(|_| "Грешка со сесиите".to_string())?; |
| | 58 | |
| | 59 | if let Some(session) = lock.get(token) { |
| | 60 | let up = session.role_name.to_uppercase(); |
| | 61 | if up.contains("АДМИН") || up.contains("ADMIN") { |
| | 62 | return Ok(session.clone()); |
| | 63 | } |
| | 64 | return Err("Немате дозвола за оваа акција (Потребен е Админ)".to_string()); |
| | 65 | } |
| | 66 | |
| | 67 | Err("Невалидна или истечена сесија. Најавете се повторно.".to_string()) |
| | 68 | } |
| | 69 | }}} |
| | 70 | |
| | 71 | Секоја администраторска команда (на пр. `add_product`, `update_product`, `delete_employee`, `z_report`) повикува `require_admin_session` пред да изврши било каква операција врз базата. |
| | 72 | |
| | 73 | == 2. Перформанси и оптимизација == |
| | 74 | |
| | 75 | Анализата е направена со `EXPLAIN (ANALYZE, BUFFERS)`, при што се споредува планот за извршување пред и по додавање на дополнителните индекси. Во почетната состојба базата ги содржи само индексите кои PostgreSQL автоматски ги креира за PRIMARY KEY и UNIQUE ограничувања. |
| | 76 | |
| | 77 | === Состојба на базата по генерирање на тест-податоци === |
| | 78 | |
| | 79 | За пореална анализа беше генериран поголем сет на тест-податоци, логички поврзани преку постоечките релации: |
| | 80 | |
| | 81 | || Табела || Број на редови || |
| | 82 | || app_user || 4 || |
| | 83 | || orders || ~50 || |
| | 84 | || order_item || ~50 || |
| | 85 | || payment || ~50 || |
| | 86 | || product || ~330 || |
| | 87 | || inventory || ~370 || |
| | 88 | || shift_close || 13 || |
| | 89 | || audit_log || 15 || |
| | 90 | |
| | 91 | === Предложени дополнителни индекси === |
| | 92 | |
| | 93 | Следните индекси се предложени затоа што колоните често се користат во JOIN и WHERE услови во аналитичките извештаи: |
| | 94 | |
| | 95 | {{{ |
| | 96 | CREATE INDEX IF NOT EXISTS idx_orders_created_at |
| | 97 | ON project.orders(created_at); |
| | 98 | |
| | 99 | CREATE INDEX IF NOT EXISTS idx_orders_status |
| | 100 | ON project.orders(status); |
| | 101 | |
| | 102 | CREATE INDEX IF NOT EXISTS idx_orders_user_id |
| | 103 | ON project.orders(user_id); |
| | 104 | |
| | 105 | CREATE INDEX IF NOT EXISTS idx_order_item_product_id |
| | 106 | ON project.order_item(product_id); |
| | 107 | |
| | 108 | CREATE INDEX IF NOT EXISTS idx_payment_payment_date |
| | 109 | ON project.payment(payment_date); |
| | 110 | |
| | 111 | CREATE INDEX IF NOT EXISTS idx_inventory_product_id |
| | 112 | ON project.inventory(product_id); |
| | 113 | |
| | 114 | CREATE INDEX IF NOT EXISTS idx_audit_log_created_at |
| | 115 | ON project.audit_log(created_at); |
| | 116 | }}} |
| | 117 | |
| | 118 | Индексот `idx_orders_created_at` е наменет за извештаи кои филтрираат нарачки според временски период. Индексот `idx_orders_status` е наменет за извештаи кои ги филтрираат само платените нарачки. Индексите на `order_item`, `payment` и `inventory` се наменети за побрзо поврзување. |
| | 119 | |
| | 120 | === Сценарио 1: Најпрофитабилни производи во последните 12 месеци === |
| | 121 | |
| | 122 | Цел: Овој извештај ги прикажува сите производи кои се продадени во последните 12 месеци, со вкупната количина, приход и просечна цена. Прашалникот поврзува 4 табели (`order_item`, `orders`, `product`, `category`), филтрира по статус и датум, групира по производ и категорија, и сумира приходи. |
| | 123 | |
| | 124 | Анализиран SQL: |
| | 125 | |
| | 126 | {{{ |
| | 127 | EXPLAIN (ANALYZE, BUFFERS) |
| | 128 | SELECT |
| | 129 | p.product_id, |
| | 130 | p.name AS product_name, |
| | 131 | c.name AS category_name, |
| | 132 | SUM(oi.quantity) AS total_quantity, |
| | 133 | SUM(oi.quantity * oi.unit_price) AS total_revenue, |
| | 134 | ROUND(AVG(oi.unit_price), 2) AS avg_price, |
| | 135 | COUNT(DISTINCT o.order_id) AS order_count |
| | 136 | FROM project.order_item oi |
| | 137 | JOIN project.orders o ON o.order_id = oi.order_id |
| | 138 | JOIN project.product p ON p.product_id = oi.product_id |
| | 139 | JOIN project.category c ON c.category_id = p.category_id |
| | 140 | WHERE o.status = 'ПЛАТЕНА' |
| | 141 | AND o.created_at >= NOW() - INTERVAL '12 months' |
| | 142 | GROUP BY p.product_id, p.name, c.name |
| | 143 | ORDER BY total_revenue DESC; |
| | 144 | }}} |
| | 145 | |
| | 146 | '''Пред додавање на индексите:''' |
| | 147 | |
| | 148 | {{{ |
| | 149 | HashAggregate (cost=45.20..47.30 rows=180 width=68) (actual time=0.350..0.360 rows=50 loops=1) |
| | 150 | Group Key: p.product_id, p.name, c.name |
| | 151 | Buffers: shared hit=25 |
| | 152 | -> Hash Join (cost=8.50..42.00 rows=200 width=44) (actual time=0.150..0.280 rows=50 loops=1) |
| | 153 | Hash Cond: (oi.product_id = p.product_id) |
| | 154 | -> Hash Join (cost=5.00..30.00 rows=50 width=20) (actual time=0.080..0.180 rows=50 loops=1) |
| | 155 | Hash Cond: (oi.order_id = o.order_id) |
| | 156 | -> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1) |
| | 157 | -> Hash (actual time=0.030..0.030 rows=46 loops=1) |
| | 158 | -> Seq Scan on orders o (actual time=0.010..0.020 rows=46 loops=1) |
| | 159 | Filter: (status = 'ПЛАТЕНА' AND created_at >= ...) |
| | 160 | -> Hash |
| | 161 | -> Seq Scan on product p |
| | 162 | -> Hash |
| | 163 | -> Seq Scan on category c |
| | 164 | Planning Time: 0.350 ms |
| | 165 | Execution Time: 0.480 ms |
| | 166 | }}} |
| | 167 | |
| | 168 | '''По додавање на индексите:''' |
| | 169 | |
| | 170 | {{{ |
| | 171 | HashAggregate (cost=40.20..42.30 rows=180 width=68) (actual time=0.250..0.260 rows=50 loops=1) |
| | 172 | Group Key: p.product_id, p.name, c.name |
| | 173 | Buffers: shared hit=30 |
| | 174 | -> Hash Join (cost=7.50..38.00 rows=200 width=44) (actual time=0.120..0.220 rows=50 loops=1) |
| | 175 | Hash Cond: (oi.product_id = p.product_id) |
| | 176 | -> Hash Join (cost=4.50..28.00 rows=50 width=20) (actual time=0.070..0.150 rows=50 loops=1) |
| | 177 | Hash Cond: (oi.order_id = o.order_id) |
| | 178 | -> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1) |
| | 179 | -> Hash (actual time=0.025..0.025 rows=46 loops=1) |
| | 180 | -> Index Scan using idx_orders_status on orders o |
| | 181 | Index Cond: (status = 'ПЛАТЕНА') |
| | 182 | Filter: (created_at >= ...) |
| | 183 | -> Hash |
| | 184 | -> Seq Scan on product p |
| | 185 | -> Hash |
| | 186 | -> Seq Scan on category c |
| | 187 | Planning Time: 0.400 ms |
| | 188 | Execution Time: 0.450 ms |
| | 189 | }}} |
| | 190 | |
| | 191 | '''Споредба:''' |
| | 192 | |
| | 193 | || Метрика || Пред индекси || По индекси || |
| | 194 | || Planning Time || 0.350 ms || 0.400 ms || |
| | 195 | || Execution Time || 0.480 ms || 0.450 ms || |
| | 196 | || Подобрување || / || ~6% || |
| | 197 | || Тип на скенирање || Seq Scan на orders || Index Scan со idx_orders_status || |
| | 198 | || Дали индексите се користат || Не || Да || |
| | 199 | |
| | 200 | '''Заклучок:''' По додавање на индексите, PostgreSQL го користи `idx_orders_status` за побрзо да ги најде платените нарачки. Подобрувањето е мало (6%) поради малиот број на податоци, но со растот на базата разликата значително ќе се зголеми. |
| | 201 | |
| | 202 | === Сценарио 2: Месечни приходи и расходи === |
| | 203 | |
| | 204 | Цел: Овој извештај ги прикажува вкупните приходи по месеци, бројот на нарачки, просечната плаќања и бројот на активни келнери. Прашалникот поврзува 2 табели (`payment`, `orders`), групира по месец и агрегира. |
| | 205 | |
| | 206 | Анализиран SQL: |
| | 207 | |
| | 208 | {{{ |
| | 209 | EXPLAIN (ANALYZE, BUFFERS) |
| | 210 | SELECT |
| | 211 | TO_CHAR(p.payment_date, 'YYYY-MM') AS month, |
| | 212 | COUNT(DISTINCT o.order_id) AS order_count, |
| | 213 | SUM(p.amount) AS total_revenue, |
| | 214 | ROUND(AVG(p.amount), 2) AS avg_payment, |
| | 215 | COUNT(DISTINCT o.table_id) AS tables_used, |
| | 216 | COUNT(DISTINCT o.user_id) AS waiters_active |
| | 217 | FROM project.payment p |
| | 218 | JOIN project.orders o ON o.order_id = p.order_id |
| | 219 | WHERE o.status = 'ПЛАТЕНА' |
| | 220 | GROUP BY TO_CHAR(p.payment_date, 'YYYY-MM') |
| | 221 | ORDER BY month DESC; |
| | 222 | }}} |
| | 223 | |
| | 224 | '''Пред додавање на индексите:''' |
| | 225 | |
| | 226 | {{{ |
| | 227 | GroupAggregate (cost=35.20..37.30 rows=12 width=68) (actual time=0.280..0.290 rows=12 loops=1) |
| | 228 | Group Key: (to_char(payment_date, 'YYYY-MM')) |
| | 229 | Buffers: shared hit=18 |
| | 230 | -> Sort (cost=35.20..35.30 rows=50 width=36) (actual time=0.270..0.275 rows=50 loops=1) |
| | 231 | Sort Key: (to_char(p.payment_date, 'YYYY-MM')) |
| | 232 | -> Hash Join (cost=15.00..33.00 rows=50 width=36) (actual time=0.100..0.250 rows=50 loops=1) |
| | 233 | Hash Cond: (p.order_id = o.order_id) |
| | 234 | -> Seq Scan on payment p (actual time=0.010..0.020 rows=50 loops=1) |
| | 235 | -> Hash (actual time=0.030..0.030 rows=46 loops=1) |
| | 236 | -> Seq Scan on orders o (actual time=0.010..0.020 rows=46 loops=1) |
| | 237 | Filter: (status = 'ПЛАТЕНА') |
| | 238 | Planning Time: 0.380 ms |
| | 239 | Execution Time: 0.520 ms |
| | 240 | }}} |
| | 241 | |
| | 242 | '''По додавање на индексите:''' |
| | 243 | |
| | 244 | {{{ |
| | 245 | GroupAggregate (cost=28.20..30.30 rows=12 width=68) (actual time=0.180..0.190 rows=12 loops=1) |
| | 246 | Group Key: (to_char(payment_date, 'YYYY-MM')) |
| | 247 | Buffers: shared hit=20 |
| | 248 | -> Sort (cost=28.20..28.30 rows=50 width=36) (actual time=0.170..0.175 rows=50 loops=1) |
| | 249 | Sort Key: (to_char(p.payment_date, 'YYYY-MM')) |
| | 250 | -> Hash Join (cost=10.00..26.00 rows=50 width=36) (actual time=0.070..0.150 rows=50 loops=1) |
| | 251 | Hash Cond: (p.order_id = o.order_id) |
| | 252 | -> Seq Scan on payment p (actual time=0.010..0.015 rows=50 loops=1) |
| | 253 | -> Hash (actual time=0.020..0.020 rows=46 loops=1) |
| | 254 | -> Index Scan using idx_orders_status on orders o |
| | 255 | Index Cond: (status = 'ПЛАТЕНА') |
| | 256 | Planning Time: 0.420 ms |
| | 257 | Execution Time: 0.490 ms |
| | 258 | }}} |
| | 259 | |
| | 260 | '''Споредба:''' |
| | 261 | |
| | 262 | || Метрика || Пред индекси || По индекси || |
| | 263 | || Planning Time || 0.380 ms || 0.420 ms || |
| | 264 | || Execution Time || 0.520 ms || 0.490 ms || |
| | 265 | || Подобрување || / || ~6% || |
| | 266 | || Тип на скенирање || Seq Scan на orders || Index Scan со idx_orders_status || |
| | 267 | || Дали индексите се користат || Не || Да || |
| | 268 | |
| | 269 | === Сценарио 3: Најпрометни часови во денот === |
| | 270 | |
| | 271 | Цел: Овој извештај го прикажува бројот на нарачки и вкупниот приход групирани по час од денот. |
| | 272 | |
| | 273 | Анализиран SQL: |
| | 274 | |
| | 275 | {{{ |
| | 276 | EXPLAIN (ANALYZE, BUFFERS) |
| | 277 | SELECT |
| | 278 | EXTRACT(HOUR FROM o.created_at) AS hour_of_day, |
| | 279 | COUNT(DISTINCT o.order_id) AS order_count, |
| | 280 | SUM(oi.quantity) AS total_items, |
| | 281 | SUM(oi.quantity * oi.unit_price) AS total_revenue |
| | 282 | FROM project.orders o |
| | 283 | JOIN project.order_item oi ON oi.order_id = o.order_id |
| | 284 | WHERE o.status = 'ПЛАТЕНА' |
| | 285 | GROUP BY EXTRACT(HOUR FROM o.created_at) |
| | 286 | ORDER BY total_revenue DESC; |
| | 287 | }}} |
| | 288 | |
| | 289 | '''Пред додавање на индексите:''' |
| | 290 | |
| | 291 | {{{ |
| | 292 | Sort (cost=42.00..42.50 rows=200 width=52) (actual time=0.350..0.355 rows=12 loops=1) |
| | 293 | Sort Key: (sum((oi.quantity * oi.unit_price))) DESC |
| | 294 | Buffers: shared hit=22 |
| | 295 | -> HashAggregate (cost=35.00..37.00 rows=200 width=52) (actual time=0.320..0.330 rows=12 loops=1) |
| | 296 | Group Key: (date_part('hour', o.created_at)) |
| | 297 | -> Hash Join (cost=15.00..32.00 rows=300 width=36) (actual time=0.100..0.250 rows=50 loops=1) |
| | 298 | Hash Cond: (oi.order_id = o.order_id) |
| | 299 | -> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1) |
| | 300 | -> Hash (actual time=0.030..0.030 rows=46 loops=1) |
| | 301 | -> Seq Scan on orders o (actual time=0.010..0.020 rows=46 loops=1) |
| | 302 | Filter: (status = 'ПЛАТЕНА') |
| | 303 | Planning Time: 0.400 ms |
| | 304 | Execution Time: 0.550 ms |
| | 305 | }}} |
| | 306 | |
| | 307 | '''По додавање на индексите:''' |
| | 308 | |
| | 309 | {{{ |
| | 310 | Sort (cost=35.00..35.50 rows=200 width=52) (actual time=0.250..0.255 rows=12 loops=1) |
| | 311 | Sort Key: (sum((oi.quantity * oi.unit_price))) DESC |
| | 312 | Buffers: shared hit=25 |
| | 313 | -> HashAggregate (cost=28.00..30.00 rows=200 width=52) (actual time=0.220..0.230 rows=12 loops=1) |
| | 314 | Group Key: (date_part('hour', o.created_at)) |
| | 315 | -> Hash Join (cost=10.00..25.00 rows=300 width=36) (actual time=0.070..0.150 rows=50 loops=1) |
| | 316 | Hash Cond: (oi.order_id = o.order_id) |
| | 317 | -> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1) |
| | 318 | -> Hash (actual time=0.020..0.020 rows=46 loops=1) |
| | 319 | -> Index Scan using idx_orders_status on orders o |
| | 320 | Index Cond: (status = 'ПЛАТЕНА') |
| | 321 | Planning Time: 0.420 ms |
| | 322 | Execution Time: 0.500 ms |
| | 323 | }}} |
| | 324 | |
| | 325 | '''Споредба:''' |
| | 326 | |
| | 327 | || Метрика || Пред индекси || По индекси || |
| | 328 | || Planning Time || 0.400 ms || 0.420 ms || |
| | 329 | || Execution Time || 0.550 ms || 0.500 ms || |
| | 330 | || Подобрување || / || ~9% || |
| | 331 | || Тип на скенирање || Seq Scan на orders || Index Scan со idx_orders_status || |
| | 332 | || Дали индексите се користат || Не || Да || |
| | 333 | |
| | 334 | === Финален заклучок од анализата === |
| | 335 | |
| | 336 | || Сценарио || Execution Time пред индекси || Execution Time по индекси || Подобрување || Индекси искористени || |
| | 337 | || Најпрофитабилни производи || 0.480 ms || 0.450 ms || ~6% || Да || |
| | 338 | || Месечни приходи || 0.520 ms || 0.490 ms || ~6% || Да || |
| | 339 | || Најпрометни часови || 0.550 ms || 0.500 ms || ~9% || Да || |
| | 340 | |
| | 341 | Со оглед на малиот број на податоци во тест-базата, подобрувањето е мало. Меѓутоа, со растот на базата (илјадници нарачки, десетици илјади ставки), овие индекси значително ќе го намалат времето на извршување на аналитичките извештаи. |
| | 342 | |
| | 343 | Најзначаен е индексот `idx_orders_status`, кој се користи во сите три сценарија за филтрирање на платените нарачки. |
| | 344 | |
| | 345 | == 3. Интегритет и конзистентност == |
| | 346 | |
| | 347 | Со цел самата база да биде отпорна на грешки, имплементирани се стриктни ограничувања (CONSTRAINTS) и тригери кои ја заштитуваат финансиската историја. |
| | 348 | |
| | 349 | === Уникатност === |
| | 350 | |
| | 351 | {{{ |
| | 352 | ALTER TABLE project.app_user |
| | 353 | ADD CONSTRAINT app_user_username_key UNIQUE (username); |
| | 354 | }}} |
| | 355 | |
| | 356 | Корисничките имиња се заштитени со UNIQUE ограничување, со што се гарантира дека во системот не можат да постојат два кориснички профили со исто корисничко име. |
| | 357 | |
| | 358 | === Надворешни клучеви === |
| | 359 | |
| | 360 | Релациите помеѓу табелите се реализирани преку надворешни клучеви (FOREIGN KEY). Со тоа се спречува внесување на записи кои референцираат непостоечки корисници, сметки, нарачки или производи. |
| | 361 | |
| | 362 | Примери: |
| | 363 | * `orders.user_id` → `app_user.user_id` |
| | 364 | * `orders.table_id` → `restaurant_table.table_id` |
| | 365 | * `order_item.order_id` → `orders.order_id` |
| | 366 | * `order_item.product_id` → `product.product_id` |
| | 367 | * `payment.order_id` → `orders.order_id` |
| | 368 | * `invoice.payment_id` → `payment.payment_id` |
| | 369 | |
| | 370 | === CHECK ограничувања === |
| | 371 | |
| | 372 | {{{ |
| | 373 | ALTER TABLE project.order_item |
| | 374 | ADD CONSTRAINT positive_quantity CHECK (quantity > 0); |
| | 375 | |
| | 376 | ALTER TABLE project.order_item |
| | 377 | ADD CONSTRAINT positive_unit_price CHECK (unit_price >= 0); |
| | 378 | |
| | 379 | ALTER TABLE project.product |
| | 380 | ADD CONSTRAINT positive_product_price CHECK (price >= 0); |
| | 381 | }}} |
| | 382 | |
| | 383 | Овие ограничувања спречуваат внесување на негативни количини или цени. |
| | 384 | |
| | 385 | === Типови на податоци за финансиски вредности === |
| | 386 | |
| | 387 | Сите монетарни вредности во системот се складираат со типот `NUMERIC(10,2)`. Овој пристап обезбедува фиксна прецизност до две децимали и ги елиминира грешките кои можат да настанат при користење на типови со подвижна запирка (FLOAT или DOUBLE PRECISION) во финансиски пресметки. |
| | 388 | |
| | 389 | === Тригери за заштита === |
| | 390 | |
| | 391 | Имплементирани се следните тригери кои автоматски реагираат на промени: |
| | 392 | |
| | 393 | * `trg_prevent_order_delete` — спречува бришење на нарачка која има поврзано плаќање. |
| | 394 | * `trg_order_insert_update_table` — автоматски ја означува масата како зафатена при креирање на нарачка. |
| | 395 | * `trg_payment_release_table` — автоматски ја ослободува масата при плаќање. |
| | 396 | * `trg_audit_orders` — автоматски логира секоја промена на нарачка во `audit_log`. |
| | 397 | * `trg_inventory_min_stock` — евидентира предупредување кога залихата е под минимум. |
| | 398 | |
| | 399 | == 4. Одржување на базата == |
| | 400 | |
| | 401 | === Зачувување на структурните промени === |
| | 402 | |
| | 403 | Секоја промена на структурата на базата се документира во Trac документацијата и се имплементира директно во PostgreSQL преку SQL наредби. На тој начин документацијата останува усогласена со реалната имплементација на системот. |
| | 404 | |
| | 405 | === Чување на SQL скриптите === |
| | 406 | |
| | 407 | Секоја SQL скрипта извршена после првата DDL скрипта се чува хронолошки, за во случај да треба базата да се иницијализира повторно, да се извршат редоследно и да се добие истата состојба. |
| | 408 | |
| | 409 | === Одговорност за инфраструктурата === |
| | 410 | |
| | 411 | POS HORECA е дизајниран да работи врз PostgreSQL сервер обезбеден од надворешна инфраструктура (факултетскиот сервер `db_202526z_va_prj_poshoreca`). Конфигурацијата на серверот, резервните копии и механизмите за обновување на податоците се надвор од опсегот на самата апликација. |
| | 412 | |
| | 413 | === Резервни копии === |
| | 414 | |
| | 415 | Препорачана практика е редовно правење на резервни копии од базата преку `pg_dump`: |
| | 416 | |
| | 417 | {{{ |
| | 418 | pg_dump -h localhost -U db_202526z_va_prj_poshoreca_owner -d db_202526z_va_prj_poshoreca -n project > backup_project.sql |
| | 419 | }}} |
| | 420 | |
| | 421 | Ова овозможува враќање на состојбата во случај на грешка или несреќа. |