祝 Nim v1.0.0 リリース!NimでAPIサーバーを書いてみる。 ORM編


祝 Nim v1.0.0 リリース!NimでAPIサーバーを書いてみる。 ORM編

ついにやってきました。
9月23日に、プログラミング言語Nimのv1.0.0がリリースされました!

v1.0.0になったことで、安定性と後方互換性が保証されるそうです。
今後、学ぶ内容が古くなって使えなくなるということはないので、
Nimを学び始めるなら、今が最も最適なタイミングだと思います。
https://nim-lang.org/blog/2019/09/23/version-100-released.html

おさらい : プログラミング言語Nimとは

  • 静的型付け言語
  • Pythonのインデントブロックをリスペクトした書きやすい構文
  • C言語並の実行速度
  • ネイティブバイナリをコンパイル可能
  • javascriptにも変換できる = webのフロント開発もできる
  • 強力なメタプログラミング機能(macro, template)
  • ガベージコレクタあり

CやC++にトランスパイルできたり、C言語で書かれた資産をFFIで利用できたり、と
C言語との相性が非常にいいです。
実行速度が最適化したC言語並に速いのはこの辺が関係してるのかも。

ともかく、特徴だけを見れば

  • 書きやすい
  • モダンな言語機能を一通り揃えてる
  • 実行速度も爆速

といった特徴を併せ持つ最強のプログラミング言語となっております。
(個人の感想です)

Nimでなんか書いてみよう

ということで、Nimを使って簡単なプログラムを書いてみたいと思います。
僕自身がNim初心者ということもあり、勉強の備忘録を兼ねてるので
説明がくどかったりするかもですがご了承ください。

NimでAPIサーバーを書く : ORM編

昨今、フロントエンドはReactやVueをnpm環境で開発するという方式が一般的になっています。

バックエンドはSSRしてhtmlを返すのではなく、汎用的なフォーマットであるjsonを返すのがベストプラクティスです。
ということで、NimでAPIサーバーを作りたいと思います。

まずはDBを操作するためにObject Relation Mappingをします。

nimble init : プロジェクトを作成

mkdir nim_rest
cd nim_rest
nimble init       # パッケージタイプを"Binary"にして、あとはすべてデフォルト

nimbleは、nimのパッケージマネージャ兼ビルドシステムです。
Rustにおけるcargo, JSにおけるnpmやyarnみたいなもんです。

今回使うパッケージ : norm

今回使うORMパッケージ norm を説明します。

norm はNimで書かれた軽量なORMで、
バリバリにmacro, template機能を使用して実装されており
シンプルかつ少ない記述量でDB操作を行うことができます。

まずはinstall

nimble install norm

1. DBへの接続・テーブル作成

今回はDBにPostgreSQLを使います。
僕はdocker-composeで用意します。

docker-compose.yml
version: "3"
services:
  db:
    image: postgres
    ports:
      - 5432:5432
    environment: 
      POSTGRES_USER: nim
      POSTGRES_PASSWORD: nim
      POSTGRES_DATABASE: nim
docker-compose up -d

これで準備OK!

次に、Nimで接続のためのコードを書きます。
ついでに、モデルの定義もします。

nim_rest.nim
import norm/postgres

db("localhost:5432", "nim", "nim", "nim"):
  type Post* = object
      title*: string
      author*: string
      content*: string

withDb:
  createTables(force=true)

db:

このブロックでは、DBへの接続とモデルの定義を行います。
db マクロ以降の withDb ブロック内で、dbに関する操作を行えるようになります。

モデル

db マクロの中でオブジェクトを定義することで、DBのスキーマ定義になります。
type名やフィールドに * アスタリスクがついてますが、これはpublicであることを示しています。

withDb:

このブロックのボディ内で、dbに関する操作を実行することができます。

createTables(bool)もnormで定義されているテンプレート関数で、
実行するとdbマクロ内で定義されたオブジェクト名で、DBのテーブルを作成します。
引数のforceをtrueにすると、すでに同じ名前のテーブルがあっても上書きして作成をします。

2. オブジェクトをDBに記録する

nim_rest.nim
import strformat
import logging

# コンソールロガーを登録
addHandler newConsoleLogger()

# insert
withDb:
  var test: Post
  for i in 1..10: # 1から10まで繰り返す。(9までではない)
    test = Post(
      title: fmt"{i}th title",
      author: "max",
      content: fmt"{i}th content",
    )
    test.insert()

insert()

post.insert()はテンプレートメソッドです。
db マクロ内部で定義したオブジェクトに対して実装されます。
実行すると、オブジェクトをDBに記録します。

logging

normのDB操作は、addHandler newConsoleLogger() を実行しておくとコンソールに出力できます。
addHandlerはproc(関数みたいなやつ)なのですが、Rubyのように()がなくても引数を指定して実行できます。

PythonっぽいのにRubyの文法も取り込んでるのがなんか面白い。

フォーマット文字列 fmt

strformat パッケージをインポートすると、フォーマット文字列 fmt"{}" が使えるようになります。
この文字列の中で、ブラケット{} で変数を囲むと値に置き換わります。

かなり便利。

書き込めたか確認してみる

dockerコンテナのPostgreSQLにログインしてSQL文を打ち込んでみます。

docker exec -it nim_rest_db_1 psql -U nim -d nim

# postgresコンテナ内のpsqlコンソール
nim=# select * from post;

# 出力結果
 id |   title    | author |   content    
----+------------+--------+--------------
  1 | 1th title  | max    | 1th content
  2 | 2th title  | max    | 2th content
  3 | 3th title  | max    | 3th content
  4 | 4th title  | max    | 4th content
  5 | 5th title  | max    | 5th content
  6 | 6th title  | max    | 6th content
  7 | 7th title  | max    | 7th content
  8 | 8th title  | max    | 8th content
  9 | 9th title  | max    | 9th content
 10 | 10th title | max    | 10th content

問題なく、DBにレコードを作成できました。

3. DBからオブジェクトから取り出す

nim_rest.nim
import sugar

# select
withDb:
  # 1つだけ取る
  var 
    id = 1
    result = Post.getOne(id)
  dump result
  # result = (id: 1, title: "1th title", author: "max", content: "1th content")

  # いっぱい取る
  var 
    limit = 10
    offset = 0
    many_posts = Post.getMany(limit, offset)
  dump many_posts
  # many_posts = @[(id: 1, title: "1th title", author: "max", content: "1th content"), (id: 2 ...

  # 条件をつけて取る
  var 
    min_id = 4
    max_id = 8
    cond_posts = Post.getMany(limit, offset, 
      "id >= ? AND id <= ? ORDER BY id DESC",   # WHERE文に相当する。
      [$min_id, $max_id]                        # WHERE文の?マークに埋め込める文字列。string型にしないとだめなので、 $演算子で文字列に変換
    )
  dump cond_posts
  # cond_posts = @[(id: 8, title: "8th title", author: "max", content: "8th content"), (id: 7, ...

getOne() と getMany()

どちらもテンプレートメソッドで、insert()と同じくdb マクロ内部で定義したオブジェクトに実装されます。
クラスメソッドっぽい感じなので、インスタンスではなく type.method() みたいに書きます。

それぞれ、戻り値の型が
- Post.getOne() : Post
- Post.getMany() : seq[Post]

となっており、getMany()ではsequece[object]型で複数の値が獲得できます。

dump()

テンプレート関数。
sugar パッケージからimportされたものです。

リファレンスによれば、式の内容をダンプして出力する関数で、デバッグの際にオブジェクトなどを出力するのに役立ちます。
https://nim-lang.org/docs/sugar.html

sugar モジュールはその名の通り、シンタックスシュガーを集めたパッケージで他にも以下のようなものがあります。

  • es6以降のアロー関数演算子 =>
  • Rust風の関数識別子 ->

条件をつけてレコードを取得

getOne() と getMany() は、SQLにおけるWHERE文に相当する部分を、

文字列の引数として渡すことができます。

上のコードでは、idが 4~8 のデータを降順で取得しています。

4. データを更新する

nim_rest.nim
# update
withDb:
  var
    id = 1 
    before = Post.getOne(id)
  before.content = "I have update this content!"
  before.update()

  # 変更できているか確認
  var after = Post.getOne(id)
  dump after

# new_post = (id: 1, title: "1th title", author: "max", content: "I have update this content!")

もう簡単ですね

  1. DBからオブジェクトを取得
  2. オブジェクトの内容を変更する
  3. obj.update() を実行

これだけです。

5. データを削除する

nim_rest.nim
# delete
withDb:
  var
    id = 1 
    post = Post.getOne(id)
  post.delete()

  # 削除したレコードを取得しようとするとエラー
  try:
    var deleted = Post.getOne(id)
    dump deleted
  except KeyError:
    let e = getCurrentException()
    echo repr(e)

これも簡単です。
オブジェクトを取得してきて obj.delete() を実行するだけです。

すでに削除したレコードをid指定で取ってこようとするとKeyErrorが発生します。
try ~ except 文を使うと確認できます。

まとめ

今回やったのは、

  • nimbleプロジェクトの作成
  • ORMパッケージ norm
    • DB接続
    • Postモデル定義
    • CRUD操作

です。

normはここで説明した以外にも、

  • 文字列フィールドのイニシャルを大文字にする。
  • DBレコードにNULL値を許容する。

など、さまざまな設定があるので、詳細な使い方をしたい人は調べてみてください。

次回、webサーバー編

祝 Nim v1.0.0 リリース!NimでAPIサーバーを書いてみる。 サーバー編
https://qiita.com/harumaxy/items/f37a79dc5b959a97a017

続く