330 lines
10 KiB
TeX
330 lines
10 KiB
TeX
% CQRS Pattern
|
|
|
|
\section{CQRS Pattern}
|
|
|
|
\begin{frame}{Mi a CQRS?}
|
|
\begin{block}{Command Query Responsibility Segregation}
|
|
\textbf{Magyarul:} Parancs-Lekérdezés Felelősség Szétválasztása
|
|
|
|
\vspace{0.3cm}
|
|
|
|
Az adatok \textbf{írásának} (módosítás) és \textbf{olvasásának} (lekérdezés) szétválasztása külön modellekre.
|
|
\end{block}
|
|
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{exampleblock}{Command (Parancs)}
|
|
\textbf{Írási műveletek:}
|
|
\begin{itemize}
|
|
\item POST, PUT, PATCH, DELETE
|
|
\item Adatok módosítása
|
|
\item Validáció, üzleti logika
|
|
\item Pl: felhasználó létrehozása
|
|
\end{itemize}
|
|
\end{exampleblock}
|
|
\end{column}
|
|
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{exampleblock}{Query (Lekérdezés)}
|
|
\textbf{Olvasási műveletek:}
|
|
\begin{itemize}
|
|
\item GET
|
|
\item Adatok lekérdezése
|
|
\item Optimalizált olvasás
|
|
\item Pl: felhasználók listázása
|
|
\end{itemize}
|
|
\end{exampleblock}
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Miért van szükség CQRS-re?}
|
|
\begin{block}{A probléma}
|
|
Hagyományos CRUD alkalmazásokban ugyanazt a modellt használjuk írásra és olvasásra.
|
|
Ez problémás lehet:
|
|
\begin{itemize}
|
|
\item Az írási és olvasási igények eltérőek
|
|
\item Az olvasás gyakran bonyolultabb (join-ok, aggregációk)
|
|
\item A teljesítmény-optimalizálás nehéz
|
|
\end{itemize}
|
|
\end{block}
|
|
|
|
\begin{exampleblock}{A megoldás: CQRS}
|
|
Külön modellek külön optimalizálással:
|
|
\begin{itemize}
|
|
\item \textbf{Write Model} (Író modell): validáció, üzleti szabályok
|
|
\item \textbf{Read Model} (Olvasó modell): gyors lekérdezések, cache
|
|
\end{itemize}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}{CQRS előnyei}
|
|
\begin{block}{Előnyök}
|
|
\begin{itemize}
|
|
\item \textbf{Független optimalizálás} - Az írás és olvasás külön optimalizálható
|
|
\begin{itemize}
|
|
\item Íráshoz: tranzakciók, validáció, konzisztencia
|
|
\item Olvasáshoz: cache, denormalizált adatok, gyors lekérdezések
|
|
\end{itemize}
|
|
|
|
\item \textbf{Skálázhatóság (Scalability)} - Read és Write modellek külön skálázhatók
|
|
\begin{itemize}
|
|
\item Több olvasó szerver, kevesebb író szerver
|
|
\end{itemize}
|
|
|
|
\item \textbf{Egyszerűség} - Komplexitás csökken
|
|
\begin{itemize}
|
|
\item Minden modell egy feladatra fókuszál
|
|
\end{itemize}
|
|
|
|
\item \textbf{Teljesítmény (Performance)} - Gyorsabb lekérdezések
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Mikor használjuk a CQRS-t?}
|
|
\begin{exampleblock}{Jó választás, ha:}
|
|
\begin{itemize}
|
|
\item \textbf{Komplex domain logika} van az alkalmazásban
|
|
\item \textbf{Nagy terhelés} várható (sok párhuzamos felhasználó)
|
|
\item \textbf{Eltérő írás/olvasás igények}
|
|
\begin{itemize}
|
|
\item Pl: ritkán írunk, sokszor olvasunk
|
|
\end{itemize}
|
|
\item \textbf{Különböző adatformátumok} kellenek íráshoz és olvasáshoz
|
|
\end{itemize}
|
|
\end{exampleblock}
|
|
|
|
\begin{alertblock}{NEM használjuk, ha:}
|
|
\begin{itemize}
|
|
\item Egyszerű CRUD alkalmazás
|
|
\item Kis terhelés
|
|
\item Az írás és olvasás hasonló
|
|
\end{itemize}
|
|
\end{alertblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[shrink=10]{CRUD vs CQRS - Architektúra összehasonlítás}
|
|
\begin{center}
|
|
\begin{tikzpicture}[node distance=1.5cm]
|
|
% CRUD
|
|
\node[draw, rectangle, fill=blue!20, minimum width=2.5cm, minimum height=0.9cm, align=center] (crud) {\small CRUD\\Modell};
|
|
\node[above=0.3cm of crud, font=\bfseries] {Hagyományos};
|
|
\node[draw, rectangle, fill=green!20, minimum width=2.5cm, minimum height=0.9cm, right=1.2cm of crud, align=center] (db1) {\small Adatbázis\\(Database)};
|
|
\draw[<->, thick] (crud) -- (db1) node[midway, above, font=\tiny] {olvas/ír};
|
|
|
|
% CQRS
|
|
\node[draw, rectangle, fill=yellow!20, minimum width=2.5cm, minimum height=0.9cm, below=2cm of crud, align=center] (cmd) {\small Parancs\\(Command)};
|
|
\node[draw, rectangle, fill=orange!20, minimum width=2.5cm, minimum height=0.9cm, below=0.6cm of cmd, align=center] (query) {\small Lekérdezés\\(Query)};
|
|
\node[above=0.3cm of cmd, font=\bfseries] {CQRS};
|
|
\node[draw, rectangle, fill=green!20, minimum width=2.5cm, minimum height=0.9cm, right=1.2cm of cmd, align=center] (db2) {\small Író DB\\(Write)};
|
|
\node[draw, rectangle, fill=green!20, minimum width=2.5cm, minimum height=0.9cm, right=1.2cm of query, align=center] (db3) {\small Olvasó DB\\(Read)};
|
|
|
|
\draw[->, thick] (cmd) -- (db2) node[midway, above, font=\tiny] {ír};
|
|
\draw[<-, thick] (query) -- (db3) node[midway, above, font=\tiny] {olvas};
|
|
\draw[->, dashed, thick] (db2) -- (db3) node[midway, right, font=\tiny] {szinkron};
|
|
\end{tikzpicture}
|
|
\end{center}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{Command - Írás példa}
|
|
\begin{exampleblock}{CreateUserCommand}
|
|
\tiny
|
|
\begin{verbatim}
|
|
// commands/create-user.command.js
|
|
class CreateUserCommand {
|
|
constructor(name, email, password) {
|
|
this.name = name;
|
|
this.email = email;
|
|
this.password = password;
|
|
}
|
|
|
|
validate() {
|
|
if (!this.email || !this.name || !this.password) {
|
|
throw new Error('All fields required');
|
|
}
|
|
if (this.password.length < 8) {
|
|
throw new Error('Password too short');
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = CreateUserCommand;
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{Command Handler}
|
|
\begin{exampleblock}{CreateUserCommandHandler}
|
|
\tiny
|
|
\begin{verbatim}
|
|
// handlers/create-user.handler.js
|
|
const userRepository = require('../repositories/user.repository');
|
|
|
|
class CreateUserCommandHandler {
|
|
async handle(command) {
|
|
command.validate();
|
|
|
|
// Üzleti logika
|
|
const existingUser = await userRepository.findByEmail(command.email);
|
|
if (existingUser) {
|
|
throw new Error('Email already exists');
|
|
}
|
|
|
|
// Password hash
|
|
const hashedPassword = await bcrypt.hash(command.password, 10);
|
|
|
|
// User létrehozás
|
|
const user = await userRepository.create({
|
|
name: command.name,
|
|
email: command.email,
|
|
password: hashedPassword
|
|
});
|
|
|
|
return user;
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{Query - Olvasás példa}
|
|
\begin{exampleblock}{GetUsersQuery}
|
|
\tiny
|
|
\begin{verbatim}
|
|
// queries/get-users.query.js
|
|
class GetUsersQuery {
|
|
constructor(filters = {}) {
|
|
this.page = filters.page || 1;
|
|
this.limit = filters.limit || 10;
|
|
this.role = filters.role;
|
|
this.active = filters.active;
|
|
}
|
|
}
|
|
|
|
// handlers/get-users.handler.js
|
|
class GetUsersQueryHandler {
|
|
async handle(query) {
|
|
// Optimalizált lekérdezés
|
|
const users = await userReadModel.find({
|
|
role: query.role,
|
|
active: query.active,
|
|
skip: (query.page - 1) * query.limit,
|
|
limit: query.limit
|
|
});
|
|
|
|
return users;
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{CQRS REST API-ban (1/2)}
|
|
\begin{exampleblock}{Command végpontok}
|
|
\tiny
|
|
\begin{verbatim}
|
|
// routes/user.routes.js
|
|
const CreateUserCommand = require('../commands/create-user.command');
|
|
const createUserHandler = require('../handlers/create-user.handler');
|
|
|
|
router.post('/users', async (req, res) => {
|
|
try {
|
|
const command = new CreateUserCommand(
|
|
req.body.name,
|
|
req.body.email,
|
|
req.body.password
|
|
);
|
|
const user = await createUserHandler.handle(command);
|
|
res.status(201).json(user);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=15]{CQRS REST API-ban (2/2)}
|
|
\begin{exampleblock}{Query végpontok}
|
|
\tiny
|
|
\begin{verbatim}
|
|
const GetUsersQuery = require('../queries/get-users.query');
|
|
const getUsersHandler = require('../handlers/get-users.handler');
|
|
|
|
router.get('/users', async (req, res) => {
|
|
try {
|
|
const query = new GetUsersQuery({
|
|
page: req.query.page,
|
|
limit: req.query.limit,
|
|
role: req.query.role,
|
|
active: req.query.active
|
|
});
|
|
const users = await getUsersHandler.handle(query);
|
|
res.json(users);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}{CQRS előnyei és hátrányai}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{block}{Előnyök}
|
|
\begin{itemize}
|
|
\item Jobb teljesítmény
|
|
\item Független skálázás
|
|
\item Egyszerűbb modellek
|
|
\item Optimalizált lekérdezések
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{column}
|
|
|
|
\begin{column}{0.48\textwidth}
|
|
\begin{alertblock}{Hátrányok}
|
|
\begin{itemize}
|
|
\item Komplexitás növekedés
|
|
\item Több kód
|
|
\item Szinkronizáció problémák
|
|
\item Eventual consistency
|
|
\end{itemize}
|
|
\end{alertblock}
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Event Sourcing + CQRS}
|
|
\begin{block}{Event Sourcing}
|
|
Az állapotváltozások eseményekként tárolása, nem a végállapot mentése.
|
|
\end{block}
|
|
|
|
\begin{exampleblock}{Események}
|
|
\begin{itemize}
|
|
\item UserCreatedEvent
|
|
\item UserEmailChangedEvent
|
|
\item UserDeactivatedEvent
|
|
\end{itemize}
|
|
\end{exampleblock}
|
|
|
|
\begin{alertblock}{CQRS + ES kombinációja}
|
|
Command-ok eseményeket generálnak, Query modellek eseményekből épülnek fel.
|
|
\end{alertblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Összefoglalás - CQRS}
|
|
\begin{itemize}
|
|
\item CQRS = Command és Query szétválasztása
|
|
\item Command: adatok módosítása (POST, PUT, DELETE)
|
|
\item Query: adatok lekérdezése (GET)
|
|
\item Előnyök: teljesítmény, skálázhatóság
|
|
\item Hátrányok: komplexitás
|
|
\item Event Sourcing jól kombinálható CQRS-sel
|
|
\end{itemize}
|
|
|
|
\vspace{0.5cm}
|
|
|
|
\end{frame}
|