7个关键数据访问模式:每位开发者必须掌握的可扩展应用

发布: (2025年12月13日 GMT+8 21:49)
5 min read
原文: Dev.to

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

随着应用演进,数据库模式也必须以可重复、受版本控制的方式进行更改(添加列、索引、拆分表等)。数据库迁移工具提供了一套系统化的方法,在不同环境中应用增量的模式变更。

典型工作流

  1. 创建迁移文件,描述要进行的更改(例如 add_users_last_login.sql)。
  2. 运行迁移,使用 CLI 或编程 API;工具会记录迁移已被执行。
  3. 部署 相同的迁移到 staging、testing 和 production,确保一致性。

常用的迁移库包括 Knex migrationsFlywayLiquibasePrisma Migrate。选择适合你技术栈的工具,并将其集成到 CI/CD 流程中。

Back to Blog

相关文章

阅读更多 »