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