技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第14章 Mix Tasks その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
引き続きMix Tasksの項目を見ていきます。
今回はEcto関連のコマンドから。

Ecto Specific Mix Tasks

このEcto周辺のコマンドですが、当たり前ですが--no-ectoとかしてない場合が対象です。

mix ecto.create

データベースの作成を行うコマンドです。デフォルトならconfigやlib/hello/repo.exに記載したデータベースに作成するはずです。
デフォルトでいいなら引数は要らないのでシンプルです。

$ mix ecto.create
The database for Hello.Repo has been created.

別の名前で定義してたりするなら、-rオプションでモジュール名を指定します。

$ mix ecto.create -r OurCustom.Repo
The database for OurCustom.Repo has been created.

PostgreSQLのユーザー周りの説明は割愛します。
(要望があれば書きますが)

ecto.drop

純粋にecto.createの逆だと思えば良さそうで、指定したデータベースからデータを消去します。
使い方もほぼcreateと同じで、デフォルトなら引数は不要。

$ mix ecto.drop
The database for Hello.Repo has been dropped.

-rでモジュール名を指定することも可能。

$ mix ecto.drop -r OurCustom.Repo
The database for OurCustom.Repo has been dropped.

mix ecto.gen.repo

データストアが1個じゃないケースもあると思います。そういうときにこのecto.gen.repoで新しいリポジトリを作成できるそうです。
たとえば、デフォルトのリポジトリHello.Repoで、別のリポジトリOurCustom.Repoであるばあい、下記のようにして作成するようです。

$ mix ecto.gen.repo -r OurCustom.Repo
* creating lib/our_custom
* creating lib/our_custom/repo.ex
* updating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/hello.ex):

worker(OurCustom.Repo, [])

* updating config/config.exsとあるので、configも修正されています。

...
config :hello, OurCustom.Repo,
database: "hello_repo",
username: "user",
password: "pass",
hostname: "localhost"
...

これは各環境で合わせて下さい。
場合によってはdev.exsprod.exsに記載してもいいかもしれません。

ファイルの生成などは自動でやってくれましたが、リポジトリのワーカーをSupervisorで管理する必要があります。
ドキュメントではlib/hello.exを編集、と書いてありますがたぶんlib/hello/application.exの間違いです。

...
children = [
  # Start the Ecto repository
  Hello.Repo,
  # Start the endpoint when the application starts
  HelloWeb.Endpoint,
  # Starts a worker by calling: Hello.Worker.start_link(arg)
  # {Hello.Worker, arg},
  # Here you could define other workers and supervisors as children
  OurCustom.Repo
]
...

これで大丈夫そうです。

mix ecto.gen.migration

マイグレーションする準備をするのがecto.gen.migrationです。こういうフレームワークにはよくある奴だと思います。
Contextの章でもいじったファイルを作成するやつで、マイグレーションも実際の所はElixirのスクリプトを実行して行っていますのでそのスクリプトを生成するコマンドということです。
基本的に何かのコマンドで自動生成されるのでそこまで出番があるように思えませんが、スキームの変更などをした場合には手動で作成する必要がありそうです。
コマンドにマイグレーションファイル名を引数として渡します。

mix ecto.gen.migration add_comments_table
* creating priv/repo/migrations
* creating priv/repo/migrations/20150318001628_add_comments_table.exs

マイグレーション用のファイルがpriv/repo/migrationsに作成されます。おそらくタイムスタンプ_指定した名前.exsというファイルが作成されます。
ファイルの中身は下記のようになっているそうです。

defmodule Hello.Repo.Migrations.AddCommentsTable do
  use Ecto.Migration

  def change do
  end
end

基本的にコマンドで生成した場合は特に何も書かれていないはずです。
このchange/0に記載された仕様をもとに、データベースを編集したりロールバックしたりします。
たとえば、commentsというテーブルを作成し、bodyword_countというフィールド+タイムスタンプがあるスキームを追加したとします。
このとき、マイグレーションファイルは下記のように編集します。

...
def change do
  create table(:comments) do
    add :body,       :string
    add :word_count, :integer
    timestamps()
  end
end
...

ちなみに、今はcreateを使いましたが、スキームを追加するのではなく変更する場合などはalterを利用します。

基本的な使い方は上記の通りですが、デフォルトではないリポジトリを対象としたマイグレーションのファイルは、ecto.createなどと同様-rオプションでリポジトリ名を指定してやる必要があります。

$ mix ecto.gen.migration -r OurCustom.Repo add_users
* creating priv/repo/migrations
* creating priv/repo/migrations/20150318172927_add_users.exs

mix ecto.migrate

マイグレーションファイルを作成したらすることは決まっています。マイグレーションです。
何も考えずにmix ecto.migrateすることが多いのではないでしょうか。

$ mix ecto.migrate
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 forward
[info] create table comments
[info] == Migrated in 0.1s

そこまで開発にかかわってこない可能性がありますが、mix ecto.migrateをするとデータベース上のschema_migrationsというテーブルにマイグレーション日時のタイムスタンプが作成されるようです。

hello_dev=# select * from schema_migrations;
version     |     inserted_at
----------------+---------------------
20150317170448 | 2015-03-17 21:07:26
20150318001628 | 2015-03-18 01:45:00
(2 rows)

あとで見てみますが、ecto.rollbackなどでロールバックするときはこの辺の情報を利用してロールバックするようです。

基本的に存在している全てのマイグレーションファイルを実行してマイグレートするコマンドですが、一応実行する個数を指定出来るようです(順番はタイムスタンプ依存?)
-n--stepのオプションを使用して実行するマイグレーションの個数を指定します。

$ mix ecto.migrate -n 2
[info] == Running Hello.Repo.Migrations.CreatePost.change/0 forward
[info] create table posts
[info] == Migrated in 0.0s
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 forward
[info] create table comments
[info] == Migrated in 0.0s

一応下記も同様

$ mix ecto.migrate --step 2

-vのオプションを使用すると、指定したタイムスタンプのマイグレーションファイルのみを実行できるようです。

$ mix ecto.migrate -v 20150317170448

--toも同じ働き。

$ mix ecto.migrate --to 20150317170448

mix ecto.rollback

ロールバック、詰まり先祖返りできます。スキーム変更したけどやり直したい!とかいう場合に活用できそうです。
それもコレもマイグレーションファイルにきっちり変更の仕様が記載されているおかげでしょう。

$ mix ecto.rollback
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 backward
[info] drop table comments
[info] == Migrated in 0.0s

何も指定しなければ全部戻ってしまいそうです。

rollbackmigrationと同様のオプションが取れます。例えば-nでn個マイグレーションファイルを遡る、などでしょうか。
こちらはオプションを大いに活用しそうです。

Creating Our Own Mix Tasks

おおよそ見ていったmixコマンドで事足りそうですが、「もうちょっとやってくれよ」「ここ毎回同じ事してる」という場合はかゆいところに手が届くmixコマンドが欲しくなると思います。
そういった需要に応えるがごとく、mixコマンドを作成できるそうです。

まずはlib/フォルダにmix/tasksディレクトリを作成していきます。

$ mkdir -p lib/mix/tasks

試しにhello.greeting.exというファイルを作成するとします。

defmodule Mix.Tasks.Hello.Greeting do
  use Mix.Task

  @shortdoc "Sends a greeting to us from Hello Phoenix"

  @moduledoc """
    This is where we would put any long form documentation or doctests.
  """

  def run(_args) do
    Mix.shell.info("Greetings from the Hello Phoenix Application!")
  end

  # We can define other functions as needed here.
end

慣習なのか知りませんが、基本的にElixirはディレクトリ名をモジュール名に適応するようです。
(そもそもコンパイル時にチェックされる?すみません、この辺は勉強不足です。)
なので、lib/ディレクトリ以下のmix/tasksディレクトリ下にあるhello.greeting.exなので、モジュール名はMix.Tasks.Hello.Greetingになります。

お次にuse Mix.Taskが宣言されています。たぶんこれでmixのコマンドとして機能するのでしょう。

@shortdocのモジュール属性ですが、これはmix help字に表示される説明文です。

@moduledocはモジュールのドキュメントです。コレに関しては割愛します。

run/1mixで呼ばれたときに実行される関数です。ここではmixで実行されたときにシェルに文字列がプリントされるだけのようです。

以上の仕様で、利用する為にはまずコンパイルします。

$ mix compile
Compiled lib/tasks/hello.greeting.ex
Generated hello.app

コンパイルすると、mix helpにも表示されるようです。

$ mix help | grep hello
mix hello.greeting # Sends a greeting to us from Hello Phoenix

実際に利用する場合は、ファイル名で実行するようです。

$ mix hello.greeting
Greetings from the Hello Phoenix Application!

run/1では今のところ文字列を出力するMix.shell.info/1しか利用していませんが、アプリケーションを実行するにはMix.Task.run/1を利用すればいいようです。

...
  def run(_args) do
    Mix.Task.run("app.start")
    Mix.shell.info("Now I have access to Repo and other goodies!")
  end
...

もうちょっといろんな機能を実装するにはMixの仕様をより深掘りする必要がありそうです・・・