Opowieść o tym, jak w projekcie legacy, który już ledwo dychał, udało się zaimplementować read model oparty na ElasticSearch (choć nie bez przeszkód i czasami pod prąd). Podczas prezentacji aplikacja legacy i walka o przyspieszenie zapytań posłuży jako punkt wyjścia do przeanalizowania konceptu “read modeli”. Po co wdrażamy je do aplikacji? Jakie są metody utrzymania ich w spójności? A w końcu – ich wady i zalety. W przypadku wspomnianej aplikacji zapytania które trwały około 8 minut udało się przyspieszyć do poniżej 1s (choć nie obyło się to bez potknięć). Zobacz jak!
6. Aplikacja legacy - krótka
charakterystyka biznesowa
• Operuje na zleceniach wykonania usługi
• Długo obecna na na rynku
• Dochodowa
• Elastyczna
7. Pola dodatkowe
• Dynamicznie definiowane przez administratora
• Reguły biznesowe
• Filtrowane konfigurowalne listy
8. CREATE TABLE custom_fields (
assignmentId int NOT NULL,
name varchar(32) NOT NULL,
value varchar(128) NOT NULL,
CONSTRAINT cf_key PRIMARY KEY(assignmentId,name)
)
assignmentId name value
------------ ------------- ------------
123 ACF Index B00001F
123 GGV Reference 543-GGV200-1
123 Inclination +770
EAV Encja-Atrybut-Wartość
9. IF a.STATUS = ‘PENDING’ AND cf.FOO > cf.BAR SET cf.BAZ = cf.BAZ + 1
Reguły biznesowe czyli tzw workflow
• Składane z klocków metodą drag and drop
• Definiowane dla typu zlecenia
• Mogły pobierać wartości ze zleceń powiązanych
11. SELECT *
FROM assignment a
JOIN custom_fields cf ON a.id = cf.assignmentId
WHERE (cf.name = "GGV Reference" AND cf.value like "%V200%")
AND (cf.name = "ABC" AND cf.value like "%GF001%") ...
Aplikacja legacy - problem
24. • Nie ma zdarzeń systemowych
• Nie ma jednego spójnego ORM’a
• Mamy bezpośrednie użycia PDO
• Mamy Eloquent’a i Doctrine’a
• Zapytania SQL widziano nawet w plikach HTML …
Do czego się podpiąć ?
25. CREATE TRIGGER assignment_extra_field_update BEFORE UPDATE ON opdrachtextravelden
FOR EACH ROW
BEGIN
INSERT IGNORE INTO `dirty_assignment` (id) VALUES (NEW.OpdrachtId);
END;
CREATE TRIGGER client_update BEFORE UPDATE ON opdrachtgevers
FOR EACH ROW
BEGIN
IF (OLD.naam != NEW.naam) THEN
INSERT IGNORE INTO `dirty_assignment` (id)
SELECT O.OpdrachtId FROM opdrachten O
JOIN projecten P ON O.ProjectId = P.ProjectId
WHERE O.ProjectId = P.ProjectId AND P.OpdrachtgeverId = NEW.OpdrachtgeverId;
END IF;
END;
Ktoś pamięta jeszcze triggery na bazie danych ?
26. • Uruchamiana w tle
• Odczytująca zawartość tabeli dirty_assignment
• Budująca i zapisująca read modele w klastrze ES
Komenda CLI
32. public function getGrid(string $gridType, bool $forceLegacyGrid): GridInterface
{
if (
!$forceLegacyGrid
&& isset($this->elasticGrids[$gridType])
&& $this->elasticSetting->isEnabled()
&& !$this->lagging()
) {
return $this->elasticGrids[$gridType];
}
return $this->legacyGrids[$gridType];
}
Grid provider
33. class LegacyAssignmentGrid implements LegacyGrid
{
public function execute(GridRequest $request): GridDTO
{
$pdo = DBConnection::GetOpenConnection();
// ... i tutaj następuje 300 linii dotychczasowego kodu
Implementacja legacy
34. class ElasticAssignmentGrid extends AbstractGrid implements ElasticGrid
{
public function execute(GridRequest $request): GridDTO
{
$profile = $this->dbProfileRepository->find($request->gridId);
$grid = $this->initializeGrid($profile, $request);
$resultSet = $this->elasticAssignmentRepository
->getByRequest($request);
foreach ($resultSet->getResults() as $result) {
// we get our read model one at a time and populate Grid with it
$grid->addRow($this->prepareRowData($result));
}
Implementacja elasticSearch
35. public function getRows(GridRequest $request, Column ...$columns): Search
{
$qb = new QueryBuilder();
$bool = $qb->query()->bool();
$bool->addFilter($this->nullBool($qb, 'search.deleted_at'));
if (!$request->includeFinishedAssignments) {
$bool->addFilter($qb->query()->term(['search.status.is_last' => false]));
}
$this->filterByDates($request, $bool, $qb);
foreach ($columns as $column) {
$path = $this->mapper->getPathForGridKey($column->getVeldnaam());
// …
Kawałek repozytorium / query.
"ruflin/elastica": "^7.0"
38. • Tylko pola wymagane w konkretnym widoku / odpowiedzi
• Pola wyliczone
• Pola przechowujące agregację
• Zmień strukturę danych jeśli chcesz
Przygotuj sobie klasę reprezentującą model odczytu
39. • Zde
fi
niuj interfejsy query lub repozytoriów tak żeby zwracały modele odczytu
• Dotychczasowy kod używający modelu zapisu może być ich pierwszą implementacją
• Nowe implementacje mogą czerpać dane bezpośrednio z bazy (z pominięciem ORM)
• Jeśli dopuszczamy “eventual consistency” możemy przełączyć odczyty na repliki
• Jesteś gotowy na wyniesienie modeli odczytu do innej tabeli / bazy
Określ kontrakty, wprowadź pierwsze implementacje
41. • Snapshot przygotowany w nocy
• Utrzymywana w spójności w oparciu o triggery
• Zdarzenia dostarczane przez ORM
• Zdarzenia domenowe
Synchronizacja z wydzieloną bazą odczytu
42. Jak skutecznie read model
Case study
Tomasz Surowiec
PHP developer
suro@tsh.io
+48 794 628 983
tsh.io