| 3 | | == Опис == |
| 4 | | |
| 5 | | Во оваа фаза е имплементирана релационата база на податоци за VQEMS системот во PostgreSQL. |
| 6 | | |
| 7 | | Структурата на базата е дефинирана со DDL скриптата, додека податоците потребни за работа и тестирање на системот се генерираат со посебна скрипта. |
| 8 | | |
| 9 | | Основните датотеки се: |
| 10 | | |
| 11 | | * [attachment:ddl.sql ddl.sql] - дефиниција на табелите, клучевите и ограничувањата |
| 12 | | * [attachment:seed_generator.py seed_generator.py] - генерирање на податоци за пополнување на базата |
| 13 | | |
| 14 | | == DML - работа со податоците == |
| 15 | | |
| 16 | | По креирањето на структурата на базата, потребно е табелите да се пополнат со податоци кои ќе овозможат реално тестирање на системот. |
| 17 | | |
| 18 | | Поради големиот број на потребни записи, податоците не се внесуваат рачно со поединечни `INSERT` наредби. Наместо тоа, се користи Python скрипта која автоматски генерира податоци за сите табели. |
| 19 | | |
| 20 | | Скриптата користи `Faker` за генерирање на реалистични вредности како: |
| 21 | | |
| 22 | | * имиња и презимиња |
| 23 | | * компании |
| 24 | | * email адреси |
| 25 | | * URL адреси |
| 26 | | * текстови за reviews и disputes |
| 27 | | * датуми |
| 28 | | * буџети |
| 29 | | * договори и проекти |
| 30 | | |
| 31 | | За да може истиот сет на податоци повторно да се генерира, се користи фиксен seed: |
| | 3 | == Имплементација на базата == |
| | 4 | |
| | 5 | Релационата база на податоци е имплементирана во PostgreSQL според претходно дефинираниот релационен модел. |
| | 6 | |
| | 7 | Структурата на базата е дефинирана во: |
| | 8 | |
| | 9 | * [attachment:ddl.sql ddl.sql] |
| | 10 | |
| | 11 | За генерирање на податоците кои се користат при пополнување и тестирање на базата се користи: |
| | 12 | |
| | 13 | * [attachment:seed_generator.py seed_generator.py] |
| | 14 | |
| | 15 | == Генерирање и внесување на податоци == |
| | 16 | |
| | 17 | Податоците се генерираат со Python скрипта и се запишуваат во CSV датотеки кои одговараат на табелите од базата. |
| | 18 | |
| | 19 | За генерирањето се користи `Faker`, при што е поставен фиксен seed за резултатите да можат повторно да се репродуцираат: |
| 41 | | == Редослед на внесување на податоците == |
| 42 | | |
| 43 | | Податоците се генерираат според зависностите помеѓу табелите, со цел да се почитуваат странските клучеви дефинирани во базата. |
| 44 | | |
| 45 | | Редоследот е приближно: |
| 46 | | |
| 47 | | 1. lookup табели (`Role`, `Permission`, `Industry`, `Project_Status`, `Technology`, ...) |
| 48 | | 2. `Vendor` и `Client` |
| 49 | | 3. `User` и неговите специјализации |
| 50 | | 4. `Vendor_Subscription` |
| 51 | | 5. `Client_Vendor_Contract` |
| 52 | | 6. `Project` |
| 53 | | 7. `Project_Technology` |
| 54 | | 8. `Project_Budget_Audit` |
| 55 | | 9. `Project_Status_History` |
| 56 | | 10. `Review` |
| 57 | | 11. `Review_Score` |
| 58 | | 12. `Dispute_Ticket` |
| 59 | | |
| 60 | | Со ова се обезбедува записите кои се референцираат преку foreign key да постојат пред да бидат употребени во зависните табели. |
| 61 | | |
| 62 | | == Пример - генерирање на Project записи == |
| 63 | | |
| 64 | | За табелата `Project` се генерираат записи со договор, статус, име, почетен и краен датум и буџет: |
| 65 | | |
| 66 | | {{{#!python |
| 67 | | NUM_PROJECTS = 1_400_000 |
| 68 | | |
| | 29 | За запишување на генерираните записи се користи заедничка функција: |
| | 30 | |
| | 31 | {{{#!python |
| | 32 | @contextmanager |
| | 33 | def csv_writer(filename: str, fieldnames: list): |
| | 34 | path = OUT_DIR / filename |
| | 35 | with path.open("w", newline="", encoding="utf-8") as f: |
| | 36 | w = csv.DictWriter(f, fieldnames=fieldnames) |
| | 37 | w.writeheader() |
| | 38 | yield w |
| | 39 | }}} |
| | 40 | |
| | 41 | == Основни податоци == |
| | 42 | |
| | 43 | Најпрво се генерираат податоците за табелите од кои зависат останатите записи. На пример, статусите на проектите се дефинирани на следниот начин: |
| | 44 | |
| | 45 | {{{#!python |
| | 46 | STATUSES = [ |
| | 47 | "Draft", |
| | 48 | "In Progress", |
| | 49 | "On Hold", |
| | 50 | "Completed", |
| | 51 | "Cancelled", |
| | 52 | "Under Review" |
| | 53 | ] |
| | 54 | |
| | 55 | write_csv( |
| | 56 | "project_status.csv", |
| | 57 | ["status_id", "status_name"], |
| | 58 | [ |
| | 59 | {"status_id": i + 1, "status_name": s} |
| | 60 | for i, s in enumerate(STATUSES) |
| | 61 | ], |
| | 62 | ) |
| | 63 | }}} |
| | 64 | |
| | 65 | На сличен начин се генерираат и `Role`, `Permission`, `Industry`, `Technology`, `Rating_Dimension` и `Subscription_Tier`. |
| | 66 | |
| | 67 | == Клиенти и продавачи == |
| | 68 | |
| | 69 | Записите за клиентите и продавачите се генерираат со реалистични имиња, веб страници и контакт информации: |
| | 70 | |
| | 71 | {{{#!python |
| | 72 | write_csv( |
| | 73 | "vendor.csv", |
| | 74 | ["vendor_id", "agency_name", "website"], |
| | 75 | [ |
| | 76 | { |
| | 77 | "vendor_id": i + 1, |
| | 78 | "agency_name": fake.company(), |
| | 79 | "website": fake.url() |
| | 80 | } |
| | 81 | for i in range(NUM_VENDORS) |
| | 82 | ], |
| | 83 | ) |
| | 84 | |
| | 85 | write_csv( |
| | 86 | "client.csv", |
| | 87 | ["client_id", "industry_id", "company_name", "contact_email"], |
| | 88 | [ |
| | 89 | { |
| | 90 | "client_id": i + 1, |
| | 91 | "industry_id": random.choice(industry_ids), |
| | 92 | "company_name": fake.company(), |
| | 93 | "contact_email": fake.company_email(), |
| | 94 | } |
| | 95 | for i in range(NUM_CLIENTS) |
| | 96 | ], |
| | 97 | ) |
| | 98 | }}} |
| | 99 | |
| | 100 | `industry_id` се избира од претходно генерираните индустрии, со што се почитува врската помеѓу `Client` и `Industry`. |
| | 101 | |
| | 102 | == Корисници == |
| | 103 | |
| | 104 | За различните типови на корисници се генерираат заедничките податоци од табелата `User`: |
| | 105 | |
| | 106 | {{{#!python |
| | 107 | user_rows.append( |
| | 108 | { |
| | 109 | "user_id": uid, |
| | 110 | "type": user_type, |
| | 111 | "first_name": fake.first_name(), |
| | 112 | "last_name": fake.last_name(), |
| | 113 | "email": fake.unique.email(), |
| | 114 | "password_hash": hashlib.sha256( |
| | 115 | fake.password().encode() |
| | 116 | ).hexdigest(), |
| | 117 | "is_active": random.random() > 0.25, |
| | 118 | "last_login_at": nullable(rand_ts(1), 0.30), |
| | 119 | "created_at": rand_ts(3), |
| | 120 | "updated_at": rand_ts(1), |
| | 121 | } |
| | 122 | ) |
| | 123 | }}} |
| | 124 | |
| | 125 | Потоа корисниците се поврзуваат со соодветниот клиент, продавач или management улога: |
| | 126 | |
| | 127 | {{{#!python |
| | 128 | write_csv( |
| | 129 | "client_user.csv", |
| | 130 | ["user_id", "client_id"], |
| | 131 | [ |
| | 132 | { |
| | 133 | "user_id": u, |
| | 134 | "client_id": random.choice(client_ids) |
| | 135 | } |
| | 136 | for u in client_user_ids |
| | 137 | ], |
| | 138 | ) |
| | 139 | }}} |
| | 140 | |
| | 141 | == Проекти == |
| | 142 | |
| | 143 | При генерирање на проект се користат веќе постоечки договори и проектни статуси: |
| | 144 | |
| | 145 | {{{#!python |
| | 146 | w.writerow( |
| | 147 | { |
| | 148 | "project_id": i + 1, |
| | 149 | "contract_id": random.choice(contract_ids), |
| | 150 | "status_id": random.choice(status_ids), |
| | 151 | "project_name": fake.catch_phrase(), |
| | 152 | "start_date": sd, |
| | 153 | "end_date": ed, |
| | 154 | "budget": round(random.uniform(1_000, 250_000), 2), |
| | 155 | } |
| | 156 | ) |
| | 157 | }}} |
| | 158 | |
| | 159 | Со ова секој проект е поврзан со валиден `Client_Vendor_Contract` и `Project_Status`. |
| | 160 | |
| | 161 | За many-to-many врската помеѓу проектите и технологиите се генерираат записи во `Project_Technology`: |
| | 162 | |
| | 163 | {{{#!python |
| 81 | | for i in range(NUM_PROJECTS): |
| 82 | | sd = rand_date(0, 4) |
| 83 | | |
| 84 | | ``` |
| 85 | | ed = ( |
| 86 | | sd + timedelta(days=random.randint(30, 730)) |
| 87 | | if random.random() > 0.2 |
| 88 | | else None |
| 89 | | ) |
| 90 | | |
| 91 | | w.writerow( |
| 92 | | { |
| 93 | | "project_id": i + 1, |
| 94 | | "contract_id": random.choice(contract_ids), |
| 95 | | "status_id": random.choice(status_ids), |
| 96 | | "project_name": fake.catch_phrase(), |
| 97 | | "start_date": sd, |
| 98 | | "end_date": ed, |
| 99 | | "budget": round(random.uniform(1_000, 250_000), 2), |
| 100 | | } |
| 101 | | ) |
| 102 | | ``` |
| 103 | | |
| 104 | | }}} |
| 105 | | |
| 106 | | При генерирањето, `contract_id` и `status_id` се избираат од веќе постоечките идентификатори, со што се задржува референцијалниот интегритет на податоците. |
| 107 | | |
| 108 | | == Пример - Reviews и Review Scores == |
| 109 | | |
| 110 | | За дел од проектите се генерира review: |
| 111 | | |
| 112 | | {{{#!python |
| 113 | | reviewed_pids = random.sample( |
| 114 | | range(1, NUM_PROJECTS + 1), |
| 115 | | k=int(NUM_PROJECTS * 0.70) |
| 116 | | ) |
| 117 | | }}} |
| 118 | | |
| 119 | | За секој review потоа се генерира оценка за секоја дефинирана димензија: |
| 120 | | |
| 121 | | {{{#!python |
| | 168 | for pid in project_ids: |
| | 169 | for tid in random.sample( |
| | 170 | tech_ids, |
| | 171 | random.randint(1, 5) |
| | 172 | ): |
| | 173 | w.writerow( |
| | 174 | { |
| | 175 | "project_id": pid, |
| | 176 | "technology_id": tid |
| | 177 | } |
| | 178 | ) |
| | 179 | }}} |
| | 180 | |
| | 181 | == Рецензии и оценки == |
| | 182 | |
| | 183 | Рецензиите се поврзуваат со постоечки проект и клиентски корисник: |
| | 184 | |
| | 185 | {{{#!python |
| | 186 | w.writerow( |
| | 187 | { |
| | 188 | "review_id": rid, |
| | 189 | "project_id": pid, |
| | 190 | "client_user_id": random.choice(client_user_ids), |
| | 191 | "review_date": rand_date(0, 3), |
| | 192 | "summary_text": fake.paragraph(nb_sentences=3), |
| | 193 | "is_published": random.random() > 0.30, |
| | 194 | } |
| | 195 | ) |
| | 196 | }}} |
| | 197 | |
| | 198 | Оценките за рецензиите се внесуваат посебно во `Review_Score`: |
| | 199 | |
| | 200 | {{{#!python |
| | 201 | with csv_writer( |
| | 202 | "review_score.csv", |
| | 203 | ["review_id", "dimension_id", "score_value"] |
| | 204 | ) as w: |
| 159 | | |
| 160 | | Како резултат се генерираат CSV датотеки за табелите во базата, кои потоа се користат за нејзино пополнување. |
| 161 | | |
| 162 | | Поголемите табели содржат голем број записи за да се симулира реална употреба на апликацијата. На пример: |
| 163 | | |
| 164 | | || '''Табела''' || '''Број на записи''' || |
| 165 | | || Vendor || 5,000 || |
| 166 | | || Client || 20,000 || |
| 167 | | || User || 77,000 || |
| 168 | | || Client_Vendor_Contract || 250,000 || |
| 169 | | || Project || 1,400,000 || |
| 170 | | || Review || 980,000 || |
| 171 | | || Review_Score || 4,900,000 || |
| 172 | | |
| 173 | | Покрај нив, `Project_Technology` и `Project_Status_History` генерираат повеќе записи за секој проект, со што и тие достигнуваат многу голем број на редици. |
| 174 | | |
| 175 | | == Заклучок == |
| 176 | | |
| 177 | | Со DDL скриптата е дефинирана структурата на PostgreSQL базата, додека DML делот е поддржан преку автоматско генерирање на реалистични податоци за нејзино пополнување. |
| 178 | | |
| 179 | | Податоците ги следат врските и ограничувањата дефинирани во релациониот модел и обезбедуваат доволно голем dataset за понатамошно тестирање, работа со погледи и оптимизација на упити. |