# 创建数据库适配器 (/docs/guides/create-a-db-adapter)

了解如何为 Better-Auth 创建自定义数据库适配器



了解如何使用 `createAdapterFactory` 为 Better-Auth 创建自定义数据库适配器。

我们的 `createAdapterFactory` 函数设计得非常灵活，我们尽力让它易于理解和使用。\
我们希望您可以专注于编写数据库逻辑，而无需担心适配器如何与 Better-Auth 配合工作。

从自定义模式配置、自定义 ID 生成、安全 JSON 解析、键映射、连接查询等，都由 `createAdapterFactory` 函数处理。\
您只需提供数据库逻辑，`createAdapterFactory` 函数将处理剩余工作。

## 快速开始 [#快速开始]

<Steps>
  <Step>
    ### 准备工作 [#准备工作]

    1. 导入 `createAdapterFactory`。
    2. 创建 `CustomAdapterConfig` 接口，代表您的适配器配置选项。
    3. 创建适配器！

    ```ts
    import { createAdapterFactory, type DBAdapterDebugLogOption } from "better-auth/adapters";

    // 您的自定义适配器配置选项
    interface CustomAdapterConfig {
      /**
       * 帮助您调试适配器问题。
       */
      debugLogs?: DBAdapterDebugLogOption;
      /**
       * 架构中的表名是否为复数形式。
       */
      usePlural?: boolean;
    }

    export const myAdapter = (config: CustomAdapterConfig = {}) =>
      createAdapterFactory({
        // ...
      });
    ```
  </Step>

  <Step>
    ### 配置适配器 [#配置适配器]

    `config` 对象主要用于向 Better-Auth 提供适配器相关信息。
    我们尽量减少在适配器函数中需要编写的代码量，这些 `config` 选项用于帮助我们实现这一点。

    ```ts
    // ...
    export const myAdapter = (config: CustomAdapterConfig = {}) =>
      createAdapterFactory({
        config: {
          adapterId: "custom-adapter", // 适配器的唯一标识符。
          adapterName: "Custom Adapter", // 适配器的名称。
          usePlural: config.usePlural ?? false, // 架构中的表名是否为复数。
          debugLogs: config.debugLogs ?? false, // 是否启用调试日志。
          supportsJSON: false, // 数据库是否支持 JSON。（默认：false）
          supportsDates: true, // 数据库是否支持日期。（默认：true）
          supportsBooleans: true, // 数据库是否支持布尔值。（默认：true）
          supportsNumericIds: true, // 数据库是否支持自动递增的数字 ID。（默认：true）
        },
        // ...
      });
    ```
  </Step>

  <Step>
    ### 创建适配器 [#创建适配器]

    `adapter` 函数是您编写与数据库交互代码的地方。

    ```ts
    // ...
    export const myAdapter = (config: CustomAdapterConfig = {}) =>
      createAdapterFactory({
        config: {
          // ...
        },
        adapter: ({}) => {
          return {
            create: async ({ data, model, select }) => {
              // ...
            },
            update: async ({ model, where, update }) => {
              // ...
            },
            updateMany: async ({ model, where, update }) => {
              // ...
            },
            delete: async ({ model, where }) => {
              // ...
            },
            // ...
          };
        },
      });
    ```

    <Callout>
      了解更多关于[数据库适配器](/docs/concepts/database#适配器)的信息。
    </Callout>
  </Step>
</Steps>

## 适配器 [#适配器]

`adapter` 函数是您编写与数据库交互代码的地方。

如果您还没有，建议先查看[配置部分](#config)中的 `options` 对象，它对您的适配器可能非常有用。

在深入适配器函数之前，让我们先了解可用的参数。

* `options`：Better Auth 选项。
* `schema`：来自用户 Better Auth 实例的架构。
* `debugLog`：调试日志函数。
* `getModelName`：用于获取数据库中转换后模型名称的函数。
* `getFieldName`：用于获取数据库中转换后字段名称的函数。
* `getDefaultModelName`：用于从架构中获取默认模型名称的函数。
* `getDefaultFieldName`：用于从架构中获取默认字段名称的函数。
* `getFieldAttributes`：用于获取特定模型和字段的字段属性的函数。
* `transformInput`：用于在保存到数据库之前转换输入数据的函数。
* `transformOutput`：用于从数据库检索后转换输出数据的函数。
* `transformWhereClause`：用于转换数据库查询 `where` 子句的函数。

```ts title="示例"
adapter: ({
  options,
  schema,
  debugLog,
  getModelName,
  getFieldName,
  getDefaultModelName,
  getDefaultFieldName,
  getFieldAttributes,
  transformInput,
  transformOutput,
  transformWhereClause,
}) => {
  return {
    // ...
  };
};
```

### 适配器方法 [#适配器方法]

* `model` 是用于数据库操作的模型或表名。
* `modelKey` 是该模型在 Better Auth 架构中的键。
* 我们会根据用户的 `schema` 配置，自动填充您返回的任何缺失字段。
* 任何包含 `select` 参数的方法，都只是为了更高效地从数据库获取数据。您不必担心只返回 `select` 参数指定的内容，因为我们会为您处理。

当适配器需要架构元数据时，请解析一次键，并将其与 `getFieldName` 等辅助函数一起使用：

```ts
const resolvedModelKey = modelKey ?? getDefaultModelName(model);
const fieldName = getFieldName({ model: resolvedModelKey, field: "email" });
```

### `create` 方法 [#create-方法]

`create` 方法用于在数据库中创建新记录。

<Callout>
  可以将 `forceAllowId` 作为参数传递给 `create` 方法，这允许在 `data` 对象中提供 `id`。
  我们在内部处理 `forceAllowId`，因此您不需要担心它。
</Callout>

参数：

* `model`：新数据将插入的模型／表名。
* `data`：要插入数据库的数据。
* `select`：要从数据库返回的字段数组。

**返回值** `Promise<T>`：插入到数据库中的记录。

```ts title="示例"
create: async ({ data, model, select }) => {
  // 插入数据的示例
  return await db.insert(model).values(data);
};
```

### `update` 方法 [#update-方法]

`update` 方法用于更新数据库中的一条记录。

参数：

* `model`：记录将更新的模型／表名。
* `where`：用于更新记录的 `where` 子句。
* `update`：用于更新记录的数据。

**返回值** `Promise<T | null>`：更新后的行，如果没有行匹配则为 `null`。对于多个 `where` 子句，某些适配器无法返回它。

```ts title="示例"
update: async ({ model, where, update }) => {
  // 更新数据的示例
  return await db.update(model).set(update).where(where);
};
```

### `updateMany` 方法 [#updatemany-方法]

`updateMany` 方法用于批量更新数据库中的多条记录。

参数：

* `model`：记录将更新的模型／表名。
* `where`：用于更新记录的 `where` 子句。
* `update`：用于更新记录的数据。

**返回值** `Promise<number>`：已更新的记录数量。

```ts title="示例"
updateMany: async ({ model, where, update }) => {
  // 批量更新数据的示例
  return await db.update(model).set(update).where(where);
};
```

### `delete` 方法 [#delete-方法]

`delete` 方法用于删除数据库中的一条记录。

参数：

* `model`：记录将从其中删除的模型／表名。
* `where`：用于删除记录的 `where` 子句。

**返回值** `Promise<void>`：无返回值。

```ts title="示例"
delete: async ({ model, where }) => {
  // 删除记录的示例
  await db.delete(model).where(where);
}
```

### `deleteMany` 方法 [#deletemany-方法]

`deleteMany` 方法用于批量删除数据库中的多条记录。

参数：

* `model`：记录将从其中删除的模型／表名。
* `where`：用于删除记录的 `where` 子句。

**返回值** `Promise<number>`：已删除的记录数量。

```ts title="示例"
deleteMany: async ({ model, where }) => {
  // 批量删除记录的示例
  return await db.delete(model).where(where);
};
```

### `consumeOne` 方法 [#consumeone-方法]

`consumeOne` 方法以原子方式删除一条匹配的记录并返回该记录，因此两个并发请求无法同时消费同一行。它用于一次性验证挑战等单次使用的凭据。

参数：

* `model`：要从中消费记录的模型／表名。
* `where`：用于匹配单条记录的 `where` 子句。

**返回值** `Promise<T | null>`：已消费的记录；如果没有匹配到任何行（包括另一请求已先一步消费时）则为 `null`。实现时最多只能删除一条匹配行。

```ts title="示例"
consumeOne: async ({ model, where }) => {
  // 单条 DELETE ... RETURNING 是原子的，因此只有一个调用者能拿到该行。
  const [row] = await db.delete(model).where(where).returning();
  return row ?? null;
};
```

<Callout type="warn">
  `consumeOne` 是可选的。如果不提供，Better Auth 会使用条件删除，并且仅在恰好删除一行时返回该记录。
</Callout>

### `incrementOne` 方法 [#incrementone-方法]

`incrementOne` 方法通过应用数值增量和可选的设置值，以原子方式更新一条匹配的记录，然后返回更新后的行。它用于速率限制、配额和单一获胜者状态转换等受保护的计数器。

参数：

* `model`：要更新的模型／表名。
* `where`：增量操作生效时仍必须匹配的 `where` 子句。
* `increment`：要应用的数值增量，例如 `{ count: 1 }` 或 `{ remaining: -1 }`。
* `set`：要在同一原子操作中设置的可选字段。`undefined` 表示保持字段不变，而 `null` 会清除字段。

**返回值** `Promise<T | null>`：更新后的记录；如果没有匹配的行则为 `null`。实现时最多只能更新一条匹配行。

```ts title="示例"
incrementOne: async ({ model, where, increment, set }) => {
  const [row] = await db
    .update(model)
    .set({
      ...set,
      count: sql`${model.count} + ${increment.count ?? 0}`,
    })
    .where(where)
    .returning();
  return row ?? null;
};
```

<Callout type="warn">
  `incrementOne` 是可选的。如果不提供，Better Auth 会使用条件更新并进行有限次数的重试。如果重试次数用尽，操作将抛出异常，且不会记录此次增量。对于竞争激烈的计数器以及失败尝试次数限制等安全控制，请使用原生增量操作。
</Callout>

回退实现会复用适配器的 CRUD 方法。它们需要完整、稳定且包含映射后 ID 的行快照、原子的条件写入以及精确的受影响行数。对于 `null` 的比较也必须能够匹配不存在的可空字段。

回退计数器的值必须是有限数字；值为 `null` 或缺失时从零开始。对于 `bigint` 计数器、结构化值上的 OR 谓词，或由触发器生成的返回值，请使用原生方法。增量回退中未更新的字段将反映初始读取的值。

### `findOne` 方法 [#findone-方法]

`findOne` 方法用于查找数据库中的单条记录。

参数：

* `model`：记录将查找的模型／表名。
* `where`：用于查找记录的 `where` 子句。
* `select`：要返回的选择子句。
* `join`：可选的连接配置，用于在一个查询中获取相关记录。

**返回值** `Promise<T | null>`：匹配的记录，如果未找到则为 `null`。

```ts title="示例"
findOne: async ({ model, where, select, join }) => {
  // 查找单条记录的示例
  return await db.select().from(model).where(where).limit(1);
};
```

### `findMany` 方法 [#findmany-方法]

`findMany` 方法用于查找数据库中的多条记录。

参数：

* `model`：记录将查找的模型／表名。
* `where`：用于查找记录的 `where` 子句。
* `limit`：返回记录的上限。
* `select`：要返回的 `select` 子句。
* `sortBy`：用于对记录排序的 `sortBy` 子句。
* `offset`：返回记录的偏移量。
* `join`：可选的连接配置，用于在一个查询中获取相关记录。

**返回值** `Promise<T[]>`：匹配记录的数组。

```ts title="示例"
findMany: async ({ model, where, limit, select, sortBy, offset, join }) => {
  // 在数据库中查找多条记录的示例。
  return await db
    .select()
    .from(model)
    .where(where)
    .limit(limit)
    .offset(offset)
    .orderBy(sortBy);
};
```

### `count` 方法 [#count-方法]

`count` 方法用于统计数据库中记录的数量。

参数：

* `model`：记录将统计的模型／表名。
* `where`：用于统计记录的 `where` 子句。

**返回值** `Promise<number>`：被统计的记录数量。

```ts title="示例"
count: async ({ model, where }) => {
  // 统计记录数的示例
  return await db.select().from(model).where(where).count();
};
```

### `options`（可选） [#options可选]

`options` 对象用于传递自定义适配器配置中可能包含的任何配置。

```ts title="示例"
const myAdapter = (config: CustomAdapterConfig) =>
  createAdapterFactory({
    config: {
      // ...
    },
    adapter: ({ options }) => {
      return {
        options: config,
      };
    },
  });
```

### `createSchema`（可选） [#createschema可选]

`createSchema` 方法允许 [Better Auth CLI](/docs/concepts/cli) 为数据库[生成](/docs/concepts/cli#生成-generate)模式。

参数：

* `tables`：用户 Better-Auth 实例架构中的表；期望生成到架构文件中。
* `file`：用户在 `generate` 命令中可能传入的文件，作为预期的架构文件输出路径。

```ts title="示例"
createSchema: async ({ file, tables }) => {
  // ... 为数据库自定义生成模式的逻辑。
};
```

## 测试您的适配器 [#测试您的适配器]

我们提供了一个通过 `@better-auth/test-utils` 包的测试套件，您可以用它来测试您的适配器。它需要您使用 `vitest`。

首先安装测试工具：

```bash
npm install -D @better-auth/test-utils
```

然后使用 `testAdapter` 和 `createTestSuite` 创建测试文件：

```ts title="my-adapter.test.ts"
import { testAdapter, createTestSuite } from "@better-auth/test-utils/adapter";
import { myAdapter } from "./my-adapter";

const normalTestSuite = createTestSuite("Normal", ({ test, adapter }) => [
  test("should create and find a user", async () => {
    // 在这里使用 adapter 实例编写您的适配器测试
  }),
]);

const { execute } = await testAdapter({
  adapter: (options) => {
    return myAdapter(/* 您的适配器配置 */);
  },
  runMigrations: async (options) => {
    // 在这里运行数据库迁移
  },
  tests: [
    normalTestSuite(),
  ],
  async onFinish() {
    // 可选：所有测试完成后的清理（例如删除数据库文件）
  },
});

execute();
```

`testAdapter` 函数为您处理测试生命周期，包括在测试前运行迁移和测试完成后清理所有表。

<Callout type="info">
  在 v1.4 版本中，适配器测试使用了 `better-auth/adapters/test` 中的 `runAdapterTest` 和 `runNumberIdAdapterTest`，该导出在 v1.5 中已移除。请改用 `@better-auth/test-utils/adapter`。
</Callout>

## 配置 [#配置]

`config` 对象用于向 Better-Auth 提供关于适配器的信息。

我们**强烈建议**认真阅读以下各项配置选项，它们将帮助你正确配置适配器。

### 必需配置 [#必需配置]

### `adapterId` [#adapterid]

适配器的唯一标识符。

### `adapterName` [#adaptername]

适配器的名称。

### 可选配置 [#可选配置]

### `supportsNumericIds` [#supportsnumericids]

数据库是否支持数字 ID。如果设为 `false`，而用户配置启用了 `useNumberId`，则会抛出错误。

### `supportsUUIDs` [#supportsuuids]

数据库是否可以原生生成 UUID。默认为 `false`。如果你的数据库会自行生成 UUID，请将此项设为 `true`。

### `supportsJSON` [#supportsjson]

数据库是否支持 JSON。如果不支持，我们将使用字符串保存 JSON 数据，并在取出时安全地将字符串解析回 JSON 对象。

### `supportsDates` [#supportsdates]

数据库是否支持日期。如果不支持，我们将把日期保存为字符串（ISO 字符串），取出时安全地解析回 `Date` 对象。

### `supportsBooleans` [#supportsbooleans]

数据库是否支持布尔值。如果不支持，将使用 `0` 或 `1` 保存布尔值，取出时安全解析回布尔值。

### `supportsArrays` [#supportsarrays]

数据库是否支持数组。默认为 `false`，在这种情况下，数组字段（`string[]`、`number[]`）会以序列化后的 `string` 形式存储，并在读取时安全地解析回来。如果你的数据库原生支持数组列，请将此项设为 `true`。

### `usePlural` [#useplural]

模式中的表名是否为复数形式。通常由用户定义，并通过你的自定义适配器选项传递。如果你不打算允许用户自定义表名，可以忽略此选项或设为 `false`。

```ts title="示例"
const adapter = myAdapter({
  // 该值会传递至 createAdapterFactory 的 `config` 对象中的 `usePlural`
  usePlural: true,
});
```

### `transaction` [#transaction]

适配器是否支持事务。如果为 `false`，操作顺序执行；否则需提供一个函数，用于执行带 `TransactionAdapter` 的回调。

设置为 `false` 时，即使同一请求的后续步骤失败，已执行的写入也会保留。因此，Better Auth 仍会运行该写入的 `after` [数据库钩子](/docs/concepts/database#database-hooks)，并在返回错误前清理二级存储。

<Callout type="warn">
  如果你的数据库不支持事务，错误处理和回滚不会那么健壮。建议使用支持事务的数据库以保证数据完整性。
</Callout>

### `debugLogs` [#debuglogs]

用于启用适配器的调试日志。你可以传入布尔值，也可以传入包含以下键的对象：`create`、`update`、`updateMany`、`findOne`、`findMany`、`delete`、`deleteMany`、`consumeOne`、`incrementOne`、`count`。
如果任意键为 `true`，则会为相应方法启用调试日志。

```ts title="示例"
// 为所有方法启用调试日志。
const adapter = myAdapter({
  debugLogs: true,
});
```

```ts title="示例"
// 仅为 `create` 和 `update` 方法启用调试日志。
const adapter = myAdapter({
  debugLogs: {
    create: true,
    update: true,
  },
});
```

### `disableIdGeneration` [#disableidgeneration]

是否禁用 ID 生成。如果设为 `true`，则忽略用户的 `generateId` 选项。

### `customIdGenerator` [#customidgenerator]

如果你的数据库只支持特定的自定义 ID 生成，可以使用此选项生成自己的 ID。

### `mapKeysTransformInput` [#mapkeystransforminput]

如果数据库在某些情况下使用不同的键名，可以用此选项映射键名。适用于数据库期望某些键名不同的情况。\
例如，MongoDB 使用 `_id`，而 Better-Auth 使用 `id`。

返回对象中的每个键表示待替换的旧键，值为新键。

可以是部分对象，仅转换部分键。

```ts title="示例"
mapKeysTransformInput: {
  id: "_id", // 我们希望将 `id` 替换为 `_id` 以保存进 MongoDB
},
```

### `mapKeysTransformOutput` [#mapkeystransformoutput]

如果数据库在某些情况下使用不同的键名，可以用此选项映射键名。适用于数据库使用不同键名的场景。\
例如，MongoDB 使用 `_id`，而 Better-Auth 使用 `id`。

返回对象中的每个键表示待替换的旧键，值为新键。

可以是部分对象，仅转换部分键。

```ts title="示例"
mapKeysTransformOutput: {
  _id: "id", // 我们希望将 `_id`（来自 MongoDB）替换为 `id`（供 Better-Auth 使用）
},
```

### `customTransformInput` [#customtransforminput]

如果需要在数据保存到数据库前转换输入数据，可以使用此选项。

<Callout type="warn">
  如果你启用了 `supportsJSON`、`supportsDates` 或 `supportsBooleans`，这些转换会在调用你的 `customTransformInput` 函数之前执行。
</Callout>

`customTransformInput` 函数接收以下参数：

* `data`：需要转换的数据。
* `field`：当前转换的字段。
* `fieldAttributes`：该字段的属性。
* `action`：调用的适配器动作（`create` 或 `update`）。
* `model`：当前模型。
* `schema`：当前模式。
* `options`：Better Auth 选项。

- `data`：需转换的数据。
- `field`：当前转换的字段。
- `fieldAttributes`：该字段的属性。
- `action`：调用的适配器动作（`create` 或 `update`）。
- `model`：当前模型。
- `schema`：当前模式。
- `options`：Better Auth 选项。

`customTransformInput` 函数在给定动作的数据对象的每个键上运行。

```ts title="示例"
customTransformInput: ({ field, data }) => {
  if (field === "id") {
    return "123"; // 强制 ID 为 "123"
  }

  return data;
};
```

### `customTransformOutput` [#customtransformoutput]

如果需要在数据返回给用户前转换输出数据，可以使用此选项。\
它和 `customTransformInput` 类似，但在数据从数据库取出后执行。

```ts title="示例"
customTransformOutput: ({ field, data }) => {
  if (field === "name") {
    return "Bob"; // 强制姓名为 "Bob"
  }

  return data;
};
```

```ts
const some_data = await adapter.create({
  model: "user",
  data: {
    name: "John",
  },
});

// 姓名将会是 "Bob"
console.log(some_data.name);
```

### `disableTransformInput` [#disabletransforminput]

是否禁用输入转换。只有在你明确知道自己在做什么，并手动处理所有转换时才应禁用。

<Callout type="warn">
  禁用输入转换可能导致重要的适配器功能失效，例如 ID 生成、布尔／日期／JSON 转换和键映射。
</Callout>

### `disableTransformOutput` [#disabletransformoutput]

是否禁用输出转换。只有在你明确知道自己在做什么，并手动处理所有转换时才应禁用。

<Callout type="warn">
  禁用输出转换可能导致重要的适配器功能失效，例如布尔／日期／JSON 解析和键映射。
</Callout>

### `disableTransformJoin` [#disabletransformjoin]

是否禁用连接转换。只有在你明确知道自己在做什么，并手动处理连接查询时才应禁用。

<Callout type="warn">
  禁用连接转换可能导致连接功能失效。
</Callout>

