Active Recordパターンを採用したTypeScript用ORM「Accel Record」の紹介

この記事では、TypeScript用のORMである[Accel Record](https://www.npmjs.com/package/accel-record)について、(製作者の立場から)簡単に紹介します。

## Accel Record 概要

[accel-record - npm](https://www.npmjs.com/package/accel-record)

Accel Recordは、型安全で同期的な、TypeScript用のORMです。
Active Recordパターンを採用しており、インターフェースはRuby on RailsのActive Recordに強く影響を受けています。

スキーマ管理とマイグレーションにはPrismaを利用しており、既存のPrismaスキーマをそのまま利用することもできます。

~~2024年06月の時点でMySQLとSQLiteをサポートしており、将来的にPostgreSQLもサポート予定です。~~
MySQLとSQLiteに加えてPostgreSQLをサポートしています。

## 特徴

- Active Recordパターン
- 型安全なクラス
- 同期的なAPI
- バリデーション
- Native ESM
- MySQL, PostgreSQL, SQLiteのサポート

いくつかの特徴については、以下でより詳しく紹介します。

## 利用例

例えば以下のようにUserモデルを定義した場合、

```ts
// prisma/schema.prisma
model User {
  id        Int    @id @default(autoincrement())
  firstName String
  lastName  String
  age       Int?
}
```

以下のように記述することができます。

```ts
import { User } from "./models/index.js";

const user: User = User.create({
  firstName: "John",
  lastName: "Doe",
});

user.update({
  age: 26,
});

for (const user of User.all()) {
  console.log(user.firstName);
}

const john: User | undefined = User.findBy({
  firstName: "John",
  lastName: "Doe",
});

john?.delete();
```

また、モデルを拡張して自由にメソッドを定義できます。

```ts
// src/models/user.ts
import { ApplicationRecord } from "./applicationRecord.js";

export class UserModel extends ApplicationRecord {
  // フルネームを取得するメソッドを定義
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}
```

```ts
import { User } from "./models/index.js";

const user = User.create({
  firstName: "John",
  lastName: "Doe",
});

console.log(user.fullName); // => "John Doe"
```

より詳しい使い方は[README](https://github.com/koyopro/accella/blob/main/packages/accel-record/README-ja.md)にあります。


## Active Recordパターン

Accel Recordは、Active Recordパターンを採用しています。
特に、インターフェースはRuby on RailsのActive Recordに強く影響を受けています。
Railsの経験がある方は、すぐに使い方を理解できると思います。

### データの作成と保存の例

```ts
import { NewUser, User } from "./models/index.js";

// Userの作成
const user: User = User.create({
  firstName: "John",
  lastName: "Doe",
});
console.log(user.id); // => 1

// このようにも書けます
const user: NewUser = User.build({});
user.firstName = "Alice";
user.lastName = "Smith";
user.save();
console.log(user.id); // => 2
```

### データの取得の例

```ts
import { User } from "./models/index.js";

const allUsers = User.all();
console.log(`IDs of all users: ${allUsers.map((u) => u.id).join(", ")}`);

const firstUser = User.first();
console.log(`Name of the first user: ${firstUser?.firstName}`);

const john = User.findBy({ firstName: "John" });
console.log(`ID of the user with the name John: ${john?.id}`);

const does = User.where({ lastName: "Doe" });
console.log(`Number of users with the last name Doe: ${does.count()}`);
```


## 型安全なクラス

Accel Recordは、型安全なクラスを提供します。
クエリのAPIも型情報が付与されており、TypeScriptの型システムを活用することができます。
エディタ上の補完機能や型チェックが効果的に働くため、開発効率を高く保てます。 

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62874/f29fac2e-bbeb-17c0-d0a7-667c993c57b6.gif)

1つ特徴的な点として、モデルの保存状態に応じて型が変わることが挙げられるので、ここで紹介します。 

Accel Recordでは、新規のモデルと保存済みのモデルを区別するために、それぞれ`NewModel`と`PersistedModel`と呼ばれる型を提供しています。
スキーマ定義に応じて一部のプロパティにおいては、`NewModel`ではundefinedを許容し`PersistedModel`ではundefinedを許容しない型となります。
これにより、保存前のモデルと保存後のモデルをどちらも型安全に扱うことができます。

```ts
import { User, NewUser } from "./models/index.js";

/*
NewModelの例：
NewUser型 は保存前のモデルを表し、以下のような型となります。

interface NewUser {
  id: number | undefined;
  firstName: string | undefined;
  lastName: string | undefined;
  age: number | undefined;
}
*/
const newUser: NewUser = User.build({});

/*
PersistedModelの例：
User型 は保存済みのモデルを表し、以下のような型となります。

interface User {
  id: number;
  firstName: string;
  lastName: string;
  age: number | undefined;
}
*/
const persistedUser: User = User.first()!;
```

`save()`等のメソッドを利用することで、NewModel型をPersistedModel型に変換することができます。

```ts
import { User, NewUser } from "./models/index.js";

// NewModel型のユーザーを用意
const user: NewUser = User.build({
  firstName: "John",
  lastName: "Doe",
});

if (user.save()) {
  // saveが成功した場合、NewModelはPersistedModelに変換されます。
  // このブロック中では、userはUser型として扱うことができます。
  console.log(user.id); // user.idは number型
} else {
  // saveが失敗した場合、NewModelはそのままの型です。
  // このブロック中では、userはNewUser型のままになります。
  console.log(user.id); // user.idは number | undefined型
}
```

## 同期的なAPI

Accel Recordは、DBアクセスの際でもPromiseやcallbackを利用しない同期的なAPIを提供します。
これにより、await等を使わずによりシンプルなコードを書くことができます。
これは主にアプリケーションの開発効率を高める目的で採用されました。

同期的なAPIを採用したことにより、以下のように関連の操作も直感的に行うことができます。

```ts
import { User, Setting, Post } from "./models/index.js";

const user = User.first()!;
const setting = Setting.build({ theme: "dark" });
const post = Post.build({ title: "Hello, World!" });

// hasOneアソシエーションの操作は自動的にSaveされる
user.setting = setting;

// hasManyアソシエーションの操作も自動的にSaveされる
user.posts.push(post);
```

```ts
import { User } from "./models/index.js";

// 関連の取得は遅延ロードとキャッシュが行われます
// user取得時に明示的に関連の取得を指示する必要はありません。
const user = User.first()!;

console.log(user.setting.theme); // settingのロードとキャッシュが行われる
console.log(user.posts.map((post) => post.title)); // postsのロードとキャッシュが行われる
```

同期的なAPIには、非同期APIを使った実装に比べるとデメリットもあります。
主にはパフォーマンスに関するものですが、このあたりのトレードオフについては別の記事で触れたいと思います。[^1]

## バリデーション

Ruby on RailsのActive Record同様、Accel Recordもバリデーション機能を提供します。 

BaseModelの `validateAttributes`メソッドをオーバーライドすることで、バリデーションを定義することができます。

```ts
// src/models/user.ts
import { ApplicationRecord } from "./applicationRecord.js";

export class UserModel extends ApplicationRecord {
  override validateAttributes() {
    // firstNameが空でないことをバリデーション
    this.validates("firstName", { presence: true });
  }
}
```

save等のメソッドを利用する場合にはバリデーションが自動的に実行され、エラーが無い場合のみ保存処理が行われます。

```ts
import { User } from "./models/index.js";

const newUser = User.build({ firstName: "" });
// バリデーションエラーが発生した場合、saveはfalseを返します。
if (newUser.save()) {
  // バリデーションエラーが無く、保存に成功した場合
} else {
  // バリデーションエラーがあり、保存に失敗した場合
}
```

## まとめ

以上で、Accel Recordの概要について簡単に紹介しました。
興味を持たれた方は、ぜひ以下のリンクから詳細をご覧ください。 
(日本語のREADMEもあります！)

accel-record - npm
https://www.npmjs.com/package/accel-record

----

(この記事は[Introduction to "Accel Record": A TypeScript ORM Using the Active Record Pattern - DEV Community](https://dev.to/koyopro/introduction-to-accel-record-a-typescript-orm-using-the-active-record-pattern-2oeh)の原文です)

[^1]: [TypeScriptの新しいORMに同期APIを採用した理由 - Qiita](https://qiita.com/koyopro/items/b11be652ee95426f244e)