17
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-18

Nimで自作ORM
という記事がありましたが、その発端となった
medy🐍👑さんのツイート: "Nim、ORMはめちゃくちゃ弱いから生SQL文をループで文字列結合して作ってるけどめちゃくちゃ難しいな"
をツイートしたのが私です。

私の方でもNim製クエリビルダ/ORMを作ってきたわけなんですが、この度ついに公開できるクオリティになったので、Nimパッケージに追加しました!それではさっそく使い方を解説していきましょう!
Github

#インストール

nimble install allographer

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

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

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

dbtool makeConf
>> config.nims

config.nims

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が使いたい場合にはそれぞれ接続情報を設定します。

# 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の関数をパクりました。

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で実行した結果です。

# 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

クエリを発行する

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

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]型で返ってきます。
返り値をオブジェクトにつめて取得したい時は以下のようにします。

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件だけ取得したい時

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

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

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

JoinとWhereが使えます


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を使うページネーションを作りましたが

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を使わない高速なページネーションの実現
OFFSETを使わない高速ページネーションを任意のPHPフレームワークで超簡単に実現する

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
}
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
}
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

17
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?