7个关键数据访问模式:每位开发者必须掌握的可扩展应用
Source: Dev.to
Repository Pattern
想象一下,你的应用中每种主要数据都有一个盒子:User 盒子、Order 盒子、Product 盒子。你不关心盒子里装的是什么,也不在乎它是怎么组织的——只需要对盒子说:“给我这个邮箱的用户”,或者“保存这个新订单”。盒子会处理剩下的工作。
在代码中,Repository 是一个提供类似集合接口的类。
// UserRepository.ts
interface UserRepository {
findById(id: string): Promise;
findByEmail(email: string): Promise;
save(user: User): Promise;
delete(id: string): Promise;
}
接口是一种契约:任何实现都必须提供这些方法。
// PostgresUserRepository.ts
import { Pool } from 'pg';
class PostgresUserRepository implements UserRepository {
constructor(private pool: Pool) {}
async findById(id: string): Promise {
const result = await this.pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async findByEmail(email: string): Promise {
const result = await this.pool.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
return result.rows[0] || null;
}
async save(user: User): Promise {
await this.pool.query(
`INSERT INTO users (id, email, name)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET
email = $2, name = $3`,
[user.id, user.email, user.name]
);
}
async delete(id: string): Promise {
const result = await this.pool.query(
'DELETE FROM users WHERE id = $1',
[id]
);
return result.rowCount > 0;
}
}
你的应用代码只依赖 UserRepository。从 PostgreSQL 切换到 MySQL 只需要一个满足同一接口的新实现。
Query Builder
为复杂查询编写原始 SQL 字符串会变得难以阅读和维护。查询构建器让你通过一连串方法调用来构造查询,提高可读性和安全性。
使用 Knex.js 的示例
// Get a paginated list of active users
const activeUsers = await knex('users')
.select('id', 'email', 'created_at')
.where('status', 'active')
.whereBetween('created_at', [startDate, endDate])
.orderBy('created_at', 'desc')
.limit(20)
.offset(40);
带 Join 的聚合
// Find users who have spent more than $1000 total
const bigSpenders = await knex('orders')
.join('users', 'orders.user_id', 'users.id')
.select([
'users.email',
knex.raw('SUM(orders.total_amount) as lifetime_value')
])
.groupBy('users.email')
.having('lifetime_value', '>', 1000);
构建器会抽象不同方言(PostgreSQL、MySQL、SQLite)的差异,并自动对值进行参数化,防止 SQL 注入。
Data Mapper
数据库中的数据形态往往与应用中对象的形态不同。Data Mapper 模式将转换逻辑集中在一起。
// UserMapper.ts
class UserMapper {
toEntity(row: UserRecord): User {
// Transform snake_case to camelCase and parse dates
return new User(
row.id,
row.email,
row.full_name,
new Date(row.created_at)
);
}
toPersist(user: User): UserRecord {
// Prepare object for persistence
return {
id: user.id,
email: user.email,
full_name: user.name,
created_at: user.joinedDate.toISOString(),
};
}
}
在服务层中的使用
// UserService.ts
class UserService {
constructor(
private repository: UserRepository,
private mapper: UserMapper
) {}
async getUserProfile(id: string): Promise {
const record = await this.repository.findById(id);
if (!record) throw new Error('User not found');
const userEntity = this.mapper.toEntity(record);
return this.buildProfile(userEntity);
}
private buildProfile(user: User): UserProfile {
// Build and return a profile DTO
// ...
}
}
领域实体(User)保持纯粹,不受持久化细节的影响。
Connection Pooling
为每个查询都打开和关闭数据库连接效率极低。连接池维护一组可复用的连接。
// pool.js
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // Maximum number of connections
idleTimeoutMillis: 30000, // Close idle connections after 30 s
});
module.exports = pool;
使用连接池
// orders.js
const pool = require('./pool');
async function getUserOrders(userId) {
const client = await pool.connect(); // Checkout a connection
try {
const result = await client.query(
'SELECT * FROM orders WHERE user_id = $1',
[userId]
);
return result.rows;
} finally {
client.release(); // Return the connection to the pool
}
}
连接池显著降低延迟和资源消耗,尤其在高负载时。
Database Migrations
随着应用演进,数据库模式也必须以可重复、受版本控制的方式进行更改(添加列、索引、拆分表等)。数据库迁移工具提供了一套系统化的方法,在不同环境中应用增量的模式变更。
典型工作流:
- 创建迁移文件,描述要进行的更改(例如
add_users_last_login.sql)。 - 运行迁移,使用 CLI 或编程 API;工具会记录迁移已被执行。
- 部署 相同的迁移到 staging、testing 和 production,确保一致性。
常用的迁移库包括 Knex migrations、Flyway、Liquibase 和 Prisma Migrate。选择适合你技术栈的工具,并将其集成到 CI/CD 流程中。