Przejdź do głównej zawartości

Najlepsze praktyki ORM

Wykorzystaj asystentów kodowania AI do projektowania efektywnych modeli danych, generowania bezpiecznych typowo zapytań i implementacji solidnych wzorców bazodanowych przy użyciu popularnych ORM-ów.

Inteligentne projektowanie schematów

Wzorzec: Użyj AI do projektowania znormalizowanych, bezpiecznych typowo schematów z właściwymi relacjami i ograniczeniami.

// Zapytanie do asystenta AI
"Zaprojektuj schemat Prisma dla platformy e-commerce z:
- Architekturą wielodostępną (organizacje)
- Rolami użytkowników i uprawnieniami
- Katalogiem produktów z wariantami
- Koszykiem i zamówieniami
- Śledzeniem inwentarza
Uwzględnij właściwe indeksy, ograniczenia i usuwanie miękkie"
// AI generuje kompleksowy schemat Prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Baza wielodostępna
model Organization {
id String @id @default(cuid())
name String
slug String @unique
plan Plan @default(FREE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// Relacje
users User[]
products Product[]
orders Order[]
@@index([deletedAt])
@@map("organizations")
}
// Zarządzanie użytkownikami z rolami
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
firstName String?
lastName String?
role UserRole @default(MEMBER)
emailVerified Boolean @default(false)
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// Relacje
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
sessions Session[]
cart Cart?
orders Order[]
@@index([organizationId, deletedAt])
@@index([email])
@@map("users")
}
// Katalog produktów
model Product {
id String @id @default(cuid())
name String
slug String
description String?
category String
basePrice Decimal @db.Money
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// Relacje
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
variants ProductVariant[]
@@unique([organizationId, slug])
@@index([organizationId, category, deletedAt])
@@index([isActive, deletedAt])
@@map("products")
}
// Warianty produktów dla SKU
model ProductVariant {
id String @id @default(cuid())
sku String
name String
price Decimal @db.Money
cost Decimal? @db.Money
// Inwentarz
stockQuantity Int @default(0)
lowStockThreshold Int @default(10)
trackInventory Boolean @default(true)
// Atrybuty
attributes Json @default("{}")
images String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// Relacje
productId String
product Product @relation(fields: [productId], references: [id])
cartItems CartItem[]
orderItems OrderItem[]
@@unique([productId, sku])
@@index([productId, deletedAt])
@@index([stockQuantity])
@@map("product_variants")
}
// Koszyk zakupów
model Cart {
id String @id @default(cuid())
expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '7 days'"))
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relacje
userId String @unique
user User @relation(fields: [userId], references: [id])
items CartItem[]
@@index([expiresAt])
@@map("carts")
}
model CartItem {
id String @id @default(cuid())
quantity Int
price Decimal @db.Money
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relacje
cartId String
cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
variantId String
variant ProductVariant @relation(fields: [variantId], references: [id])
@@unique([cartId, variantId])
@@map("cart_items")
}
// Zamówienia
model Order {
id String @id @default(cuid())
orderNumber String @unique @default(dbgenerated("'ORD-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' || LPAD(nextval('order_seq')::TEXT, 6, '0')"))
status OrderStatus @default(PENDING)
// Ceny
subtotal Decimal @db.Money
tax Decimal @db.Money
shipping Decimal @db.Money
total Decimal @db.Money
// Informacje o kliencie (zdenormalizowane dla historii)
customerEmail String
customerName String
// Znaczniki czasowe
placedAt DateTime @default(now())
paidAt DateTime?
shippedAt DateTime?
deliveredAt DateTime?
cancelledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relacje
userId String
user User @relation(fields: [userId], references: [id])
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
items OrderItem[]
@@index([organizationId, status])
@@index([userId])
@@index([orderNumber])
@@map("orders")
}
model OrderItem {
id String @id @default(cuid())
quantity Int
price Decimal @db.Money
total Decimal @db.Money
// Migawka produktu (zdenormalizowana)
productName String
variantName String
variantSku String
// Relacje
orderId String
order Order @relation(fields: [orderId], references: [id])
variantId String
variant ProductVariant @relation(fields: [variantId], references: [id])
@@index([orderId])
@@map("order_items")
}
// Enumeracje
enum Plan {
FREE
STARTER
PROFESSIONAL
ENTERPRISE
}
enum UserRole {
OWNER
ADMIN
MEMBER
READONLY
}
enum OrderStatus {
PENDING
PROCESSING
PAID
SHIPPED
DELIVERED
CANCELLED
REFUNDED
}
model Session {
id String @id @default(cuid())
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([expiresAt])
@@map("sessions")
}

Korzyści generowane przez AI:

  • Właściwa normalizacja ze strategiczną denormalizacją
  • Kompleksowe indeksy dla częstych zapytań
  • Wsparcie usuwania miękkiego w całym systemie
  • Bezpieczne typowo enumeracje dla statusów
  • Ścieżki audytu ze znacznikami czasowymi
// Poproś o encje TypeORM z AI
"Przekonwertuj ten schemat na encje TypeORM z:
- Właściwymi dekoratorami i relacjami
- Niestandardowymi repozytoriami
- Budowniczymi zapytań
- Wsparciem migracji"
// AI generuje encje TypeORM:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
ManyToOne,
OneToMany,
Index,
Unique,
BeforeInsert,
BeforeUpdate
} from 'typeorm';
import { IsEmail, IsEnum, Min } from 'class-validator';
import bcrypt from 'bcrypt';
@Entity('users')
@Index(['organizationId', 'deletedAt'])
@Unique(['email'])
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
@IsEmail()
email: string;
@Column()
passwordHash: string;
@Column({ nullable: true })
firstName?: string;
@Column({ nullable: true })
lastName?: string;
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.MEMBER
})
@IsEnum(UserRole)
role: UserRole;
@Column({ default: false })
emailVerified: boolean;
@Column({ type: 'timestamp', nullable: true })
lastLoginAt?: Date;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt?: Date;
// Relacje
@ManyToOne(() => Organization, org => org.users)
organization: Organization;
@Column()
organizationId: string;
@OneToMany(() => Order, order => order.user)
orders: Order[];
// Hooki
@BeforeInsert()
async hashPassword() {
if (this.passwordHash) {
this.passwordHash = await bcrypt.hash(this.passwordHash, 10);
}
}
async validatePassword(password: string): Promise<boolean> {
return bcrypt.compare(password, this.passwordHash);
}
// Właściwości wirtualne
get fullName(): string {
return [this.firstName, this.lastName].filter(Boolean).join(' ');
}
}
// Niestandardowe repozytorium
@EntityRepository(User)
export class UserRepository extends Repository<User> {
async findActiveByEmail(email: string): Promise<User | null> {
return this.createQueryBuilder('user')
.leftJoinAndSelect('user.organization', 'org')
.where('user.email = :email', { email })
.andWhere('user.deletedAt IS NULL')
.andWhere('org.deletedAt IS NULL')
.getOne();
}
async findByOrganization(
orgId: string,
options: { includeDeleted?: boolean } = {}
): Promise<User[]> {
const query = this.createQueryBuilder('user')
.where('user.organizationId = :orgId', { orgId });
if (!options.includeDeleted) {
query.andWhere('user.deletedAt IS NULL');
}
return query.getMany();
}
async updateLastLogin(userId: string): Promise<void> {
await this.update(userId, { lastLoginAt: new Date() });
}
}

Inteligentne strategie migracji

// Poproś o strategię migracji z AI
"Wygeneruj migracje bazy danych dla:
- Dodania wsparcia wielu walut do produktów
- Implementacji logów audytu
- Dodania wyszukiwania pełnotekstowego
- Wdrożenia bez przestojów"
// AI dostarcza kompleksowy plan migracji:
// Migracja 1: Dodaj wsparcie wielu walut (bezpieczne)
export class AddMultiCurrencySupport1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Dodaj kolumny walut z domyślnymi (nie łamiące)
await queryRunner.query(`
ALTER TABLE products
ADD COLUMN currency VARCHAR(3) DEFAULT 'USD' NOT NULL,
ADD COLUMN base_prices JSONB DEFAULT '{"USD": 0}'::jsonb;
`);
// Uzupełnij base_prices z istniejących danych
await queryRunner.query(`
UPDATE products
SET base_prices = jsonb_build_object('USD', base_price);
`);
// Utwórz tabelę kursów wymiany
await queryRunner.query(`
CREATE TABLE exchange_rates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_currency VARCHAR(3) NOT NULL,
to_currency VARCHAR(3) NOT NULL,
rate DECIMAL(10, 6) NOT NULL,
valid_from TIMESTAMP NOT NULL,
valid_to TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_rate_period
UNIQUE (from_currency, to_currency, valid_from)
);
CREATE INDEX idx_exchange_rates_lookup
ON exchange_rates(from_currency, to_currency, valid_from DESC);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE exchange_rates`);
await queryRunner.query(`
ALTER TABLE products
DROP COLUMN currency,
DROP COLUMN base_prices;
`);
}
}
// Migracja 2: Dodaj logi audytu (zero przestojów)
export class AddAuditLogs1234567891 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Utwórz tabelę logów audytu
await queryRunner.query(`
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name VARCHAR(255) NOT NULL,
record_id VARCHAR(255) NOT NULL,
action VARCHAR(50) NOT NULL,
user_id UUID,
organization_id UUID,
old_values JSONB,
new_values JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Partycjonowanie dla wydajności
CREATE INDEX idx_audit_logs_table_record
ON audit_logs(table_name, record_id, created_at DESC);
CREATE INDEX idx_audit_logs_user
ON audit_logs(user_id, created_at DESC);
CREATE INDEX idx_audit_logs_org
ON audit_logs(organization_id, created_at DESC);
`);
// Utwórz funkcję wyzwalacza
await queryRunner.query(`
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (
table_name,
record_id,
action,
user_id,
organization_id,
old_values,
new_values,
created_at
) VALUES (
TG_TABLE_NAME,
CASE
WHEN TG_OP = 'DELETE' THEN OLD.id::text
ELSE NEW.id::text
END,
TG_OP,
current_setting('app.current_user_id', true)::uuid,
current_setting('app.current_org_id', true)::uuid,
CASE
WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD)
ELSE NULL
END,
CASE
WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW)
ELSE NULL
END,
NOW()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
// Dodaj wyzwalacze do krytycznych tabel
const auditedTables = ['users', 'products', 'orders', 'product_variants'];
for (const table of auditedTables) {
await queryRunner.query(`
CREATE TRIGGER audit_trigger_${table}
AFTER INSERT OR UPDATE OR DELETE ON ${table}
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();
`);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const auditedTables = ['users', 'products', 'orders', 'product_variants'];
for (const table of auditedTables) {
await queryRunner.query(`DROP TRIGGER IF EXISTS audit_trigger_${table} ON ${table}`);
}
await queryRunner.query(`DROP FUNCTION IF EXISTS audit_trigger_function()`);
await queryRunner.query(`DROP TABLE audit_logs`);
}
}
// Migracja 3: Dodaj wyszukiwanie pełnotekstowe
export class AddFullTextSearch1234567892 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Dodaj kolumnę tsvector
await queryRunner.query(`
ALTER TABLE products
ADD COLUMN search_vector tsvector;
`);
// Utwórz funkcję do aktualizacji wektora wyszukiwania
await queryRunner.query(`
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.category, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
// Utwórz wyzwalacz
await queryRunner.query(`
CREATE TRIGGER update_search_vector_trigger
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
`);
// Zaktualizuj istniejące rekordy
await queryRunner.query(`
UPDATE products SET
search_vector =
setweight(to_tsvector('english', COALESCE(name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(description, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(category, '')), 'C');
`);
// Utwórz indeks GIN
await queryRunner.query(`
CREATE INDEX idx_products_search
ON products USING GIN(search_vector);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TRIGGER IF EXISTS update_search_vector_trigger ON products`);
await queryRunner.query(`DROP FUNCTION IF EXISTS update_search_vector()`);
await queryRunner.query(`DROP INDEX IF EXISTS idx_products_search`);
await queryRunner.query(`ALTER TABLE products DROP COLUMN search_vector`);
}
}

Zapobieganie zapytaniom N+1

"Wykryj i napraw zapytania N+1 w tym kodzie:
const users = await User.findAll();
for (const user of users) {
const orders = await user.getOrders();
console.log(orders);
}"
// AI sugeruje eager loading:
const users = await User.findAll({
include: [{
model: Order,
as: 'orders',
where: { status: 'completed' },
required: false
}]
});
// Lub z Prismą:
const users = await prisma.user.findMany({
include: {
orders: {
where: { status: 'COMPLETED' }
}
}
});

Operacje wsadowe

"Optymalizuj operacje wstawiania/aktualizacji wsadowej"
// AI generuje efektywne wzorce:
// Operacje wsadowe w Prismie
async function bulkCreateProducts(products: ProductInput[]) {
// Użyj createMany dla wstawień
const created = await prisma.$transaction(async (tx) => {
// Utwórz produkty
await tx.product.createMany({
data: products.map(p => ({
name: p.name,
slug: generateSlug(p.name),
organizationId: p.organizationId,
basePrice: p.basePrice
})),
skipDuplicates: true
});
// Pobierz utworzone produkty dla wariantów
const createdProducts = await tx.product.findMany({
where: {
slug: {
in: products.map(p => generateSlug(p.name))
}
}
});
// Utwórz warianty wsadowo
const variants = products.flatMap((p, idx) =>
p.variants.map(v => ({
...v,
productId: createdProducts[idx].id
}))
);
await tx.productVariant.createMany({
data: variants
});
return createdProducts;
});
return created;
}
// Operacje wsadowe w TypeORM
async function bulkUpdatePrices(
updates: Array<{ id: string; price: number }>
) {
// Podziel aktualizacje dla wydajności
const chunks = chunk(updates, 1000);
for (const batch of chunks) {
await dataSource
.createQueryBuilder()
.update(ProductVariant)
.set({
price: () => "CASE id " +
batch.map(u => `WHEN '${u.id}' THEN ${u.price}`).join(' ') +
" END",
updatedAt: new Date()
})
.where("id IN (:...ids)", {
ids: batch.map(u => u.id)
})
.execute();
}
}

Pooling połączeń

"Skonfiguruj optymalne pooling połączeń"
// Konfiguracja Prismy
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// AI sugeruje ustawienia pool
connectionLimit = 25
pool = {
min: 5
max: 25
idleTimeoutMillis: 30000
createTimeoutMillis: 30000
acquireTimeoutMillis: 30000
}
}
// Konfiguracja TypeORM
const dataSource = new DataSource({
type: "postgres",
// Ustawienia pool połączeń
extra: {
max: 25, // Maks połączeń
min: 5, // Min połączeń
idleTimeoutMillis: 30000, // Zamknij bezczynne połączenia
connectionTimeoutMillis: 30000,
statement_timeout: 60000, // Timeout zapytania
query_timeout: 60000,
// Włącz przygotowane instrukcje
prepare: true
},
// Włącz cachowanie zapytań
cache: {
type: "redis",
options: {
socket: {
host: "localhost",
port: 6379
}
},
duration: 30000 // 30 sekund
}
});

Cachowanie zapytań

"Implementuj inteligentne cachowanie zapytań"
class CachedRepository<T> {
constructor(
private repository: Repository<T>,
private cachePrefix: string
) {}
async findWithCache(
key: string,
query: () => Promise<T[]>,
ttl: number = 300 // 5 minut
): Promise<T[]> {
const cacheKey = `${this.cachePrefix}:${key}`;
// Spróbuj najpierw cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Wykonaj zapytanie
const results = await query();
// Cachuj wyniki
await redis.setex(
cacheKey,
ttl,
JSON.stringify(results)
);
return results;
}
async invalidateCache(patterns: string[]) {
for (const pattern of patterns) {
const keys = await redis.keys(
`${this.cachePrefix}:${pattern}`
);
if (keys.length) {
await redis.del(...keys);
}
}
}
}

Wzorce transakcji generowane przez AI

// Poproś o wzorzec transakcji z AI
"Wygeneruj solidną obsługę transakcji dla:
- Składania zamówienia ze sprawdzeniem inwentarza
- Przetwarzania płatności
- Powiadomienia e-mail
- Rollback przy jakimkolwiek błędzie"
// AI dostarcza kompleksowe rozwiązanie:
import { DataSource } from 'typeorm';
import { PrismaClient } from '@prisma/client';
// Wzorzec transakcji TypeORM
export class OrderService {
constructor(
private dataSource: DataSource,
private paymentService: PaymentService,
private emailService: EmailService
) {}
async placeOrder(
userId: string,
items: OrderItemInput[]
): Promise<Order> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction('SERIALIZABLE');
try {
// 1. Zablokuj i sprawdź inwentarz
const variants = await queryRunner.manager
.createQueryBuilder(ProductVariant, 'v')
.setLock('pessimistic_write')
.whereInIds(items.map(i => i.variantId))
.getMany();
// Waliduj inwentarz
for (const item of items) {
const variant = variants.find(v => v.id === item.variantId);
if (!variant || variant.stockQuantity < item.quantity) {
throw new InsufficientInventoryError(
`Niewystarczający stan dla ${variant?.name}`
);
}
}
// 2. Utwórz zamówienie
const order = await queryRunner.manager.save(Order, {
userId,
status: OrderStatus.PENDING,
items: items.map(item => ({
variantId: item.variantId,
quantity: item.quantity,
price: variants.find(v => v.id === item.variantId)!.price
}))
});
// 3. Zaktualizuj inwentarz
for (const item of items) {
await queryRunner.manager.decrement(
ProductVariant,
{ id: item.variantId },
'stockQuantity',
item.quantity
);
}
// 4. Przetwórz płatność (usługa zewnętrzna)
const payment = await this.paymentService.charge({
orderId: order.id,
amount: order.total,
userId
});
// 5. Zaktualizuj status zamówienia
order.status = OrderStatus.PAID;
order.paymentId = payment.id;
await queryRunner.manager.save(order);
// Zatwierdź transakcję
await queryRunner.commitTransaction();
// 6. Wyślij e-mail (poza transakcją)
await this.emailService.sendOrderConfirmation(order);
return order;
} catch (error) {
await queryRunner.rollbackTransaction();
// Kompensuj usługi zewnętrzne jeśli potrzeba
if (error.paymentId) {
await this.paymentService.refund(error.paymentId);
}
throw error;
} finally {
await queryRunner.release();
}
}
}
// Wzorzec transakcji Prismy z ponawianiem
export class PrismaOrderService {
constructor(
private prisma: PrismaClient,
private paymentService: PaymentService
) {}
async placeOrder(
userId: string,
items: OrderItemInput[],
maxRetries: number = 3
): Promise<Order> {
let retries = 0;
while (retries < maxRetries) {
try {
const order = await this.prisma.$transaction(
async (tx) => {
// 1. Sprawdź inwentarz z blokowaniem wierszy
const variants = await Promise.all(
items.map(item =>
tx.$queryRaw<ProductVariant[]>`
SELECT * FROM product_variants
WHERE id = ${item.variantId}
FOR UPDATE
`
)
);
// Waliduj
for (let i = 0; i < items.length; i++) {
const variant = variants[i][0];
if (!variant || variant.stockQuantity < items[i].quantity) {
throw new Error(`Niewystarczający inwentarz`);
}
}
// 2. Utwórz zamówienie
const order = await tx.order.create({
data: {
userId,
status: 'PENDING',
subtotal: 0, // Oblicz z pozycji
tax: 0,
shipping: 0,
total: 0,
customerEmail: '', // Pobierz od użytkownika
customerName: '',
items: {
create: items.map((item, idx) => ({
variantId: item.variantId,
quantity: item.quantity,
price: variants[idx][0].price,
total: variants[idx][0].price * item.quantity,
productName: '', // Pobierz ze złączeń
variantName: variants[idx][0].name,
variantSku: variants[idx][0].sku
}))
}
},
include: {
items: true
}
});
// 3. Zaktualizuj inwentarz
await Promise.all(
items.map(item =>
tx.productVariant.update({
where: { id: item.variantId },
data: {
stockQuantity: {
decrement: item.quantity
}
}
})
)
);
// 4. Zintegruj płatność
const payment = await this.processPaymentWithRetry(
order,
userId
);
// 5. Zaktualizuj zamówienie płatnością
const finalOrder = await tx.order.update({
where: { id: order.id },
data: {
status: 'PAID',
paidAt: new Date()
},
include: {
items: {
include: {
variant: true
}
}
}
});
return finalOrder;
},
{
isolationLevel: 'Serializable',
timeout: 30000 // 30 sekund
}
);
// Sukces - wyślij powiadomienia
await this.sendOrderNotifications(order);
return order;
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error(
`Składanie zamówienia nie powiodło się po ${maxRetries} próbach: ${error.message}`
);
}
// Wykładnicze odroczenie
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, retries) * 100)
);
}
}
throw new Error('Nieoczekiwany błąd przy składaniu zamówienia');
}
private async processPaymentWithRetry(
order: Order,
userId: string,
maxAttempts: number = 2
): Promise<Payment> {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await this.paymentService.charge({
orderId: order.id,
amount: order.total,
userId,
idempotencyKey: `${order.id}-${attempt}`
});
} catch (error) {
lastError = error;
if (attempt < maxAttempts) {
await new Promise(resolve =>
setTimeout(resolve, 1000 * attempt)
);
}
}
}
throw lastError!;
}
}
// Poproś o generowanie testów z AI
"Wygeneruj kompleksowe testy jednostkowe dla UserRepository z:
- Strategiami mockowania
- Przypadkami granicznymi
- Testami wydajności
- Testowaniem migawek"
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User, Organization } from '../entities';
import { UserRepository } from './user.repository';
describe('UserRepository', () => {
let repository: UserRepository;
let mockQueryBuilder: any;
beforeEach(async () => {
// Mock query builder
mockQueryBuilder = {
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
getOne: jest.fn(),
getMany: jest.fn(),
getManyAndCount: jest.fn()
};
const module = await Test.createTestingModule({
providers: [
UserRepository,
{
provide: getRepositoryToken(User),
useValue: {
createQueryBuilder: jest.fn(() => mockQueryBuilder),
save: jest.fn(),
findOne: jest.fn(),
update: jest.fn()
}
}
]
}).compile();
repository = module.get<UserRepository>(UserRepository);
});
describe('findActiveByEmail', () => {
it('powinien znaleźć aktywnego użytkownika z organizacją', async () => {
const mockUser = {
id: '123',
email: 'test@example.com',
organization: { id: '456', name: 'Test Org' }
};
mockQueryBuilder.getOne.mockResolvedValue(mockUser);
const result = await repository.findActiveByEmail('test@example.com');
expect(mockQueryBuilder.leftJoinAndSelect)
.toHaveBeenCalledWith('user.organization', 'org');
expect(mockQueryBuilder.where)
.toHaveBeenCalledWith('user.email = :email', {
email: 'test@example.com'
});
expect(mockQueryBuilder.andWhere)
.toHaveBeenCalledWith('user.deletedAt IS NULL');
expect(result).toEqual(mockUser);
});
it('powinien zwrócić null dla nieistniejącego użytkownika', async () => {
mockQueryBuilder.getOne.mockResolvedValue(null);
const result = await repository.findActiveByEmail('none@example.com');
expect(result).toBeNull();
});
});
describe('Wydajność', () => {
it('powinien wykonać zapytania w akceptowalnym czasie', async () => {
mockQueryBuilder.getMany.mockResolvedValue([]);
const start = Date.now();
await repository.findByOrganization('123');
const duration = Date.now() - start;
expect(duration).toBeLessThan(100); // Próg 100ms
});
});
});

Śledzenie wydajności ORM

// Poproś o konfigurację monitorowania wydajności z AI
"Dodaj kompleksowe monitorowanie wydajności ORM z:
- Śledzeniem czasu wykonania zapytań
- Wykrywaniem wolnych zapytań
- Metrykami pool połączeń
- Wzorcami użycia pamięci"
// AI dostarcza rozwiązanie monitorujące:
import { Logger } from '@nestjs/common';
import { EntityManager } from 'typeorm';
// Interceptor wydajności zapytań
export class QueryLogger {
private logger = new Logger('Database');
private slowQueryThreshold = 1000; // 1 sekunda
constructor(private entityManager: EntityManager) {
this.setupLogging();
}
private setupLogging() {
const queryRunner = this.entityManager.connection.createQueryRunner();
// Nadpisz metodę query
const originalQuery = queryRunner.query.bind(queryRunner);
queryRunner.query = async (query: string, params?: any[]) => {
const start = Date.now();
const id = Math.random().toString(36).substring(7);
try {
// Loguj rozpoczęcie zapytania
this.logger.debug(`[${id}] Rozpoczynanie zapytania: ${this.truncate(query)}`);
const result = await originalQuery(query, params);
const duration = Date.now() - start;
// Loguj ukończenie
if (duration > this.slowQueryThreshold) {
this.logger.warn(
`[${id}] Wykryto wolne zapytanie (${duration}ms): ${query}`,
{ params, duration }
);
// Wyślij do serwisu monitorowania
await this.sendMetrics({
type: 'slow_query',
query,
duration,
params
});
} else {
this.logger.debug(`[${id}] Zapytanie ukończone w ${duration}ms`);
}
return result;
} catch (error) {
const duration = Date.now() - start;
this.logger.error(
`[${id}] Zapytanie nie powiodło się po ${duration}ms: ${error.message}`,
{ query, params, error }
);
throw error;
}
};
}
private truncate(query: string, maxLength = 200): string {
return query.length > maxLength
? query.substring(0, maxLength) + '...'
: query;
}
}
// Middleware wydajności Prismy
export function setupPrismaMonitoring(prisma: PrismaClient) {
// Metryki zapytań
prisma.$use(async (params, next) => {
const start = Date.now();
try {
const result = await next(params);
const duration = Date.now() - start;
// Śledź metryki
metrics.histogram('prisma.query.duration', duration, {
model: params.model,
action: params.action
});
if (duration > 1000) {
logger.warn('Wolne zapytanie Prismy', {
model: params.model,
action: params.action,
duration,
args: params.args
});
}
return result;
} catch (error) {
metrics.increment('prisma.query.error', {
model: params.model,
action: params.action,
error: error.constructor.name
});
throw error;
}
});
// Monitorowanie pool połączeń
setInterval(async () => {
const pool = await prisma.$metrics.json();
metrics.gauge('prisma.pool.active', pool.counters.find(
c => c.key === 'prisma_pool_active_connections'
)?.value || 0);
metrics.gauge('prisma.pool.idle', pool.counters.find(
c => c.key === 'prisma_pool_idle_connections'
)?.value || 0);
metrics.gauge('prisma.pool.wait', pool.counters.find(
c => c.key === 'prisma_pool_wait_count'
)?.value || 0);
}, 30000); // Co 30 sekund
}
// Śledzenie użycia pamięci
export class ORMMemoryMonitor {
private baseline: NodeJS.MemoryUsage;
constructor() {
this.baseline = process.memoryUsage();
this.startMonitoring();
}
private startMonitoring() {
setInterval(() => {
const current = process.memoryUsage();
const heapDelta = current.heapUsed - this.baseline.heapUsed;
if (heapDelta > 100 * 1024 * 1024) { // 100MB wzrost
logger.warn('Wykryto wysokie użycie pamięci', {
heapUsed: Math.round(current.heapUsed / 1024 / 1024) + 'MB',
heapDelta: Math.round(heapDelta / 1024 / 1024) + 'MB',
external: Math.round(current.external / 1024 / 1024) + 'MB'
});
}
metrics.gauge('orm.memory.heap', current.heapUsed);
metrics.gauge('orm.memory.external', current.external);
}, 60000); // Co minutę
}
}

Rozwój ORM z AI

Kluczowe wnioski:

  1. Projektowanie schematów: Użyj AI do generowania znormalizowanych schematów z właściwymi relacjami, indeksami i ograniczeniami
  2. Bezpieczeństwo typów: Wykorzystuj AI do generowania bezpiecznych typowo zapytań i wychwytywania błędów na etapie kompilacji
  3. Wydajność: Pozwól AI identyfikować zapytania N+1, sugerować eager loading i optymalizować operacje wsadowe
  4. Testowanie: Generuj kompleksowe suity testów włączając przypadki graniczne i benchmarki wydajności
  5. Monitorowanie: Implementuj solidne logowanie i metryki do śledzenia wydajności ORM w produkcji

Częste wzorce:

  • Wzorzec repozytorium dla złożonych zapytań
  • Unit of Work dla zarządzania transakcjami
  • Budownicze zapytań dla dynamicznych zapytań
  • Strategie migracji dla wdrożeń bez przestojów
  • Warstwy cachowania dla operacji ciężkich odczytowo

Antywzorce do unikania:

  • Nadmierne pobieranie danych (select *)
  • Zapytania N+1 bez eager loading
  • Brakujące indeksy na kluczach obcych
  • Zbyt duże transakcje
  • Ignorowanie limitów pool połączeń