| 1 | == Напреден апликативен развој
|
|---|
| 2 |
|
|---|
| 3 | Backend делот од апликацијата е .NET 9 Web API кој до базата пристапува преку
|
|---|
| 4 | Entity Framework Core и драјверот Npgsql
|
|---|
| 5 | ([https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL Npgsql.EntityFrameworkCore.PostgreSQL 9.0.4]).
|
|---|
| 6 | Базата е зад SSH тунел, па секоја физичка конекција е уште еден канал низ тунелот -
|
|---|
| 7 | тоа е причината зошто во оваа фаза и трансакциите и pool-от се разгледуваат заедно.
|
|---|
| 8 |
|
|---|
| 9 | Оваа фаза опфаќа:
|
|---|
| 10 |
|
|---|
| 11 | * '''Трансакции''' - кои сценарија пишуваат во повеќе табели и како тие се водат како една целина
|
|---|
| 12 | * '''Ниво на изолација''' - што се случува кога два студента запишуваат ист семестар истовремено
|
|---|
| 13 | * '''Pooling''' - како се менаџираат конекциите и колку од нив навистина се отвораат
|
|---|
| 14 |
|
|---|
| 15 | == 1. Трансакции
|
|---|
| 16 |
|
|---|
| 17 | === Како EF Core води трансакција
|
|---|
| 18 |
|
|---|
| 19 | EF Core не бара анотација како {{{@Transactional}}}. Секој повик на
|
|---|
| 20 | {{{SaveChangesAsync()}}} сам по себе е една трансакција - сите промени натрупани во
|
|---|
| 21 | контекстот се испраќаат во еден BEGIN/COMMIT. Затоа операција која пишува во повеќе
|
|---|
| 22 | табели '''еднаш''' не бара ништо дополнително.
|
|---|
| 23 |
|
|---|
| 24 | Експлицитна трансакција е потребна кога операцијата мора да повика
|
|---|
| 25 | {{{SaveChangesAsync()}}} '''повеќе пати''', најчесто затоа што вториот запис го
|
|---|
| 26 | употребува идентификаторот доделен при првиот. Тогаш двата повика се затвораат во
|
|---|
| 27 | {{{BeginTransactionAsync()}}} ... {{{CommitAsync()}}}, за да не остане запис од
|
|---|
| 28 | првиот чекор ако вториот падне.
|
|---|
| 29 |
|
|---|
| 30 | Во апликацијата тоа се три сценарија: регистрација на студент, запишување на
|
|---|
| 31 | семестар и бришење на предмет.
|
|---|
| 32 |
|
|---|
| 33 | === 1.1 Регистрација на студент (UC002)
|
|---|
| 34 |
|
|---|
| 35 | Корисникот, неговите контакт податоци и средношколските податоци се три табели.
|
|---|
| 36 | Contact и high_school имаат надворешен клуч кон users, па идентификаторот на
|
|---|
| 37 | корисникот мора прво да постои - оттука и двата повика на {{{SaveChangesAsync}}}.
|
|---|
| 38 |
|
|---|
| 39 | {{{#!csharp
|
|---|
| 40 | public async Task<bool> RegisterAsync(RegisterDto registerDto)
|
|---|
| 41 | {
|
|---|
| 42 | if (await _userRepository.UserExistsAsync(registerDto.Email))
|
|---|
| 43 | throw new InvalidOperationException("User with this email already exists");
|
|---|
| 44 |
|
|---|
| 45 | // Use a transaction to ensure all entities are saved together
|
|---|
| 46 | using var transaction = await _context.Database.BeginTransactionAsync();
|
|---|
| 47 | try
|
|---|
| 48 | {
|
|---|
| 49 | // quota and enrollment_year are attributes of Users in the ER
|
|---|
| 50 | // model, so they are set here rather than on a separate entity.
|
|---|
| 51 | var user = new User
|
|---|
| 52 | {
|
|---|
| 53 | Name = registerDto.Name,
|
|---|
| 54 | Surname = registerDto.Surname,
|
|---|
| 55 | Index = registerDto.Index,
|
|---|
| 56 | Email = registerDto.Email,
|
|---|
| 57 | PasswordHash = BCrypt.Net.BCrypt.HashPassword(registerDto.Password),
|
|---|
| 58 | Bday = DateTime.SpecifyKind(registerDto.Bday, DateTimeKind.Unspecified),
|
|---|
| 59 | CreatedAt = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Unspecified),
|
|---|
| 60 | Role = (Models.UserRole)registerDto.Role,
|
|---|
| 61 | EMBG = registerDto.EMBG,
|
|---|
| 62 | Quota = (Models.Quota)registerDto.quotaType,
|
|---|
| 63 | EnrollmentYear = registerDto.enrollmentYear
|
|---|
| 64 | };
|
|---|
| 65 |
|
|---|
| 66 | _context.User.Add(user);
|
|---|
| 67 | await _context.SaveChangesAsync(); // <- тука user.Id добива вредност
|
|---|
| 68 |
|
|---|
| 69 | int userId = user.Id;
|
|---|
| 70 |
|
|---|
| 71 | var contactInfo = new ContactInfo
|
|---|
| 72 | {
|
|---|
| 73 | UserId = userId,
|
|---|
| 74 | City = registerDto.city,
|
|---|
| 75 | Address = registerDto.address,
|
|---|
| 76 | Municipality = registerDto.municipality,
|
|---|
| 77 | PhoneNumber = registerDto.phoneNumber,
|
|---|
| 78 | MicrosoftEmail = registerDto.microsoftEmail
|
|---|
| 79 | };
|
|---|
| 80 |
|
|---|
| 81 | var highSchool = new HighSchool
|
|---|
| 82 | {
|
|---|
| 83 | UserId = userId,
|
|---|
| 84 | GPA = registerDto.gpa,
|
|---|
| 85 | HighSchoolType = (Models.HighSchoolType)registerDto.tip
|
|---|
| 86 | };
|
|---|
| 87 |
|
|---|
| 88 | _context.ContactInfo.Add(contactInfo);
|
|---|
| 89 | _context.HighSchool.Add(highSchool);
|
|---|
| 90 |
|
|---|
| 91 | // Save all related entities in a single transaction
|
|---|
| 92 | await _context.SaveChangesAsync();
|
|---|
| 93 | await transaction.CommitAsync();
|
|---|
| 94 |
|
|---|
| 95 | return true;
|
|---|
| 96 | }
|
|---|
| 97 | catch
|
|---|
| 98 | {
|
|---|
| 99 | await transaction.RollbackAsync();
|
|---|
| 100 | throw;
|
|---|
| 101 | }
|
|---|
| 102 | }
|
|---|
| 103 | }}}
|
|---|
| 104 |
|
|---|
| 105 | Без трансакцијата, пад при вториот {{{SaveChangesAsync}}} би оставил корисник без
|
|---|
| 106 | контакт и без средношколски податоци - запис кој ниту може да се употреби, ниту
|
|---|
| 107 | може да се регистрира повторно, бидејќи е-поштата е UNIQUE.
|
|---|
| 108 |
|
|---|
| 109 | === 1.2 Запишување на семестар (UC007)
|
|---|
| 110 |
|
|---|
| 111 | Запишувањето создава еден ред во enrolled_semesters и '''точно пет''' реда во
|
|---|
| 112 | semesters_subjects. Редовите за предметите го бараат идентификаторот на
|
|---|
| 113 | запишувањето, па повторно се работи за два чекора.
|
|---|
| 114 |
|
|---|
| 115 | {{{#!csharp
|
|---|
| 116 | await using var transaction = await _context.Database.BeginTransactionAsync();
|
|---|
| 117 | try
|
|---|
| 118 | {
|
|---|
| 119 | var enrolment = new EnrolledSemesters
|
|---|
| 120 | {
|
|---|
| 121 | UserId = studentId,
|
|---|
| 122 | SemesterId = request.SemesterId,
|
|---|
| 123 | MajorId = request.MajorId,
|
|---|
| 124 | // enrolled_semesters.quota is NOT NULL; fall back to the
|
|---|
| 125 | // state quota when the student record has none.
|
|---|
| 126 | QuotaType = student.Quota ?? Models.Quota.drzavna,
|
|---|
| 127 | CratedAt = now,
|
|---|
| 128 | LastChange = now,
|
|---|
| 129 | Verified = null
|
|---|
| 130 | };
|
|---|
| 131 |
|
|---|
| 132 | _context.EnrolledSemesters.Add(enrolment);
|
|---|
| 133 | await _context.SaveChangesAsync();
|
|---|
| 134 |
|
|---|
| 135 | foreach (var subjectId in subjectIds)
|
|---|
| 136 | {
|
|---|
| 137 | _context.SemesterSubjects.Add(new SemesterSubject
|
|---|
| 138 | {
|
|---|
| 139 | EnrolledSemesterId = enrolment.Id,
|
|---|
| 140 | SubjectId = subjectId,
|
|---|
| 141 | ProfessorId = professorBySubject[subjectId],
|
|---|
| 142 | Signature = false
|
|---|
| 143 | });
|
|---|
| 144 | }
|
|---|
| 145 |
|
|---|
| 146 | await _context.SaveChangesAsync();
|
|---|
| 147 | await transaction.CommitAsync();
|
|---|
| 148 |
|
|---|
| 149 | return new EnrollSemesterResultDto
|
|---|
| 150 | {
|
|---|
| 151 | Ok = true,
|
|---|
| 152 | Message = $"Enrolled with {subjectIds.Count} subjects.",
|
|---|
| 153 | EnrolledSemesterId = enrolment.Id
|
|---|
| 154 | };
|
|---|
| 155 | }
|
|---|
| 156 | }}}
|
|---|
| 157 |
|
|---|
| 158 | Овде трансакцијата не е само заштита од полузапишан семестар. Ограничувањето за
|
|---|
| 159 | големина на запишувањето од Фаза 7 е '''одложен''' тригер:
|
|---|
| 160 |
|
|---|
| 161 | {{{#!sql
|
|---|
| 162 | CREATE CONSTRAINT TRIGGER trg_enrolment_size
|
|---|
| 163 | AFTER INSERT OR UPDATE OR DELETE ON semesters_subjects
|
|---|
| 164 | DEFERRABLE INITIALLY DEFERRED
|
|---|
| 165 | FOR EACH ROW EXECUTE FUNCTION check_enrolment_size();
|
|---|
| 166 | }}}
|
|---|
| 167 |
|
|---|
| 168 | Одложен тригер се проверува при COMMIT, а не по секој ред. Токму затоа петте
|
|---|
| 169 | предмети се внесуваат во иста трансакција: ако се внесуваа секој во своја
|
|---|
| 170 | трансакција, првиот ред ќе беше видлив во база како запишување со еден предмет, а
|
|---|
| 171 | правилото „најмногу 5 предмети и најмногу 30 кредити“ ќе се проверуваше пет пати
|
|---|
| 172 | наместо еднаш, на крајот.
|
|---|
| 173 |
|
|---|
| 174 | === 1.3 Бришење на предмет (UC012)
|
|---|
| 175 |
|
|---|
| 176 | Предметот е референциран од три врзни табели. Тие мора да се избришат пред него, а
|
|---|
| 177 | ако некоја од нив падне, предметот мора да остане недопрен.
|
|---|
| 178 |
|
|---|
| 179 | {{{#!csharp
|
|---|
| 180 | // semesters_subjects references subjects, so a taken subject cannot go.
|
|---|
| 181 | var taken = await _context.SemesterSubjects.CountAsync(ss => ss.SubjectId == subjectId);
|
|---|
| 182 | if (taken > 0)
|
|---|
| 183 | {
|
|---|
| 184 | return Fail($"Cannot delete: {taken} enrolment(s) already contain this subject.");
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | await using var transaction = await _context.Database.BeginTransactionAsync();
|
|---|
| 188 | try
|
|---|
| 189 | {
|
|---|
| 190 | // The mapping rows must go first; their foreign keys would block the delete.
|
|---|
| 191 | var prereqs = await _context.DependencySubjects
|
|---|
| 192 | .Where(d => d.SubjectId == subjectId || d.DependencyId == subjectId)
|
|---|
| 193 | .ToListAsync();
|
|---|
| 194 | _context.DependencySubjects.RemoveRange(prereqs);
|
|---|
| 195 |
|
|---|
| 196 | var majors = await _context.MajorSubjects
|
|---|
| 197 | .Where(ms => ms.SubjectId == subjectId).ToListAsync();
|
|---|
| 198 | _context.MajorSubjects.RemoveRange(majors);
|
|---|
| 199 |
|
|---|
| 200 | var teaching = await _context.ProfessorSubjects
|
|---|
| 201 | .Where(ps => ps.SubjectId == subjectId).ToListAsync();
|
|---|
| 202 | _context.ProfessorSubjects.RemoveRange(teaching);
|
|---|
| 203 |
|
|---|
| 204 | _context.Subjects.Remove(subject);
|
|---|
| 205 | await _context.SaveChangesAsync();
|
|---|
| 206 | await transaction.CommitAsync();
|
|---|
| 207 |
|
|---|
| 208 | return Ok($"Subject '{subject.Name}' deleted.");
|
|---|
| 209 | }
|
|---|
| 210 | catch
|
|---|
| 211 | {
|
|---|
| 212 | await transaction.RollbackAsync();
|
|---|
| 213 | throw;
|
|---|
| 214 | }
|
|---|
| 215 | }}}
|
|---|
| 216 |
|
|---|
| 217 | === 1.4 Оценување (UC010) - кога трансакција не е потребна
|
|---|
| 218 |
|
|---|
| 219 | Внесувањето на оценка пишува во една табела, со еден
|
|---|
| 220 | {{{SaveChangesAsync}}}, па EF Core сам го обвиткува во трансакција. Додавање на
|
|---|
| 221 | експлицитна трансакција овде не би променило ништо, освен уште една размена со
|
|---|
| 222 | серверот.
|
|---|
| 223 |
|
|---|
| 224 | {{{#!csharp
|
|---|
| 225 | case GradeAction.Add:
|
|---|
| 226 | if (existing is not null)
|
|---|
| 227 | {
|
|---|
| 228 | return Fail("This subject is already graded. Use edit to change the grade.");
|
|---|
| 229 | }
|
|---|
| 230 | await _profRepository.AddPassedSubjectAsync(new PassedSubject
|
|---|
| 231 | {
|
|---|
| 232 | SemesterSubjectId = semesterSubject.Id,
|
|---|
| 233 | Grade = (Grade)request.Grade,
|
|---|
| 234 | // date_passed is `timestamp without time zone`.
|
|---|
| 235 | DatePassed = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Unspecified)
|
|---|
| 236 | });
|
|---|
| 237 | return Ok($"Grade {request.Grade} added.");
|
|---|
| 238 | }}}
|
|---|
| 239 |
|
|---|
| 240 | Вреди да се забележи дека и овде има повеќе од еден запис во базата, но вториот го
|
|---|
| 241 | прави '''базата''', не апликацијата: тригерот trg_grade_rules од Фаза 7 го
|
|---|
| 242 | поставува потписот и создава известување за студентот. Бидејќи тие се извршуваат во
|
|---|
| 243 | истата трансакција како и внесувањето, оценка без потпис и без известување не може
|
|---|
| 244 | да постои.
|
|---|
| 245 |
|
|---|
| 246 | === 1.5 Ниво на изолација и конкурентни запишувања
|
|---|
| 247 |
|
|---|
| 248 | Ниту EF Core ниту Npgsql не го менуваат нивото на изолација, па важи
|
|---|
| 249 | стандардното ниво на PostgreSQL - '''READ COMMITTED'''. Тоа значи дека секое
|
|---|
| 250 | барање во трансакцијата ја гледа состојбата потврдена пред тоа барање, но не ги
|
|---|
| 251 | гледа незавршените трансакции на другите.
|
|---|
| 252 |
|
|---|
| 253 | Тоа е доволно за сите сценарија освен за едно. Пред да запише, апликацијата
|
|---|
| 254 | проверува дали студентот веќе го запишал тој семестар:
|
|---|
| 255 |
|
|---|
| 256 | {{{#!csharp
|
|---|
| 257 | // enrolled_semesters has UNIQUE (user_id, semester_id); check first so
|
|---|
| 258 | // the student gets a sentence rather than a constraint violation.
|
|---|
| 259 | if (await _context.EnrolledSemesters.AnyAsync(
|
|---|
| 260 | es => es.UserId == studentId && es.SemesterId == request.SemesterId))
|
|---|
| 261 | {
|
|---|
| 262 | return Fail("You are already enrolled in that semester.");
|
|---|
| 263 | }
|
|---|
| 264 | }}}
|
|---|
| 265 |
|
|---|
| 266 | Ако студентот два пати кликне на копчето, двете барања ја прават оваа проверка пред
|
|---|
| 267 | било кое од нив да потврди, па двете ја поминуваат. Проверката во апликација не
|
|---|
| 268 | може да го спречи тоа - таа чита состојба која во меѓувреме се менува.
|
|---|
| 269 |
|
|---|
| 270 | Ограничувањето во базата може. Проверено со две истовремени трансакции врз шемата
|
|---|
| 271 | project:
|
|---|
| 272 |
|
|---|
| 273 | {{{
|
|---|
| 274 | -- сесија А -- сесија Б (0.5s подоцна)
|
|---|
| 275 | BEGIN; BEGIN;
|
|---|
| 276 | INSERT INTO enrolled_semesters INSERT INTO enrolled_semesters
|
|---|
| 277 | (user_id, quota, major_id, (user_id, quota, major_id,
|
|---|
| 278 | semester_id) semester_id)
|
|---|
| 279 | VALUES (1, 'drzavna', 1, 3); VALUES (1, 'drzavna', 1, 3);
|
|---|
| 280 | SELECT pg_sleep(2); -- чека А да заврши
|
|---|
| 281 | COMMIT; ERROR: duplicate key value violates unique
|
|---|
| 282 | constraint "enrolled_semesters_user_id_semester_id_key"
|
|---|
| 283 | DETAIL: Key (user_id, semester_id)=(1, 3) already exists.
|
|---|
| 284 | ROLLBACK
|
|---|
| 285 | }}}
|
|---|
| 286 |
|
|---|
| 287 | Втората трансакција не добива грешка веднаш - таа '''чека''' на редот вметнат од
|
|---|
| 288 | првата, бидејќи PostgreSQL мора прво да види дали првата ќе потврди или ќе се
|
|---|
| 289 | врати. Дури по COMMIT-от на првата, втората паѓа.
|
|---|
| 290 |
|
|---|
| 291 | Апликацијата затоа ја фаќа таа грешка и ја претвора во истата реченица која ја враќа
|
|---|
| 292 | и проверката погоре, наместо студентот да добие 500:
|
|---|
| 293 |
|
|---|
| 294 | {{{#!csharp
|
|---|
| 295 | catch (DbUpdateException ex) when (IsUniqueViolation(ex))
|
|---|
| 296 | {
|
|---|
| 297 | // Two enrolments for the same semester sent at the same time both
|
|---|
| 298 | // pass the check above, because each transaction reads the state
|
|---|
| 299 | // from before the other one wrote. UNIQUE (user_id, semester_id)
|
|---|
| 300 | // is what actually settles it, so the loser gets the same
|
|---|
| 301 | // sentence as if the check had caught it.
|
|---|
| 302 | await transaction.RollbackAsync();
|
|---|
| 303 | return Fail("You are already enrolled in that semester.");
|
|---|
| 304 | }
|
|---|
| 305 | catch
|
|---|
| 306 | {
|
|---|
| 307 | await transaction.RollbackAsync();
|
|---|
| 308 | throw;
|
|---|
| 309 | }
|
|---|
| 310 |
|
|---|
| 311 | /// <summary>23505 is unique_violation.</summary>
|
|---|
| 312 | private static bool IsUniqueViolation(DbUpdateException ex) =>
|
|---|
| 313 | ex.InnerException is PostgresException { SqlState: "23505" };
|
|---|
| 314 | }}}
|
|---|
| 315 |
|
|---|
| 316 | Значи проверката во апликацијата постои заради пораката, а ограничувањето во базата
|
|---|
| 317 | заради точноста. Истиот принцип важи и за правилата од Фаза 7: апликацијата ги
|
|---|
| 318 | проверува за да даде разбирлива порака, а базата ги чува за да важат и кога некој
|
|---|
| 319 | пишува директно во неа.
|
|---|
| 320 |
|
|---|
| 321 | == 2. Pooling
|
|---|
| 322 |
|
|---|
| 323 | === 2.1 Pool на конекции (Npgsql)
|
|---|
| 324 |
|
|---|
| 325 | Како и кај Spring Boot, конекциите не се отвораат рачно. Npgsql има вграден pool
|
|---|
| 326 | кој е вклучен стандардно, па {{{UseNpgsql(connectionString)}}} е доволно.
|
|---|
| 327 |
|
|---|
| 328 | Стандардните вредности на Npgsql 9.0.4 (отчитани од
|
|---|
| 329 | {{{NpgsqlConnectionStringBuilder}}}):
|
|---|
| 330 |
|
|---|
| 331 | {{{
|
|---|
| 332 | Pooling = True
|
|---|
| 333 | Minimum Pool Size = 0
|
|---|
| 334 | Maximum Pool Size = 100
|
|---|
| 335 | Connection Idle Lifetime = 300 (секунди)
|
|---|
| 336 | Connection Pruning Interval = 10 (секунди)
|
|---|
| 337 | Timeout = 15 (секунди, чекање на слободна конекција)
|
|---|
| 338 | Command Timeout = 30 (секунди)
|
|---|
| 339 | Multiplexing = False
|
|---|
| 340 | }}}
|
|---|
| 341 |
|
|---|
| 342 | Стандардните 100 конекции се премногу за оваа поставеност: серверот е споделен меѓу
|
|---|
| 343 | сите проекти, а секоја конекција е уште еден канал низ SSH тунелот. Затоа pool-от е
|
|---|
| 344 | ограничен во самиот connection string:
|
|---|
| 345 |
|
|---|
| 346 | {{{#!json
|
|---|
| 347 | "ConnectionStrings": {
|
|---|
| 348 | "DefaultConnection": "Host=localhost;Port=9999;Database=db_202526z_va_prj_know2026;Username=db_202526z_va_prj_iknow2026_owner;Password=REDACTED_ROTATE_ME;Search Path=project;Include Error Detail=true;Maximum Pool Size=10;Minimum Pool Size=1;Connection Idle Lifetime=300;Timeout=15"
|
|---|
| 349 | }
|
|---|
| 350 | }}}
|
|---|
| 351 |
|
|---|
| 352 | * '''Maximum Pool Size=10''' - горна граница на отворени конекции, исто како default-от на HikariCP
|
|---|
| 353 | * '''Minimum Pool Size=1''' - една конекција останува отворена, за тунелот да не мора да отвора нов канал за секое прво барање
|
|---|
| 354 | * '''Connection Idle Lifetime=300''' - неупотребените конекции над минимумот се затвораат по 5 минути
|
|---|
| 355 | * '''Timeout=15''' - ако сите 10 се зафатени, барањето чека најмногу 15 секунди пред да падне
|
|---|
| 356 |
|
|---|
| 357 | === 2.2 Pool на DbContext (EF Core)
|
|---|
| 358 |
|
|---|
| 359 | Покрај конекциите, EF Core може да ги рециклира и самите DbContext објекти. Тоа е
|
|---|
| 360 | одделен pool: {{{AddDbContextPool}}} ги чува инстанците на контекстот (со нивните
|
|---|
| 361 | поставки и мапирања), додека Npgsql ги чува физичките конекции под нив.
|
|---|
| 362 |
|
|---|
| 363 | {{{#!csharp
|
|---|
| 364 | // AddDbContextPool reuses the DbContext instances themselves; the connections
|
|---|
| 365 | // underneath them are pooled separately by Npgsql, sized in the connection
|
|---|
| 366 | // string. Both matter here because every physical connection is one more
|
|---|
| 367 | // channel through the SSH tunnel.
|
|---|
| 368 | builder.Services.AddDbContextPool<AppDbContext>(options =>
|
|---|
| 369 | options.UseNpgsql(
|
|---|
| 370 | builder.Configuration.GetConnectionString("DefaultConnection"),
|
|---|
| 371 | npgsql =>
|
|---|
| 372 | {
|
|---|
| 373 | npgsql.MapEnum<UserRole>("user_role", AppDbContext.ProjectSchema, PgEnumLabels.UserRole);
|
|---|
| 374 | npgsql.MapEnum<HighSchoolType>("hs_type", AppDbContext.ProjectSchema, PgEnumLabels.Lowercase);
|
|---|
| 375 | npgsql.MapEnum<Quota>("quota_type", AppDbContext.ProjectSchema, PgEnumLabels.Lowercase);
|
|---|
| 376 | npgsql.MapEnum<sType>("semester_type", AppDbContext.ProjectSchema, PgEnumLabels.Lowercase);
|
|---|
| 377 | npgsql.MapEnum<Grade>("grade_type", AppDbContext.ProjectSchema, PgEnumLabels.Grade);
|
|---|
| 378 | }));
|
|---|
| 379 | }}}
|
|---|
| 380 |
|
|---|
| 381 | Ова е применливо бидејќи AppDbContext има само еден конструктор, оној кој прима
|
|---|
| 382 | {{{DbContextOptions}}}. Контекст кој би примал сопствени зависности (на пример
|
|---|
| 383 | тековниот корисник) не смее да се рециклира, бидејќи состојбата од едно барање би
|
|---|
| 384 | протекла во следното.
|
|---|
| 385 |
|
|---|
| 386 | === 2.3 Мерење на pool-от
|
|---|
| 387 |
|
|---|
| 388 | Мерено со апликацијата пуштена врз локална база со истата шема, со броење на
|
|---|
| 389 | {{{pg_stat_activity}}}:
|
|---|
| 390 |
|
|---|
| 391 | {{{#!sql
|
|---|
| 392 | SELECT count(*) FROM pg_stat_activity WHERE datname = 'iknow';
|
|---|
| 393 | }}}
|
|---|
| 394 |
|
|---|
| 395 | {{{
|
|---|
| 396 | состојба отворени конекции
|
|---|
| 397 | --------------------------------------------------------------------
|
|---|
| 398 | по стартување и едно барање 1
|
|---|
| 399 | за време на 50 истовремени барања 10
|
|---|
| 400 | по завршување на барањата 10
|
|---|
| 401 | }}}
|
|---|
| 402 |
|
|---|
| 403 | Првиот ред е Minimum Pool Size=1 - една конекција останува отворена. Вториот ред
|
|---|
| 404 | покажува дека pool-от расте со оптоварувањето, но застанува точно на Maximum Pool
|
|---|
| 405 | Size=10; останатите барања чекаат слободна конекција наместо да отворат нова.
|
|---|
| 406 | Третиот ред покажува дека отворените конекции не се затвораат веднаш - тие остануваат
|
|---|
| 407 | во pool-от и се затвораат дури по Connection Idle Lifetime, бидејќи повторното
|
|---|
| 408 | отворање низ тунел е поскапо од држењето отворена конекција.
|
|---|
| 409 |
|
|---|
| 410 | Истото може да се провери и од страната на тунелот - секоја нова конекција се гледа
|
|---|
| 411 | како нов канал во логовите на тунел скриптата:
|
|---|
| 412 |
|
|---|
| 413 | {{{
|
|---|
| 414 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested.
|
|---|
| 415 | debug1: channel 2: new direct-tcpip [direct-tcpip] (inactive timeout: 0)
|
|---|
| 416 | }}}
|
|---|
| 417 |
|
|---|
| 418 | === 2.4 Проверка дека апликацијата е поврзана
|
|---|
| 419 |
|
|---|
| 420 | Контролерот DbHealth го враќа одговорот кој покажува дека тунелот, конекцијата и
|
|---|
| 421 | мапирањето на ентитетите работат:
|
|---|
| 422 |
|
|---|
| 423 | {{{#!json
|
|---|
| 424 | {
|
|---|
| 425 | "connected": true,
|
|---|
| 426 | "server": "PostgreSQL 18.4 ... on x86_64-pc-linux-gnu, 64-bit",
|
|---|
| 427 | "counts": {
|
|---|
| 428 | "users": 8,
|
|---|
| 429 | "subjects": 20,
|
|---|
| 430 | "enrolledSemesters": 4,
|
|---|
| 431 | "semesterSubjects": 6,
|
|---|
| 432 | "passedSubjects": 4
|
|---|
| 433 | }
|
|---|
| 434 | }
|
|---|
| 435 | }}}
|
|---|
| 436 |
|
|---|
| 437 | == Историјат
|
|---|
| 438 |
|
|---|
| 439 | '''Верзија 1''' - Прва верзија: трансакции во трите сценарија кои пишуваат во
|
|---|
| 440 | повеќе табели, обработка на конкурентни запишувања преку ограничувањето
|
|---|
| 441 | UNIQUE (user_id, semester_id), и конфигурација и мерење на pool-от на конекции и
|
|---|
| 442 | на DbContext.
|
|---|
| 443 |
|
|---|
| 444 | == Статус
|
|---|
| 445 |
|
|---|
| 446 | ''' [[span(style=color: #FF8000, Во тек )]] '''
|
|---|