LaravelそっくりなNim製クエリビルダallographerを作った話

[Nimで自作ORM](https://qiita.com/kamawanu/items/58568ef4463d1ceace63)
という記事がありましたが、その発端となった
[medy🐍👑さんのツイート: "Nim、ORMはめちゃくちゃ弱いから生SQL文をループで文字列結合して作ってるけどめちゃくちゃ難しいな"](https://twitter.com/dumblepytech1/status/1174702114284670976)
をツイートしたのが私です。

私の方でもNim製クエリビルダ/ORMを作ってきたわけなんですが、この度ついに公開できるクオリティになったので、Nimパッケージに追加しました！それではさっそく使い方を解説していきましょう！
[Github](https://github.com/itsumura-h/nim-allographer)

#インストール
```sh
nimble install allographer
```
インストールすると`dbtool`というコマンドが使えるようになります。コマンドにパスを通しましょう。

```sh
export PATH=$PATH:~/.nimble/bin
```

`dbtool`コマンドを使うと、コンフィグファイルを作れます。

```sh
dbtool makeConf
>> config.nims
```

config.nims

```nim
import os

# DB Connection
putEnv("DB_DRIVER", "sqlite")
putEnv("DB_CONNECTION", "/project/db.sqlite3")
putEnv("DB_USER", "")
putEnv("DB_PASSWORD", "")
putEnv("DB_DATABASE", "")

# Logging
putEnv("LOG_IS_DISPLAY", "true")
putEnv("LOG_IS_FILE", "true")
putEnv("LOG_DIR", "/project/logs")
```

デフォルトではsqliteを使う設定が書き込まれています。`DB_CONNECTION`の値には、現在いるディレクトリパスが自動で入力されます。もしmysqlやpostgresが使いたい場合にはそれぞれ接続情報を設定します。

```nim
# mysql
putEnv("DB_DRIVER", "mysql")
putEnv("DB_CONNECTION", "127.0.0.1:3306")

# postgres
putEnv("DB_DRIVER", "postgres")
putEnv("DB_CONNECTION", "127.0.0.1:5432")

# user, password, databaseは共通
```

ログの設定は以下の通りです

|キー|値|説明|
|---|---|---|
|LOG_IS_DISPLAY|true/false|ターミナルに表示するか否か|
|LOG_IS_FILE|true/false|ファイル出力するか否か|
|LOG_DIR|ディレクトリパス|ログ出力するディレクトリ|

ログ出力すると、`log.log`, `error.log`というファイルが作られます。1000行に到達すると新しいファイルが作られ、古いファイルは連番でリネームされます。

```
├── log.log
├── log.log.1
└── error.log
```

# テーブルを作る
Schema Builderはテーブルを作るための仕組みです。丹念にLaravelの関数をパクりました。

```nim
import allographer/schema_builder

schema([
  table("auth", [
    Column().increments("id"),
    Column().string("name"),
    Column().timestamps()
  ]),
  table("users", [
    Column().increments("id"),
    Column().string("name"),
    Column().foreign("auth_id").reference("id").on("auth").onDelete(SET_NULL)
  ], reset=true)
])
```

`table()`の引数に`reset=true`を設定すると、マイグレーションを再び実行した時にテーブルを作り直します。中のデータも消えてしまうので注意。
カラムを作るメソッドの引数でテーブル名を与えます。
`increments()`はPrimary key auto incrementのカラムを作るメソッド、`string()`はvarchar型のカラムを作るメソッド、`timestamps()`はcreated_atとupdated_atのtimestamp型のカラムを作るメソッド、`foreign()`は外部キーを作るメソッド、となっています。
sqlite,mysql,postgresでcreate tableの文法は少しずつ違うので、接続先DBに合わせてクエリが作られるようにしています。以下は上記のクエリをそれぞれのDBで実行した結果です。

```sql
# sqlite
CREATE TABLE "auth" (
  'id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  'name' VARCHAR NOT NULL CHECK (length('name') <= 255),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
CREATE TABLE "users" (
  'id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  'name' VARCHAR NOT NULL CHECK (length('name') <= 255),
  'auth_id' INTEGER, FOREIGN KEY('auth_id') REFERENCES auth(id) ON DELETE SET NULL
)

# mysql
CREATE TABLE auth (
  `id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(255) NOT NULL,
  `created_at` DATETIME,
  `updated_at` DATETIME DEFAULT (NOW())
)
CREATE TABLE users (
  `id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(255) NOT NULL,
  `auth_id` BIGINT, FOREIGN KEY(`auth_id`) REFERENCES auth(id) ON DELETE SET NULL
)

# postgres
CREATE TABLE auth (
  "id" SERIAL NOT NULL PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  created_at TIMESTAMP,
  updated_at TIMESTAMP DEFAULT (NOW())
)
CREATE TABLE users (
  "id" SERIAL NOT NULL PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "auth_id" INT,
  FOREIGN KEY("auth_id") REFERENCES auth(id) ON DELETE SET NULL
)
```

その他のメソッドはこちらをご覧ください。
[schema_builder](https://github.com/itsumura-h/nim-allographer/blob/master/documents/schema_builder.md)

# クエリを発行する
Query Builderはクエリを発行する仕組みです。

```nim
import allographer/query_builder

let result = RDB()
            .table("users")
            .select("id", "email", "name")
            .limit(5)
            .offset(10)
            .get()
echo result

>> SELECT id, email, name FROM users LIMIT 5 OFFSET 10
>> @[
  {"id":11,"email":"user11@gmail.com","name":"user11"},
  {"id":12,"email":"user12@gmail.com","name":"user12"},
  {"id":13,"email":"user13@gmail.com","name":"user13"},
  {"id":14,"email":"user14@gmail.com","name":"user14"},
  {"id":15,"email":"user15@gmail.com","name":"user15"}
]
```

返り値は`seq[JsonNode]`型で返ってきます。
返り値をオブジェクトにつめて取得したい時は以下のようにします。

```nim
type User = ref object
  id: int
  name: string
  email: string

let users = RDB()
            .table("users")
            .select("id", "email", "name")
            .limit(5)
            .offset(10)
            .get(User)

for user in users:
  echo user.name

>> "user11"
>> "user12"
>> "user13"
>> "user14"
>> "user15"
```

返り値が`seq[User]`で返ってきます。

条件に一致したものを1件だけ取得したい時

```nim
let user = RDB()
            .table("users")
            .where("name", "=", "John")
            .first()
```

プライマリキーから取得したい時

```nim
let user = RDB().table("users").find(1)
```

JoinとWhereが使えます

```nim

let users = RDB()
            .table("users")
            .select("users.id", "contacts.phone", "orders.price")
            .join("contacts", "users.id", "=", "contacts.user_id")
            .join("orders", "users.id", "=", "orders.user_id")
            .where("orders.id", "<=", 100)
            .get()
```

多くのフレームワークが持っている、内部的にOFFSETとLIMITを使うページネーションを作りましたが

```nim
let display = 3
let page = 1
let users = RDB()
            .table("users")
            .select("id", "name")
            .paginate(display, page)
>> SELECT id, name FROM users LIMIT 3 OFFSET 0
```

こちらの記事を参考により高速なページネーションも実装しました
[OFFSETを使わない高速なページネーションの実現](https://qiita.com/mpyw/items/07c03600c2e38e91415e)
[OFFSETを使わない高速ページネーションを任意のPHPフレームワークで超簡単に実現する](https://qiita.com/mpyw/items/b94b7d69146777f7a407)

```nim
var users = RDB().table("users").select("id", "name").fastPaginate(3)

>> {
  "previousId":0,
  "hasPreviousId": false,
  "currentPage":[
    {"id":1,"name":"user1"},
    {"id":2,"name":"user2"},
    {"id":3,"name":"user3"},
  ],
  "nextId":4,
  "hasNextId": true
}
```
```nim
users = RDB().table("users")
        .select("id", "name")
        .fastPaginateNext(3, users["nextId"].getInt)

>> {
  "previousId":4,
  "hasPreviousId": true,
  "currentPage":[
    {"id":5,"name":"user5"},
    {"id":6,"name":"user6"},
    {"id":7,"name":"user7"}
  ],
  "nextId":8,
  "hasNextId": true
}
```
```nim
users = RDB().table("users")
        .select("id", "name")
        .fastPaginateBack(3, users["previousId"].getInt)

>> {
  "previousId":0,
  "hasPreviousId": false,
  "currentPage":[
    {"id":1,"name":"user1"},
    {"id":2,"name":"user2"},
    {"id":3,"name":"user3"},
  ],
  "nextId":4,
  "hasNextId": true
}
```

多くのメソッドはLaravelからそのまま流用しています。メソッド一覧はこちらをご覧ください。
[query_builder](https://github.com/itsumura-h/nim-allographer/blob/master/documents/query_builder.md)