Skip to content

Feathers.js 数据库适配器详解 - 支持多种数据库的秘密

发布时间:2024-06-08
作者:一介布衣
标签:Feathers.js, 数据库适配器, MongoDB, SQL, 数据库抽象

前言

前面我们学习了 Feathers.js 的核心功能,今天咱们来深入了解它的数据库适配器系统。说实话,Feathers.js 能够支持这么多种数据库,而且使用方式完全一致,这背后的适配器设计真的很巧妙。

我记得刚开始做项目的时候,经常为选择数据库而纠结:用 MySQL 还是 MongoDB?用 PostgreSQL 还是 SQLite?而且一旦选定了,后期想换就很麻烦。后来用了 Feathers.js 才发现,原来可以这么轻松地在不同数据库之间切换,甚至在同一个项目中使用多种数据库。

今天我就带大家深入了解 Feathers.js 的适配器系统,看看它是如何实现"一套代码,多种数据库"的。

适配器系统架构

统一接口设计

javascript
// 所有适配器都实现相同的接口
interface DatabaseAdapter {
  find(params)
  get(id, params)
  create(data, params)
  update(id, data, params)
  patch(id, data, params)
  remove(id, params)
}

// 无论底层是什么数据库,使用方式都一样
const users = app.service('users');
await users.find({ query: { status: 'active' } });
await users.create({ name: 'John', email: 'john@example.com' });

适配器层次结构

javascript
// 适配器继承关系
AdapterService (基类)

MemoryService (内存适配器)

FileService (文件适配器)

DatabaseService (数据库基类)

├── MongoDBService (MongoDB 适配器)
├── KnexService (SQL 适配器)
├── SequelizeService (Sequelize 适配器)
└── CustomService (自定义适配器)

官方适配器详解

1. 内存适配器(@feathersjs/memory)

bash
npm install @feathersjs/memory
javascript
// 内存适配器配置
const { MemoryService } = require('@feathersjs/memory');

app.use('todos', new MemoryService({
  // 配置选项
  multi: true,        // 允许批量操作
  id: 'id',          // 主键字段名
  startId: 1,        // 起始ID
  store: {},         // 自定义存储对象
  paginate: {
    default: 10,
    max: 50
  },
  
  // 自定义匹配器
  matcher: (query) => {
    return (item) => {
      // 自定义查询逻辑
      return Object.keys(query).every(key => {
        if (key.startsWith('$')) return true;
        return item[key] === query[key];
      });
    };
  },
  
  // 自定义排序器
  sorter: (sort) => {
    return (a, b) => {
      for (const [field, direction] of Object.entries(sort)) {
        if (a[field] < b[field]) return direction === 1 ? -1 : 1;
        if (a[field] > b[field]) return direction === 1 ? 1 : -1;
      }
      return 0;
    };
  }
}));

// 使用示例
const todosService = app.service('todos');

// 创建数据
await todosService.create([
  { title: '学习 Feathers.js', completed: false },
  { title: '写技术博客', completed: true },
  { title: '做项目实战', completed: false }
]);

// 查询数据
const activeTodos = await todosService.find({
  query: { completed: false }
});

// 复杂查询
const recentTodos = await todosService.find({
  query: {
    createdAt: { $gte: new Date('2024-01-01') },
    $sort: { createdAt: -1 },
    $limit: 5
  }
});

2. MongoDB 适配器(@feathersjs/mongodb)

bash
npm install @feathersjs/mongodb mongodb
javascript
// MongoDB 连接配置
const { MongoClient } = require('mongodb');
const { MongoDBService } = require('@feathersjs/mongodb');

module.exports = function (app) {
  const connection = app.get('mongodb');
  const database = connection.substr(connection.lastIndexOf('/') + 1);
  const mongoClient = MongoClient.connect(connection, {
    useNewUrlParser: true,
    useUnifiedTopology: true
  }).then(client => client.db(database));

  app.set('mongoClient', mongoClient);
};

// MongoDB 服务配置
const { MongoDBService } = require('@feathersjs/mongodb');

class UsersService extends MongoDBService {
  constructor(options, app) {
    super(options, app);
  }

  // 自定义查询方法
  async findWithAggregation(pipeline, params) {
    const { query = {} } = params;
    
    // 添加匹配阶段
    if (Object.keys(query).length > 0) {
      pipeline.unshift({ $match: this.objectifyQuery(query) });
    }
    
    const result = await this.Model.aggregate(pipeline).toArray();
    
    return {
      total: result.length,
      limit: params.query?.$limit || result.length,
      skip: params.query?.$skip || 0,
      data: result
    };
  }

  // 地理位置查询
  async findNearby(coordinates, maxDistance, params) {
    const pipeline = [
      {
        $geoNear: {
          near: {
            type: 'Point',
            coordinates: coordinates
          },
          distanceField: 'distance',
          maxDistance: maxDistance,
          spherical: true
        }
      }
    ];
    
    return this.findWithAggregation(pipeline, params);
  }

  // 全文搜索
  async search(searchText, params) {
    const query = {
      $text: { $search: searchText }
    };
    
    return this.find({
      ...params,
      query: { ...params.query, ...query }
    });
  }
}

// 注册服务
module.exports = function (app) {
  const options = {
    Model: app.get('mongoClient').then(db => db.collection('users')),
    paginate: app.get('paginate'),
    multi: true
  };

  app.use('/users', new UsersService(options, app));
};

3. SQL 适配器(@feathersjs/knex)

bash
npm install @feathersjs/knex knex
# 选择数据库驱动
npm install mysql2      # MySQL
npm install pg          # PostgreSQL  
npm install sqlite3     # SQLite
npm install mssql       # SQL Server
javascript
// Knex 配置
const knex = require('knex');

const db = knex({
  client: 'mysql2',
  connection: {
    host: process.env.DB_HOST || 'localhost',
    port: process.env.DB_PORT || 3306,
    user: process.env.DB_USER || 'root',
    password: process.env.DB_PASSWORD || '',
    database: process.env.DB_NAME || 'feathers_app'
  },
  pool: {
    min: 2,
    max: 10
  },
  migrations: {
    tableName: 'knex_migrations'
  }
});

app.set('knexClient', db);

// SQL 服务配置
const { KnexService } = require('@feathersjs/knex');

class PostsService extends KnexService {
  constructor(options, app) {
    super({
      ...options,
      name: 'posts'  // 表名
    }, app);
  }

  // 自定义查询构建
  createQuery(params) {
    const { query = {} } = params;
    let knexQuery = super.createQuery(params);

    // 关联查询
    if (query.$populate) {
      const populate = Array.isArray(query.$populate) ? query.$populate : [query.$populate];
      
      populate.forEach(relation => {
        switch (relation) {
          case 'author':
            knexQuery = knexQuery
              .leftJoin('users', 'posts.authorId', 'users.id')
              .select('posts.*', 'users.username as authorName', 'users.avatar as authorAvatar');
            break;
          case 'category':
            knexQuery = knexQuery
              .leftJoin('categories', 'posts.categoryId', 'categories.id')
              .select('posts.*', 'categories.name as categoryName');
            break;
        }
      });
    }

    // 全文搜索
    if (query.$search) {
      knexQuery = knexQuery.where(function() {
        this.where('title', 'like', `%\${query.$search}%`)
            .orWhere('content', 'like', `%\${query.$search}%`);
      });
    }

    // 日期范围查询
    if (query.dateFrom || query.dateTo) {
      if (query.dateFrom) {
        knexQuery = knexQuery.where('createdAt', '>=', query.dateFrom);
      }
      if (query.dateTo) {
        knexQuery = knexQuery.where('createdAt', '<=', query.dateTo);
      }
    }

    return knexQuery;
  }

  // 事务支持
  async createWithTransaction(data, params) {
    const trx = await this.Model.transaction();
    
    try {
      // 创建文章
      const post = await super.create(data, { ...params, knex: trx });
      
      // 创建标签关联
      if (data.tags && data.tags.length > 0) {
        const tagRelations = data.tags.map(tagId => ({
          postId: post.id,
          tagId: tagId
        }));
        
        await trx('post_tags').insert(tagRelations);
      }
      
      // 更新用户文章计数
      await trx('users')
        .where('id', data.authorId)
        .increment('postCount', 1);
      
      await trx.commit();
      return post;
      
    } catch (error) {
      await trx.rollback();
      throw error;
    }
  }

  // 批量操作
  async bulkUpdate(updates, params) {
    const trx = await this.Model.transaction();
    
    try {
      const results = [];
      
      for (const update of updates) {
        const result = await super.patch(update.id, update.data, { 
          ...params, 
          knex: trx 
        });
        results.push(result);
      }
      
      await trx.commit();
      return results;
      
    } catch (error) {
      await trx.rollback();
      throw error;
    }
  }
}

// 注册服务
module.exports = function (app) {
  const options = {
    Model: app.get('knexClient'),
    name: 'posts',
    paginate: app.get('paginate'),
    multi: true
  };

  app.use('/posts', new PostsService(options, app));
};

自定义适配器开发

1. 基础适配器类

javascript
// src/adapters/base-adapter.js
const { AdapterService } = require('@feathersjs/adapter-commons');

class BaseCustomAdapter extends AdapterService {
  constructor(options = {}) {
    super({
      id: 'id',
      paginate: {},
      multi: true,
      ...options
    });
  }

  // 必须实现的方法
  async _find(params) {
    throw new Error('_find 方法必须被子类实现');
  }

  async _get(id, params) {
    throw new Error('_get 方法必须被子类实现');
  }

  async _create(data, params) {
    throw new Error('_create 方法必须被子类实现');
  }

  async _update(id, data, params) {
    throw new Error('_update 方法必须被子类实现');
  }

  async _patch(id, data, params) {
    throw new Error('_patch 方法必须被子类实现');
  }

  async _remove(id, params) {
    throw new Error('_remove 方法必须被子类实现');
  }

  // 通用查询构建方法
  buildQuery(query) {
    const where = {};
    const options = {};

    Object.keys(query).forEach(key => {
      if (key.startsWith('$')) {
        // 处理特殊查询参数
        switch (key) {
          case '$limit':
            options.limit = parseInt(query[key]);
            break;
          case '$skip':
            options.offset = parseInt(query[key]);
            break;
          case '$sort':
            options.sort = query[key];
            break;
          case '$select':
            options.select = query[key];
            break;
        }
      } else {
        // 处理普通字段查询
        where[key] = this.buildFieldQuery(query[key]);
      }
    });

    return { where, options };
  }

  buildFieldQuery(value) {
    if (typeof value === 'object' && value !== null) {
      // 处理复杂查询操作符
      const conditions = {};
      
      Object.keys(value).forEach(operator => {
        switch (operator) {
          case '$gt':
          case '$gte':
          case '$lt':
          case '$lte':
          case '$ne':
          case '$in':
          case '$nin':
            conditions[operator] = value[operator];
            break;
        }
      });
      
      return Object.keys(conditions).length > 0 ? conditions : value;
    }
    
    return value;
  }

  // 分页处理
  async paginate(query, countQuery) {
    const { where, options } = this.buildQuery(query);
    
    const [data, total] = await Promise.all([
      this.executeQuery(where, options),
      this.executeCount(countQuery || where)
    ]);

    return {
      total,
      limit: options.limit || total,
      skip: options.offset || 0,
      data
    };
  }

  // 子类需要实现的查询执行方法
  async executeQuery(where, options) {
    throw new Error('executeQuery 方法必须被子类实现');
  }

  async executeCount(where) {
    throw new Error('executeCount 方法必须被子类实现');
  }
}

module.exports = BaseCustomAdapter;

2. Redis 适配器示例

javascript
// src/adapters/redis-adapter.js
const Redis = require('redis');
const BaseCustomAdapter = require('./base-adapter');

class RedisAdapter extends BaseCustomAdapter {
  constructor(options = {}) {
    super(options);
    
    this.redis = Redis.createClient(options.redis || {});
    this.keyPrefix = options.keyPrefix || 'feathers:';
    this.hashKey = options.hashKey || 'data';
    
    this.redis.on('error', (err) => {
      console.error('Redis 连接错误:', err);
    });
  }

  getKey(id) {
    return `\${this.keyPrefix}\${id}`;
  }

  getIndexKey() {
    return `\${this.keyPrefix}index`;
  }

  async _find(params) {
    const { query = {} } = params;
    
    // 获取所有ID
    const ids = await this.redis.smembers(this.getIndexKey());
    
    if (ids.length === 0) {
      return {
        total: 0,
        limit: query.$limit || 0,
        skip: query.$skip || 0,
        data: []
      };
    }

    // 批量获取数据
    const pipeline = this.redis.pipeline();
    ids.forEach(id => {
      pipeline.hgetall(this.getKey(id));
    });
    
    const results = await pipeline.exec();
    let data = results
      .map(([err, result]) => err ? null : this.deserialize(result))
      .filter(item => item !== null);

    // 应用过滤
    data = this.applyFilters(data, query);

    // 应用排序
    if (query.$sort) {
      data = this.applySorting(data, query.$sort);
    }

    // 应用分页
    const total = data.length;
    const skip = query.$skip || 0;
    const limit = query.$limit || total;
    
    data = data.slice(skip, skip + limit);

    return {
      total,
      limit,
      skip,
      data
    };
  }

  async _get(id, params) {
    const data = await this.redis.hgetall(this.getKey(id));
    
    if (!data || Object.keys(data).length === 0) {
      throw new Error(`记录 \${id} 不存在`);
    }
    
    return this.deserialize(data);
  }

  async _create(data, params) {
    const id = data[this.id] || this.generateId();
    const record = {
      ...data,
      [this.id]: id,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };

    const serialized = this.serialize(record);
    
    // 使用事务确保原子性
    const multi = this.redis.multi();
    multi.hmset(this.getKey(id), serialized);
    multi.sadd(this.getIndexKey(), id);
    
    await multi.exec();
    
    return record;
  }

  async _update(id, data, params) {
    // 检查记录是否存在
    await this._get(id, params);
    
    const record = {
      ...data,
      [this.id]: id,
      updatedAt: new Date().toISOString()
    };

    const serialized = this.serialize(record);
    await this.redis.hmset(this.getKey(id), serialized);
    
    return record;
  }

  async _patch(id, data, params) {
    const existing = await this._get(id, params);
    
    const record = {
      ...existing,
      ...data,
      [this.id]: id,
      updatedAt: new Date().toISOString()
    };

    const serialized = this.serialize(record);
    await this.redis.hmset(this.getKey(id), serialized);
    
    return record;
  }

  async _remove(id, params) {
    const record = await this._get(id, params);
    
    // 使用事务删除
    const multi = this.redis.multi();
    multi.del(this.getKey(id));
    multi.srem(this.getIndexKey(), id);
    
    await multi.exec();
    
    return record;
  }

  // 辅助方法
  serialize(data) {
    const serialized = {};
    Object.keys(data).forEach(key => {
      const value = data[key];
      if (typeof value === 'object') {
        serialized[key] = JSON.stringify(value);
      } else {
        serialized[key] = String(value);
      }
    });
    return serialized;
  }

  deserialize(data) {
    const deserialized = {};
    Object.keys(data).forEach(key => {
      try {
        deserialized[key] = JSON.parse(data[key]);
      } catch (error) {
        deserialized[key] = data[key];
      }
    });
    return deserialized;
  }

  applyFilters(data, query) {
    return data.filter(item => {
      return Object.keys(query).every(key => {
        if (key.startsWith('$')) return true;
        
        const queryValue = query[key];
        const itemValue = item[key];
        
        if (typeof queryValue === 'object') {
          return this.matchComplexQuery(itemValue, queryValue);
        }
        
        return itemValue === queryValue;
      });
    });
  }

  matchComplexQuery(value, query) {
    if (query.$gt !== undefined) return value > query.$gt;
    if (query.$gte !== undefined) return value >= query.$gte;
    if (query.$lt !== undefined) return value < query.$lt;
    if (query.$lte !== undefined) return value <= query.$lte;
    if (query.$ne !== undefined) return value !== query.$ne;
    if (query.$in !== undefined) return query.$in.includes(value);
    if (query.$nin !== undefined) return !query.$nin.includes(value);
    return true;
  }

  applySorting(data, sortQuery) {
    return data.sort((a, b) => {
      for (const [field, direction] of Object.entries(sortQuery)) {
        const aVal = a[field];
        const bVal = b[field];
        
        if (aVal < bVal) return direction === 1 ? -1 : 1;
        if (aVal > bVal) return direction === 1 ? 1 : -1;
      }
      return 0;
    });
  }

  generateId() {
    return Date.now().toString() + Math.random().toString(36).substr(2, 9);
  }

  // 清理方法
  async clear() {
    const ids = await this.redis.smembers(this.getIndexKey());
    
    if (ids.length > 0) {
      const multi = this.redis.multi();
      ids.forEach(id => {
        multi.del(this.getKey(id));
      });
      multi.del(this.getIndexKey());
      
      await multi.exec();
    }
    
    return { message: `清理了 \${ids.length} 条记录` };
  }

  // 统计信息
  async getStats() {
    const totalKeys = await this.redis.scard(this.getIndexKey());
    const memoryUsage = await this.redis.memory('usage', this.getIndexKey());
    
    return {
      totalRecords: totalKeys,
      memoryUsage: memoryUsage,
      keyPrefix: this.keyPrefix
    };
  }
}

module.exports = RedisAdapter;

3. 适配器注册和使用

javascript
// src/services/cache/cache.service.js
const RedisAdapter = require('../../adapters/redis-adapter');
const hooks = require('./cache.hooks');

module.exports = function (app) {
  const options = {
    redis: {
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD
    },
    keyPrefix: 'cache:',
    paginate: app.get('paginate')
  };

  // 注册 Redis 适配器服务
  app.use('/cache', new RedisAdapter(options));

  // 获取服务实例并配置钩子
  const service = app.service('cache');
  service.hooks(hooks);

  // 添加自定义方法
  service.clear = function() {
    return this.clear();
  };

  service.getStats = function() {
    return this.getStats();
  };
};

// 使用示例
const cacheService = app.service('cache');

// 缓存数据
await cacheService.create({
  id: 'user:123',
  data: { name: 'John', email: 'john@example.com' },
  ttl: 3600
});

// 获取缓存
const cached = await cacheService.get('user:123');

// 查询缓存
const results = await cacheService.find({
  query: { 
    pattern: 'user:*',
    $limit: 10 
  }
});

多数据库混合使用

1. 不同服务使用不同数据库

javascript
// src/services/index.js
const users = require('./users/users.service.js');           // MongoDB
const posts = require('./posts/posts.service.js');           // MySQL
const cache = require('./cache/cache.service.js');           // Redis
const logs = require('./logs/logs.service.js');              // Elasticsearch
const files = require('./files/files.service.js');          // 文件系统

module.exports = function (app) {
  // 每个服务使用最适合的数据库
  app.configure(users);    // 用户数据 → MongoDB
  app.configure(posts);    // 文章数据 → MySQL
  app.configure(cache);    // 缓存数据 → Redis
  app.configure(logs);     // 日志数据 → Elasticsearch
  app.configure(files);    // 文件数据 → 文件系统
};

2. 数据同步策略

javascript
// src/hooks/data-sync.js
const syncToCache = (cacheKey, ttl = 3600) => {
  return async (context) => {
    if (context.method === 'get' || context.method === 'find') {
      // 读取时同步到缓存
      try {
        await context.app.service('cache').create({
          id: typeof cacheKey === 'function' ? cacheKey(context) : cacheKey,
          data: context.result,
          ttl
        });
      } catch (error) {
        console.error('缓存同步失败:', error);
      }
    }
    
    return context;
  };
};

const invalidateCache = (cachePattern) => {
  return async (context) => {
    if (['create', 'update', 'patch', 'remove'].includes(context.method)) {
      try {
        const pattern = typeof cachePattern === 'function' ? cachePattern(context) : cachePattern;
        
        // 查找匹配的缓存键
        const cacheKeys = await context.app.service('cache').find({
          query: { pattern }
        });
        
        // 删除匹配的缓存
        for (const key of cacheKeys.data) {
          await context.app.service('cache').remove(key.id);
        }
      } catch (error) {
        console.error('缓存失效失败:', error);
      }
    }
    
    return context;
  };
};

// 使用数据同步钩子
app.service('users').hooks({
  after: {
    get: [syncToCache((context) => `user:\${context.id}`, 1800)],
    find: [syncToCache('users:list', 300)],
    create: [invalidateCache('users:*')],
    update: [invalidateCache((context) => `user:\${context.id}`)],
    patch: [invalidateCache((context) => `user:\${context.id}`)],
    remove: [invalidateCache((context) => `user:\${context.id}`)]
  }
});

适配器性能优化

1. 连接池优化

javascript
// MongoDB 连接池优化
const mongoClient = MongoClient.connect(connection, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  maxPoolSize: 10,        // 最大连接数
  minPoolSize: 2,         // 最小连接数
  maxIdleTimeMS: 30000,   // 连接空闲时间
  serverSelectionTimeoutMS: 5000,  // 服务器选择超时
  socketTimeoutMS: 45000, // Socket 超时
  bufferMaxEntries: 0,    // 禁用缓冲
  bufferCommands: false   // 禁用命令缓冲
});

// SQL 连接池优化
const knex = require('knex')({
  client: 'mysql2',
  connection: {
    host: 'localhost',
    user: 'root',
    password: '',
    database: 'test'
  },
  pool: {
    min: 2,
    max: 10,
    createTimeoutMillis: 3000,
    acquireTimeoutMillis: 30000,
    idleTimeoutMillis: 30000,
    reapIntervalMillis: 1000,
    createRetryIntervalMillis: 100,
    propagateCreateError: false
  }
});

2. 查询优化

javascript
// 查询缓存适配器
class CachedAdapter extends BaseCustomAdapter {
  constructor(options) {
    super(options);
    this.cache = new Map();
    this.cacheTimeout = options.cacheTimeout || 60000;
  }

  async _find(params) {
    const cacheKey = this.getCacheKey('find', params);

    // 检查缓存
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);
      if (Date.now() - cached.timestamp < this.cacheTimeout) {
        return cached.data;
      }
      this.cache.delete(cacheKey);
    }

    // 执行查询
    const result = await super._find(params);

    // 缓存结果
    this.cache.set(cacheKey, {
      data: result,
      timestamp: Date.now()
    });

    return result;
  }

  getCacheKey(method, params) {
    return `\${method}:\${JSON.stringify(params.query || {})}`;
  }

  clearCache() {
    this.cache.clear();
  }
}

3. 批量操作优化

javascript
// 批量操作适配器
class BatchAdapter extends BaseCustomAdapter {
  constructor(options) {
    super(options);
    this.batchSize = options.batchSize || 100;
    this.batchTimeout = options.batchTimeout || 1000;
    this.pendingOperations = [];
    this.batchTimer = null;
  }

  async _create(data, params) {
    if (Array.isArray(data)) {
      return this.batchCreate(data, params);
    }

    return super._create(data, params);
  }

  async batchCreate(dataArray, params) {
    const results = [];

    // 分批处理
    for (let i = 0; i < dataArray.length; i += this.batchSize) {
      const batch = dataArray.slice(i, i + this.batchSize);
      const batchResults = await this.processBatch(batch, params);
      results.push(...batchResults);
    }

    return results;
  }

  async processBatch(batch, params) {
    // 子类实现具体的批量处理逻辑
    return Promise.all(batch.map(item => super._create(item, params)));
  }
}

适配器测试

1. 适配器测试框架

javascript
// test/adapters/adapter-test-suite.js
const assert = require('assert');

class AdapterTestSuite {
  constructor(AdapterClass, options = {}) {
    this.AdapterClass = AdapterClass;
    this.options = options;
    this.adapter = null;
  }

  async setup() {
    this.adapter = new this.AdapterClass(this.options);
    await this.adapter.setup?.();
  }

  async teardown() {
    await this.adapter.teardown?.();
  }

  async runAllTests() {
    await this.setup();

    try {
      await this.testBasicCRUD();
      await this.testQueryOperators();
      await this.testPagination();
      await this.testSorting();
      await this.testBatchOperations();
      console.log('所有测试通过!');
    } finally {
      await this.teardown();
    }
  }

  async testBasicCRUD() {
    console.log('测试基础 CRUD 操作...');

    // 测试创建
    const created = await this.adapter.create({
      name: 'Test Item',
      value: 42
    });
    assert(created.id, '创建的记录应该有 ID');
    assert.equal(created.name, 'Test Item');

    // 测试获取
    const retrieved = await this.adapter.get(created.id);
    assert.equal(retrieved.name, 'Test Item');

    // 测试更新
    const updated = await this.adapter.patch(created.id, {
      name: 'Updated Item'
    });
    assert.equal(updated.name, 'Updated Item');

    // 测试删除
    const removed = await this.adapter.remove(created.id);
    assert.equal(removed.id, created.id);

    // 验证删除
    try {
      await this.adapter.get(created.id);
      assert.fail('删除的记录不应该存在');
    } catch (error) {
      // 预期的错误
    }
  }

  async testQueryOperators() {
    console.log('测试查询操作符...');

    // 创建测试数据
    const testData = [
      { name: 'Item 1', value: 10 },
      { name: 'Item 2', value: 20 },
      { name: 'Item 3', value: 30 }
    ];

    const created = await Promise.all(
      testData.map(item => this.adapter.create(item))
    );

    try {
      // 测试 $gt 操作符
      const gtResults = await this.adapter.find({
        query: { value: { $gt: 15 } }
      });
      assert.equal(gtResults.data.length, 2);

      // 测试 $in 操作符
      const inResults = await this.adapter.find({
        query: { value: { $in: [10, 30] } }
      });
      assert.equal(inResults.data.length, 2);

    } finally {
      // 清理测试数据
      await Promise.all(
        created.map(item => this.adapter.remove(item.id))
      );
    }
  }

  async testPagination() {
    console.log('测试分页功能...');

    // 创建测试数据
    const testData = Array.from({ length: 25 }, (_, i) => ({
      name: `Item \${i + 1}`,
      value: i + 1
    }));

    const created = await Promise.all(
      testData.map(item => this.adapter.create(item))
    );

    try {
      // 测试分页
      const page1 = await this.adapter.find({
        query: { $limit: 10, $skip: 0 }
      });
      assert.equal(page1.data.length, 10);
      assert.equal(page1.total, 25);

      const page2 = await this.adapter.find({
        query: { $limit: 10, $skip: 10 }
      });
      assert.equal(page2.data.length, 10);
      assert.equal(page2.skip, 10);

    } finally {
      // 清理测试数据
      await Promise.all(
        created.map(item => this.adapter.remove(item.id))
      );
    }
  }
}

// 使用测试套件
const RedisAdapter = require('../src/adapters/redis-adapter');

async function testRedisAdapter() {
  const testSuite = new AdapterTestSuite(RedisAdapter, {
    redis: { host: 'localhost', port: 6379 },
    keyPrefix: 'test:'
  });

  await testSuite.runAllTests();
}

testRedisAdapter().catch(console.error);

总结

通过这篇文章,我们深入学习了 Feathers.js 的数据库适配器系统:

适配器系统架构

  • 统一接口设计原理
  • 适配器层次结构
  • 抽象层的作用

官方适配器详解

  • 内存适配器的配置和使用
  • MongoDB 适配器的高级功能
  • SQL 适配器的复杂查询

自定义适配器开发

  • 基础适配器类设计
  • Redis 适配器完整实现
  • 适配器注册和使用

多数据库混合使用

  • 不同服务使用不同数据库
  • 数据同步和缓存策略
  • 性能优化技巧

性能优化和测试

  • 连接池优化配置
  • 查询缓存和批量操作
  • 完整的测试框架

掌握了适配器系统,你就能够:

  • 灵活选择最适合的数据库
  • 轻松在不同数据库间切换
  • 开发自定义的数据存储方案
  • 构建高性能的混合数据架构
  • 确保适配器的质量和稳定性

下一篇文章,我们将深入学习 MongoDB 适配器的高级用法。


相关文章推荐:

有问题欢迎留言讨论,我会及时回复大家!