なんとな~くしあわせ?の日記

ClojureとかAWSの設定とかをメモする技術ブログ

JRubyに挑戦2

f:id:panzer-jagdironscrap1:20170920235632p:plain

もとの記事は JRuby - FreeStyleWiki

とりあえずJRuby+Hanami+HerokuでREST APIを動かすところまでできたので記事を書いておく。

REST APIを作ってみる

最初の目的であるREST APIを作る

  • HanamiによるAPIの作成手順

ほとんどコレの内容を見て作成した。使用するSQLについてはいろいろ変えている。

speakerdeck.com

作りたいもの
  • APIの場所は /rest/employees にしたい*1

Hanami - ドキュメント

新規アプリの追加
> jruby -S bundle exec hanami generate app api

これで apps/api という階層ができる。Railsと違って複数のアプリが作成できるようだ。

新規Action/Viewの追加
> jruby -S bundle exec hanami generate action api employees#list

これで employees というActionができる。ルートテーブルにもいろいろ追加された。


ここまででできていること

  • Hanamiの全体設定としてのアプリのURL(マウント)設定
  • config/environment.rbApi アプリケーションのマウント場所が書いてある

つまり、http://{ホスト名}/api 以下にアプリが配置されるということ

Hanami.configure do
  mount Api::Application, at: '/api'
...
  • Apiアプリの設定ファイル
  • '''apps/api/application.rb''' に '''Api''' アプリケーションの設定が書いてある

デフォルトではhtmlを返す設定になっているので、API用に以下を追記する

module Api
  class Application < Hanami::Application
    configure do

      # 以下を追加
      default_request_format :json
      default_response_format :json
      body_parsers :json
      ...
  • employeesのためのコントローラー
  • apps/api/controllers/employees/list.rb に employeesを返すためのModuleができている

これの面白いところはコントローラーがメソッドごとに分割されているところだろう

module Api::Controllers::Employees
  class List
    include Api::Action
    accept:json # 追加

    def call(params)
    end
  end
end
  • employeesのためのビュー
  • apps/api/views/employees/list.rb に オブジェクトをJSONにして返すためのModuleができている

これもメソッドごとに分割されている

module Api::Views::Employees
  class List
    include Api::View
    # 以下を追加
    layout false 

    def render
      "[]"
    end
  end
end
新規Entity/Repositoryの追加

ここでモデルと言わずE/RになってるのはRailsと違うところなのだと思う。

  • employeeのモデルクラス作成

モデルは単数形になることに注意

> jruby -S bundle exec hanami generate model employee
  • 元となる理想のテーブル定義(MySQL)
CREATE TABLE employees (
    emp_no      INT             NOT NULL,  -- UNSIGNED AUTO_INCREMENT??
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)    NOT NULL,
    last_name   VARCHAR(16)    NOT NULL,
    gender      VARCHAR(1)     NOT NULL,  -- Enumeration of either 'M' or 'F'
    hire_date   DATE            NOT NULL,
    PRIMARY KEY (emp_no)                   -- Index built automatically on primary-key column
                                           -- INDEX (first_name)
                                           -- INDEX (last_name)
);
  • 作成されたmigrationファイルを改変する
  • db/migrations/YYYYMMDD000000_create_employees.rb
Hanami::Model.migration do
  change do
    create_table :employees do
      primary_key :emp_no, Integer, null: false
      column :birth_date, Date, null: false
      column :first_name, String, null: false, size: 14
      column :last_name, String, null: false, size: 16
      column :gender, String, null: false, size: 1
      column :hire_date, Date, null: false
    end
  end
end

Hanamiのマイグレーションファイルの書き方は、ここに書いてある

そして、実は生のSQLでも実行できる

Hanami::Model.migration do
  up do
    execute %{
      INSERT INTO `employees` VALUES (10001,'1953-09-02','Georgi','Facello','M','1986-06-26'),
      (10002,'1964-06-02','Bezalel','Simmel','F','1985-11-21'),
      (10003,'1959-12-03','Parto','Bamford','M','1986-08-28'),
      ...
    }
  end
end
employeeのDBのテーブルを作成

以下の手順でエラーが出たときは、おそらくWindows/Unix間のパスの問題なので .env.development を見る

  • .env.development
  • windowsの場合、migrate実行時は"jdbc:sqlite:db\\[sqliteのdbファイル名] にする必要がある
DATABASE_URL="jdbc:sqlite:db\\generic_dao_jruby_development.sqlite"
  • 開発環境はSQLite3で動く、ゼロから始める場合は以下のコマンドでdbファイルを作成
> jruby -S bundle exec hanami db create
  • その後以下のコマンドでSQLを実行
> jruby -S bundle exec hanami db migrate
JSONを返す部分を追記

とりあえず、以下の追記でサーバを再起動すればJSONが出力されるはず

  • apps/api/controllers/employees/list.rb
   class List
      include Api::Action
      accept :json
 +    expose :employees
  
      def call(params)
 +      @employees = EmployeeRepository.new.all
      end
    end
  end
  • apps/api/views/employees/list.rb
 module Api::Views::Employees
   class List
     include Api::View
      layout false
  
      def render
 -      "[]"
 +      _raw JSON.dump(employees.map{|employee| employee.to_h })
      end
    end
  end
hanami-modelについての見解

さて、ここまででサーバが起動できるはず

> jruby -S bundle exec hanami s

http://localhost:2300/employee にアクセスするとJSONが返ってくる*2

Hanamiのすごいところは、ここまででO/Rをマッピングするコードを書いていないところだ。詳しくはhanami-modelのREADMEを見れば良いと思うが、model/README.md at master · hanami/model · GitHub。これは結構すごいと思う。

つまりはDB側でカラムの更新があり、usersテーブルにbirthdayというカラムを追加したとしても特にmodelの側でコードの変更は無いということだ。

Javaのものと比べるとすると、例えばMyBatisだとmapper.xmlの中にDBのカラムとObjectのフィールド要素をマッピングするファイルが必要になる。例えばHibernateだとクラスのフィールドに対してアノテーションを付けまくる必要がある。

*1:本当は間に/v1をはさみたい

*2:何かデータは入れておいてね

JRubyに挑戦1

もとの記事は JRuby - FreeStyleWiki

JRuby

環境構築

最初はJavaでSpring-bootを使おうとしたけど、連携の面倒さに辟易したのでJRubyを試してみる。

IDE

  • 他の人にもインストールさせると想定してAtomを使ってみる

qiita.com

qiita.com

結局, Atom v1.19.6でatomic-emacsパッケージをインストールした。

適当にインストーラからインストールすること、バージョン 9.1.13.0

>jruby --version
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
jruby 9.1.13.0 (2.3.3) 2017-09-06 8e1c115 Java HotSpot(TM) 64-Bit Server VM 25.112-b15 on 1.8.0_112-b15 +jit [mswin32-x86_64]
  • Git

qiita.com

> apm install git-plus

ターミナル

bundler, railsコマンドを使いたいので以下を導入

Atomを導入済みであれば、apmコマンドでパッケージをインストールできるはずなので、そうする。

> apm install platformio-ide-terminal

再起動したら Ctrl+バッククォート というすっごいわかりにくいショートカットで起動する

起動した図、これは結構うれしい

f:id:panzer-jagdironscrap1:20170920210337p:plain

日本語化

  • apm install japanese-menu して再起動
> apm install japanese-menu

JRuby on Rails

  • GettingStarted

GettingStarted · jruby/jruby Wiki · GitHub を見て、まずはRailsを入れる。

> jruby -S gem install rails

実行すると、java版のいろいろなライブラリが入る。ネイティブ(C言語連携してるやつ)のライブラリに関してはjavaで書き直す必要があったのだろう。

終わったらさっそく rails new <プロジェクト名> で作成する

→ 動きませんでした…

Hanami on Jruby

それでは、ということで最近話題のHanamiを動かしてみる

> jruby -S gem install hanami

> jruby -S hanami new generic-dao-jruby 

> cd generic_dao_jruby

> jruby -S bundle install

> jruby -S bundle exec hanami s

動いた!

f:id:panzer-jagdironscrap1:20170920210616p:plain

Hanamiについての見解

自分がHanamiフレームワークを知ったのは、以下の記事がはてブでバズっていたからです。RubyJavaでWEB開発をしている人はぜひ見てみてください。

Rubyist Magazine - HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか

MVCにService層を追加する

MVCというのはご存知ですね?*1

WEBシステムを作る際、もはやModel/View/Controllerを分けることは古典的しぐさです。しかしながら、それなりに大きなシステムを作っていくとビジネスロジックが肥大化します。書き方が素人だとModelのコードがぐちゃぐちゃになりバグの温床になります。JavaだとそのへんService層を作るのですが、Railsだとそれがデフォルトでは存在しないのでどうやっていくか自分で考えないといけません。

Qiitaにもそれを行っている記事があります
qiita.com

Hanamiの試みとは?

Hanamiはサービス層をデフォルトで装備したフレームワークです。不勉強で知らなかったのですがこれは ドメイン駆動設計 - Wikipedia 発祥の考え方のようです。

先ほどの文章の中に、Hanamiが解決したい問題が垣間見えます*2

Railsプロジェクトに蔓延した Fat Model 、 Fat Controller 、信用できないテスト、層をまたいで依存したビジネスロジック、 難解で巧妙な Model 同士のやり取り、Rails Mountable Engine を用いた独自ルールや DSL だらけの gem 、 eager loading に頼り切った非効率な SQL 、ロジックまみれの View ……。 メンテナンス性を下げる要因はたくさんあるが、共通して言えることは、複雑な要件に対して簡単すぎる設計をしてしまっていることだ。 しかもこれらのコードの不吉な匂いは、システムをローンチする頃には既に漂い始めている。

Hanamiはこれからも発展していってほしいプロジェクトです。

*1:わからなければブラウザバックしてください

*2:この文書のうちのほとんどはJavaにも当てはまる設計の問題である、勉強すべし

SQLもプログラミング言語…っぽい4

このシリーズも4回目になる。お題は以下の通り

テーブルの正規化

正規化とはなんぞや?

ここが分かりやすかった → データベースの正規化(正規形)とはなんぞや

以下、上のリンクに従って概要をまとめてみる

非正規形

わかりやすく言うとExcelで左のセルを縦に結合したとき、右に複数行のデータがあればそれは非正規形であると言える。データベースに配列型をぶっこもうとしている状態、アカンのである。

食品名 栄養成分 単位
精白米
炭水化物 168 kcal
エネルギー 37.1 g
ビタミン C 1 μg
D 1 μg
E 1 μg

上の例だと、「精白米」という1レコードに対して「炭水化物、エネルギー、ビタミン」という3レコードがくっついている。

この考えをそのままデータベースに適用しようとすると、配列型が用意されてないDBだとできない*1。それで、VARCHAR型に無理やりカンマ区切りでデータを入れたりするやらかし先生がいるかもしれない。「炭水化物,エネルギー,ビタミン」みたいな感じで…

実は、これはSQLの有名なアンチパターンで、「Jaywalking」でググるといろいろ出てくる。

www.slideshare.net

上のスライドには種本がある、自分はまだ読めていない

SQLアンチパターン

SQLアンチパターン


第1正規形

先ほどのExcelの結合部分を右の行数に合わせて分割する。繰り返し項目が増えるが、これでレコードがそれぞれ1行で表せる。

食品名 栄養成分 単位
精白米 炭水化物 168 kcal
精白米 エネルギー 37.1 g
精白米 ビタミンC 1 μg
精白米 ビタミンD 1 μg
精白米 ビタミンE 1 μg

第2正規形・第3正規形

この2つはそんなに変わらない気がする。表を独立する部分で切り出して行けば自ずとこれになる*2。自分が作った例では表が簡単すぎて分割できない…他のサイトを見てみてください。

外部結合の設定

SQLは上の要領で分割して粗結合にしたテーブルを外部結合して見たい情報を揃えるのが一般的な使い方だ。

以前取り上げた例をもとにやってみよう
SQLもプログラミング言語…っぽい - なんとな~くしあわせ?の日記

LEFT JOIN


Parentテーブル

主キー 整理番号
1 12345 山田 太郎
2 12346 山田 花子
3 12346 山田 花子
4 12347 山田 嘘子
5 12348 山田 良子


Childテーブル

整理番号 住所 郵便番号
12345 どこかの台 1丁目 131-0202
12346 すすきが原 3丁目 251-0003
12346 ニセコ 201-0003

Parentテーブルの整理番号とChildテーブルの整理番号が共通してるので、Parentテーブルを基本として左外部結合してみる

CREATE TABLE parent(`主キー` text, `整理番号` int, `姓` text, `名` text);

INSERT INTO parent(`主キー`, `整理番号`, `姓`, `名`)
VALUES (00001, 12345, "山田", "太郎")
, (00002, 12346, "山田", "花子") 
, (00003, 12346, "山田", "花子") 
, (00004, 12347, "山田", "嘘子") 
, (00005, 12348, "山田", "良子");

CREATE TABLE child(`整理番号` int, `住所` text, `郵便番号` text);

INSERT INTO child(`整理番号`, `住所`, `郵便番号`)
VALUES (12345, "どこかの台 1丁目", "131-0202")
, (12346, "すすきが原 3丁目", "251-0003") 
, (12346, "ニセコ", "201-0003");

このように、左に指定したテーブルが中心となる。そのため右に結合したテーブルにNULLが入ることもある。

主キー 整理番号 整理番号 住所 郵便番号
1 12345 山田 太郎 12345 どこかの台 1丁目 131-0202
2 12346 山田 花子 12346 すすきが原 3丁目 251-0003
3 12346 山田 花子 12346 すすきが原 3丁目 251-0003
2 12346 山田 花子 12346 ニセコ 201-0003
3 12346 山田 花子 12346 ニセコ 201-0003
4 12347 山田 嘘子 (null) (null) (null)
5 12348 山田 良子 (null) (null) (null)

右に配置されるレコードが複数行ありえる場合、レコードが増えてしまう。それを防ぐためには、以下のようにレコードを一意に絞りこめるようにする。

LEFT JOIN child
ON parent.`整理番号` = child.`整理番号`
AND child.`郵便番号` = '251-0003'

書籍の紹介

とまあここまでは実は応用情報技術者試験の範囲のようだ。

以下の本のSQLのところを見るともっと網羅的な解説が載っている。

ニュースペックテキスト 応用情報技術者 平成29・30年 (情報処理技術者試験)

ニュースペックテキスト 応用情報技術者 平成29・30年 (情報処理技術者試験)

履歴管理

みんなやってることなので、Qiitaに記事がある

実際的な設計

これ読んどけば十分な気がする

たぶん大体のRDBの人々は1番目のテーブルにバージョン番号を持たせる方法を使っているのではないだろうか。2番目の履歴テーブルをもう一つ作る方法は結構めんどくさそう。

しかし、Stackoverflowの同様のスレッドを見てみると2番目を薦める人が多かった(そしてベストアンサーはRDBをそういうことに使うな!という解答。まあ履歴番号だけ持たせて、実装はGitに任せるとかのほうが正確なシステムにはよいのかもしれない。)

stackoverflow.com


*1:Postgresql先生にはあるようだ ぱせらんメモ - PostgreSQLで配列型のカラムを使ってみる

*2:コッド博士はこれをちゃんと定義して説明したから偉い、想像するだけなら凡人でもできる