ElixirでSI開発入門 #8 Railsからのモデルの移行1(FitGap分析)


（この記事は、「[fukuoka.ex(その2) Elixir Advent Calendar 2017](https://adventar.org/calendars/2871")」の16日目、および[「Data Platform Advent Calendar 2017」](https://qiita.com/advent-calendar/2017/dataplatform)の4日目です）

昨日は、@piacere_ex さんの[「Elixirでデータ分析入門#2：インプットしたデータを変換する（前処理①）」](https://qiita.com/piacere_ex/items/64c2127857599137e8a7)でした

# はじめに

Elixirで実際にプロダクト開発した経験からサンプルコードを交えて解説する本連載
今回から数回に渡り別言語のフレームワークで構築されたDBへ対してElixir＋Phoenixで接続する際に必要なモデルの移行を考えます。

今回はRuby on RailsでMySQLに作成したテーブルを例に、
既存システムのDB情報を元にEctoモデルを自動作成するツールを開発します。
Railsの実行速度に限界を感じている方にはぜひ読んでいただきたい内容です。

本連載の記事はこちら
|> [ElixirでSI開発入門 #1 Ectoで悲観的ロック](https://qiita.com/tuchiro/items/7b04b0661cfa4a7af179)
|> [ElixirでSI開発入門 #2 Ectoで楽観的ロック](https://qiita.com/tuchiro/items/8949cc43ba90f4e68bf3)
|> [ElixirでSI開発入門 #3 主キーが"id "じゃない既存DBへの接続](https://qiita.com/tuchiro/items/a4808baacea9a07d2409)
|> [ElixirでSI開発入門 #4 本番パスワードを環境変数に持たせる]
(https://qiita.com/tuchiro/items/4ccba7e210c596c383af)
|> [ElixirでSI開発入門 #5 Ectoで自由にSQLを書いて実行する(参照編)](https://qiita.com/tuchiro/items/34a3c7f0f7deb5d3241f)
|> [ElixirでSI開発入門 #6 Ectoで自由にSQLを書いて実行する(更新編)](https://qiita.com/tuchiro/items/04a0e68222beba91958f)
|> [ElixirでSI開発入門 #7 Multiで使う関数を再利用可能な粒度に分割する](https://qiita.com/tuchiro/items/8048e38404e0968d9b6e)
|> ElixirでSI開発入門 #8 Railsからのモデルの移行1(FitGap分析)

　**★★★お礼：各種ランキングに83回のランクインを達成しました　★★★**

4/27から、37日間に渡り、毎日お届けしている[「季節外れのfukuoka.ex Elixir Advent Calendar」](https://qiita.com/advent-calendar/2017/elixir-or-phoenix)と[「季節外れのfukuoka.ex(その2) Elixir Advent Calender」](https://adventar.org/calendars/2871)ですが、[Qiitaトップページトレンドランキング](https://qiita.com/trend)に12回入賞、[Elixirウィークリーランキング](https://qiita.com/tags/elixir)では6週連続で１／２／３フィニッシュを飾り、各種ランキング通算で、**トータル83回ものランクイン**を達成しています

みなさまの暖かい応援に励まされ、**合計552件ものQiita「いいね」**もいただき、fukuoka.exアドバイザーズとfukuoka.exキャストの一同、ますます季節外れのfukuoka.ex Advent Calendar、頑張っていきます!

[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/bbe9d349-c978-e9e8-ba7d-f8feda74be91.png)](https://adventar.org/calendars/2871)


# 想定される課題
## 課題１ Rails（ActiveRecord）とEctoの型のギャップ

　データ移行や、システムリプレイス、外部IF通信など、
　異なる言語、フレームワーク、ミドルウェア間でデータのやり取りを行う場合、
　必ずぶち当たるのが型桁のギャップの問題です。
　ElixirはRubyの影響を強く受けているものの、
　双方の代用的ORマッパーであるActiveRecordとEctoにもギャップが存在する可能性が高く、
　双方で相いれないデータ定義をどう処理するかが課題となります。

## 課題２ DDLから取得できない情報の存在

　何らかの成果物（今回はDDL）を解析する場合、各実装におとす過程で消失し、不可逆な情報が存在します。
　今回のケースでいうと、MIXタスクでモデルを生成する時に必要な情報のうち、コンテキスト名は変換しても求めることができません。

|項目	|取得可否	|記法	|備考|
|:--|:--|:--|:--|
|コンテキスト名|	不可	|UpperCamelCase 	|概念の問題なので自動的に変換できない|
|モジュール名|	可	|UpperCamelCase 	|複数形から単数形への変換が必要|
|複数形名| 	可	|sneak_case	|ActiveRecordでMigrationすればテーブル名は複数形|


# 実装の前提

最終的には

- Railsで実装されたモデルに対応するDBのDDLからEctoモデルを作成する

を目標にしますが、
その前段として、課題１のRailsとEctoの型に関するFitGap分析を行います。

- Railsで代表的な型を定義したモデルを生成する。
- Ectoで同じく代表的な型を定義したモデルを作成する。
- 双方のマイグレーション結果のDDLから型のFitGapを整理する
- DBはMySQLを使用する

また、以下の環境で実装しました

- Elixir     v1.6.1
- Phoenix v1.3.2
- Ecto      v2.2.10
- Ruby v2.2.3p173
- Rails v5.2.0
- MySQL v5.7.22 

# 事前準備

## Rails環境の構築
まず、事前準備としてRuby on Railsの開発環境を構築します。

下記の記事を参考に実行しました。

[最速！MacでRuby on Rails環境構築](https://qiita.com/narikei/items/cd029911597cdc71c516)

## MySQLのインストール

また、今回はDBとしてMySQLを対象とする為、こちらも下記記事を参考にインストールしローカル端末上で起動しました。

[Mac での MySQL あれこれ]
(https://qiita.com/itooww/items/13055c8bb1d226ee5844)

## DBの作成

mysqlコマンドでDBを作成します。

```sql
> mysql -u root
mysql CREATE USER admin IDENTIFIED BY 'password' ;
mysql> CREATE DATABASE mig_sample_development ;
mysql> use mig_sample_production ;
```
## Rails側のモデルの作成

任意のフォルダで以下のコマンドを実行し、Railsプロジェクトを作成します。

```
> bundle exec rails new mig_sample -d mysql
> cd mig_sample
```

作成したプロジェクトフォルダにて以下のコマンドを実行し、モデルを作成します。
※モデルはデータ型の確認のため、一般的によく使う型を一通り定義した業務的には特に意味を持たないモデルです。

```
> rails g model Sample col_stg:string:index col_text:text col_float:float col_integer:integer:unique col_decimal:decimal col_datatime:datetime col_time:time col_date:date col_binary:binary col_boolean:boolean
```

モデルと同時に以下のマイグレーションファイルが作成されるので確認する。

```Ruby:db/migrate/20180516023237_create_samples.rb
class CreateSamples < ActiveRecord::Migration[5.2]
  def change
    create_table :samples do |t|
      t.string :col_stg
      t.text :col_text
      t.float :col_float
      t.integer :col_integer
      t.decimal :col_decimal
      t.datetime :col_datatime
      t.time :col_time
      t.date :col_date
      t.binary :col_binary
      t.boolean :col_boolean

      t.timestamps
    end
    add_index :samples, :col_stg
  end
end
```

rakeタスクでマイグレーションを実行する。
　(Elixirでのmix ecto.migrateに相当)

```
>rake db:migrate
```



マイグレーションを実行した結果作成されたテーブルのDDLが以下となる。

```sql
CREATE TABLE `samples` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `col_stg` varchar(255) DEFAULT NULL,
  `col_text` text,
  `col_float` float DEFAULT NULL,
  `col_integer` int(11) DEFAULT NULL,
  `col_decimal` decimal(10,0) DEFAULT NULL,
  `col_datatime` datetime DEFAULT NULL,
  `col_time` time DEFAULT NULL,
  `col_date` date DEFAULT NULL,
  `col_binary` blob,
  `col_boolean` tinyint(1) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_samples_on_col_stg` (`col_stg`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

## 検証用にElixirのモデルも作成

移行プログラムを作成するにあたって、検証用にElixirでも同じようなモデルを作成する。

```
> mix phx.new ecto_mig_sample --database mysql
> cd ecto_mig_sample/
> mix ecto.create
> mix phx.gen.html Samples Sample samples col_string:string col_int:integer:uniquecol_float:float col_boolean:boolean col_binary:binary col_decimal:decimal col_map:map col_date:date col_time:time col_u_datetime:utc_datetime
mix ecto.migrate
```

Elixirのマイグレーションで作成されたテーブルのDDLがこちらである。

```sql
CREATE TABLE `samples` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `col_string` varchar(255) DEFAULT NULL,
  `col_int` int(11) DEFAULT NULL,
  `col_float` double DEFAULT NULL,
  `col_boolean` tinyint(1) NOT NULL DEFAULT '0',
  `col_binary` blob,
  `col_decimal` decimal(10,0) DEFAULT NULL,
  `col_map` text,
  `col_date` date DEFAULT NULL,
  `col_time` time DEFAULT NULL,
  `col_u_datetime` datetime DEFAULT NULL,
  `inserted_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `samples_col_int_index` (`col_int`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

## 型のGapを確認する

上記の検証結果から今回対象とする
Railsのデータ型 -> MySQLのデータ型 -> Ectoのデータ型
の対応を整理したのが以下の表である。

| Rails | MySQL | Ecto | Elixir |
|:--|:--|:--|:--|
| id | bigint(20) | id | integer |
| string | varchar(255) | :string | UTF-8 encoded string |
| text | text | ※1 | - |
| N/A | N/A | :map | map |
| float | float | ※2 | - |
| float #limitの範囲が24~53 | double | :float | float |
| integer #limitが1 | tinyint(1) | ※3 | - |
| boolean | tinyint(1) | :boolean | boolean |
| integer | int(11) | :integer | integer |
| decimal | decimal(10,0) | :decimal | Decimal |
| datetime | datetime | :utc_datetime | DateTime |
| time | time | :time Time | Time |
| date | date | :date Date | Date |
| binary | blob | :binary | binary |

※1 [Ectoの型に現状TEXTはない](https://hexdocs.pm/ecto/Ecto.Schema.html)ただし、先ほどの検証結果からmap型を指定するとDB上はTEXTに対応するが意味合いが異なるため、今回は考慮の対象外とする。

※2 [Ectoにはfloat型の定義がある](https://hexdocs.pm/ecto/Ecto.Schema.html)が上記の検証結果から特に桁数を指定しない場合のMySQLのデータ型としては　doubleに相当する。
　今回はRailsからの移行が目的のため、桁数定義による厳密な型の対応は割愛する。

※3 本来tinyint(1)はデータ型としてtynyint(1)でしかないのだが、MySQLの仕様上Railsおよび、Ectoでのbooleanがtinyint(1)としてMySQL上で扱われる。
　　　ややこしいので、今回のツールではtinyintは対象外とし、tinyint(1)=booleanとして扱う

# まとめ

DDLからカラム定義と型を判別し、ecto.schemaの対応する型としてMixでgenしてやればモデルの以降を半自動化できそうです。
（コンテキストの問題があるので全自動化はできない）

次回は、実際のツールの開発に入ります。
お楽しみに〜〜 (^_^)/

明日は、@twinbee さんの[「Elixirから簡単にRustを呼び出せるRustler#5 NIFからメッセージ送信を行う」](https://qiita.com/twinbee/items/cd818fd509ace2ae0d0a)です


<b>★★★　満員御礼！Elixir MeetUpを6月末に開催します　★★★</b>
<font color="red"><b>※応募多数により、増枠しました</b></font>
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します

私もこの連載で取り上げてない、開発ネタを出す予定ですのでぜひ奮ってご応募ください。

[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)