技術メモ

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

Phoenix入門 (第13章 Contexts その4)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
前回かなり中途半端なところで終わりましたが、気にせず続けます。

Cross-context dependencies

前回はCMSってContextを作ろう!というところで終わってました。なので作って行きます。

スキーム

今回は

  • title:string タイトル。文字列。
  • body:string 本文。文字列。
  • views:integer 閲覧数?数値。

というモデルになっているようです。

mix phx.gen.htmlでContext生成

PS \hello> mix phx.gen.html CMS Page pages title:string body:text views:integer --web CMS
* creating lib/hello_web/controllers/cms/page_controller.ex
* creating lib/hello_web/templates/cms/page/edit.html.eex
* creating lib/hello_web/templates/cms/page/form.html.eex
* creating lib/hello_web/templates/cms/page/index.html.eex
* creating lib/hello_web/templates/cms/page/new.html.eex
* creating lib/hello_web/templates/cms/page/show.html.eex
* creating lib/hello_web/views/cms/page_view.ex
* creating test/hello_web/controllers/cms/page_controller_test.exs
* creating lib/hello/cms/page.ex
* creating priv/repo/migrations/20190403003143_create_pages.exs
* creating lib/hello/cms.ex
* injecting lib/hello/cms.ex
* creating test/hello/cms/cms_test.exs
* injecting test/hello/cms/cms_test.exs

Add the resource to your CMS :browser scope in lib/hello_web/router.ex:

    scope "/cms", HelloWeb.CMS, as: :cms do
      pipe_through :browser
      ...
      resources "/pages", PageController
    end


Remember to update your repository by running migrations:

    $ mix ecto.migrate

復習にはなりますがmix phx.gen.html Context名 モデル名(コントローラとかで使われる) スキーム名 スキーム (--web 名前空間)でリソースに必要なファイルやContextが生成できます。
--web 名前空間のオプションですが、今回Pageという名前のモデル?モジュール名を利用しているので、そのまま使うとPageControllerとかが被ってしまいます。
そういうときにはこのオプションを利用するようです。(基本的にContext毎に分けた方がいいのでは?とは思いますが、まだ慣れてないので今はそういう物だと考えておきます。)
生成されているファイルがlib/hello_web/controllers/cms/page_controller.exとなっている所からも重複が回避されているのが分かります。

スキームに合わせたテンプレートの調整

viewsという項目はユーザーが直接編集するものではないのでフォームのビューからは消しておきます。lib/hello_web/templates/cms/page/form.html.eexviewsに該当する箇所を消去します。

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <%= label f, :body %>
  <%= textarea f, :body %>
  <%= error_tag f, :body %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

あっても動くとは思いますがドキュメントに従いましょう。

スキームに合わせたchangesetの変更

上記と同様の理由でスキームに付属するchangesetのバリデーションも変更します。
Contextで対応するのかな?と思っていましたがchangesetで落としてしまうようです。
lib/hello/cms/page.exを編集します。

...
  @doc false
  def changeset(page, attrs) do
    page
    |> cast(attrs, [:title, :body]) # :viewsを消去
    |> validate_required([:title, :body]) # :viewsを消去
  end
end

マイグレーションファイルの調整

priv/repo/migrationsの該当するファイル(mix phx.gen.htmlで生成されたやつ)を編集します。

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

  def change do
    create table(:pages) do
      add :title, :string
      add :body, :text
      add :views, :integer, default: 0 # この箇所にデフォルト値を設定

      timestamps()
    end

  end
end

今回はスキームの変更によるものでは無いですが、おおよそスキームを変更した場合はマイグレーションファイルの調整が必要になるでしょう。

ルーティングの追加(router.ex)

CMSのContextを追加したのでPageのリソースが見れる様にします。
lib/hello_web/router.exを修正します。

...
  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController
    resources "/sessions", SessionController, only: [:new, :create, :delete], singleton: true
  end

  scope "/cms", HelloWeb.CMS, as: :cms do
    pipe_through [:browser, :authenticate_user]

    resources "/pages", PageController
  end
...

scope "/", HelloWeb do句を真似て書けば問題無いと思います。
ただし、今回はCMSに関連するページは認証が必要なようにしたいので:authenticate_userプラグを適応するようpipe_through [:browser, :authenticate_user]となっています。

マイグレートと認証機能の確認

ここまで来ると機能は実装されてないものの、だいたいの表示はできる筈です。
mix ecto.migrateでデータベースをマイグレートしてサーバーを起動してみます。

PS \hello> mix ecto.migrate
Compiling 6 files (.ex)
Generated hello app
[info] == Running 20190403003143 Hello.Repo.Migrations.CreatePages.change/0 forward
[info] create table pages
[info] == Migrated 20190403003143 in 0.0s
PS \hello> mix phx.server

生成したPageのURLはhttp://localhost:4000/cms/pagesです。ここにアクセスしてみます。

f:id:ysmn_deus:20190403100232p:plain

ログインしていないのでput_flashで怒られています。
http://localhost:4000/sessions/newにて、前回までに作成したユーザーでログインしてみます。

f:id:ysmn_deus:20190403102543p:plain

ヨシ!
認証系の機能がここまで簡単に追加できるのは驚愕です。
Plugの拡張性の高さヤバイ。

Authorの追加

ではPageを追加・・・と行きたいところですが、作成者の情報を紐付けておかないと面倒なことになりそうなので先にそちらを済ませましょう。
emailの情報をAccountsのContextに追加したようにphx.gen.contextCMSのContextにAuthorの情報を追加していきます。

スキームはこんなかんじ。

  • bio:text 出身?text形?昔のバージョンによく見られるっぽいんですが、たぶん:stringと同義。生成されたスキームでは:stringになってた。
  • role:string 役割。文字列。
  • genre:string ジャンル。文字列。
  • user_id:references:users:unique ユーザーID、ユーザーの情報と関連している
PS \hello> mix phx.gen.context CMS Author authors bio:text role:string genre:string user_id:references:users:unique
You are generating into an existing context.
The Hello.CMS context currently has 6 functions and 1 files in its directory.

  * It's OK to have multiple resources in the same context as     long as they are closely related
  * If they are not closely related, another context probably works better

If you are not sure, prefer creating a new context over adding to the existing one.

Would you like to proceed? [Yn] Y
* creating lib/hello/cms/author.ex
* creating priv/repo/migrations/20190403013056_create_authors.exs
* injecting lib/hello/cms.ex
* injecting test/hello/cms/cms_test.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate

UserとAuthorのContextを分けておくことで開発上いろんな利点がありそうです。
Authorに別情報が必要になってCMSのContextを編集する際でもAccounts側のUserは変更不要ですしCMS側にAcountsのUserデータ以外が流出することもないです。

マイグレーションファイルの調整

Accountsで認証情報を追加したときのように、従属関係にある際はon_deletenull: falseを設定しておいた方が良いです。(不用意にデータが残ってしまう可能性がある。)
なのでpriv/repo/migrationsに先ほどのmix phx.gen.contextで生成されたマイグレーションファイルを調整します。

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

  def change do
    create table(:authors) do
      add :bio, :text
      add :role, :string
      add :genre, :string
      add :user_id, references(:users, on_delete: :delete_all), null: false # ここを修正

      timestamps()
    end

    create unique_index(:authors, [:user_id])
  end
end

これでAuthorに関しては問題ありませんが、まだ従属関係はあります。
PageがAuthorに関連付いているので、この関連付けをデータベースに登録するためにマイグレーションファイルを作成します。
mix ecto.gen.migrationで任意のマイグレーションファイルを作成できるようです。

PS \hello> mix ecto.gen.migration add_author_id_to_pages
Compiling 2 files (.ex)
Generated hello app
* creating priv/repo/migrations/20190403015018_add_author_id_to_pages.exs

いつものようなタイムスタンプ+名前.exsが生成されました。
一応中身を見てみます。

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

  def change do

  end
end

このchange関数の中に処理を記載してマイグレーションすれば良さそうです。
いつものマイグレーションファイルはcreate table(hogehoge)doなどが記載されています。

今回はドキュメントに従い、下記のようにしました。

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

  def change do
    alter table(:pages) do
      add :author_id, references(:authors, on_delete: :delete_all), null: false
    end

    create index(:pages, [:author_id])
  end
end

create tableでテーブル作成ですが、alter tableでテーブルにカラムを追加できるようです。
add以降は割といつも通りでしょうか。
作成者が削除されたらページも削除されてしまうのはなんだか寂しい気がしますが、今回はCMSを作る練習と言うことでスルーします。

マイグレーションファイルの調整が完了すれば、やることは一つ。mix ecto.migrateマイグレーションします。

PS \hello> mix ecto.migrate
warning: variable "null" does not exist and is being expanded to "null()", please use parentheses to remove the ambiguity or change the variable name
  priv/repo/migrations/20190403013056_create_authors.exs:9

** (CompileError) priv/repo/migrations/20190403013056_create_authors.exs:9: undefined function add/4
    (elixir) src/elixir_locals.erl:107: :elixir_locals."-ensure_no_undefined_local/3-lc$^0/1-0-"/2
    (elixir) src/elixir_locals.erl:107: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir) lib/code.ex:715: Code.load_file/2
    (ecto_sql) lib/ecto/migrator.ex:489: Ecto.Migrator.load_migration/1
    (elixir) lib/enum.ex:1327: Enum."-map/2-lists^map/1-0-"/2
    (ecto_sql) lib/ecto/migrator.ex:435: Ecto.Migrator.do_migrate/4
    (ecto_sql) lib/ecto/migrator.ex:429: Ecto.Migrator.migrate/4
    (ecto_sql) lib/ecto/adapters/sql.ex:820: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection) lib/db_connection.ex:1415: DBConnection.run_transaction/4
    (ecto_sql) lib/ecto/adapters/sql.ex:727: Ecto.Adapters.SQL.lock_for_migrations/5
    (ecto_sql) lib/ecto/migrator.ex:318: Ecto.Migrator.lock_for_migrations/3
    (ecto_sql) lib/mix/tasks/ecto.migrate.ex:110: anonymous fn/4 in Mix.Tasks.Ecto.Migrate.run/2

null: falsenull, falseって打ってた、アホス。
修正して再実行。

PS \hello> mix ecto.migrate
[info] == Running 20190403013056 Hello.Repo.Migrations.CreateAuthors.change/0 forward
[info] create table authors
[info] create index authors_user_id_index
[info] == Migrated 20190403013056 in 0.0s
[info] == Running 20190403015018 Hello.Repo.Migrations.AddAuthorIdToPages.change/0 forward
[info] alter table pages
[info] create index pages_author_id_index
[info] == Migrated 20190403015018 in 0.0s

問題無さそう。
データベース周りの準備は良さそうなのでCMSとしての機能を実装していきます。

かなり長くなってるのでまた区切ります。
流石に次回アタリでケリをつけたい。

Phoenix入門 (第13章 Contexts その3)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
Contextsの3回目です。なんか3回じゃ終わらないかも・・・(´゚'ω゚`)

前回はContext内でリレーションを作りました。
今回はContextに機能を追加していくところになるかと。

Adding Account functions

とりあえず前回まででユーザー情報(IDとパスワード)とメール情報のCRUDはできました。(メールはユーザー情報経由ですが。)
ですが、基本的な機能だけで認証機能とかはまだありません。
お次は認証情報(今回はメールアドレス)を使ってセッションを作成したりするお話っぽいです。

想定としては、メールアドレスとパスワードを入力してユーザー情報を取得する、という流れです。
なので

> user = Accounts.authenticate_by_email_password(email, password)

という形でユーザー情報が得られ、この情報をセッションに渡せば良さそうです。(ユーザー情報のやりとりなんかはChannelの章あたりが参考になるかもしれません。)

Contextに関数を追加

この関数をContextのlib/hello/accounts.exに追記します。

...
  def authenticate_by_email_password(email, _password) do
    query =
      from u in User,
        inner_join: c in assoc(u, :credential),
        where: c.email == ^email
  
    case Repo.one(query) do
      %User{} = user -> {:ok, user}
      nil -> {:error, :unauthorized}
    end
  end
end

一番最後に追記しました。

ここで、^emailとなっていて、「ピンオペレータ?」と思ったんですが右辺だしなんやねんこれ、と思っていたんですがどうやらEctoのクエリとして外部の変数(クエリ内で出てこない、Elixir上の変数)を利用するときはハット(^)を変数の名前の前に付けるそうです。

hexdocs.pm

もうちょっとEctoと仲良くなる必要があるかもしれません。

とりあえずこの章ではパスワードは破棄してますが、もしパスワード認証を利用したい場合はGuardianComeoninというトークンを発行したりハッシュ化したりするElixirのライブラリがあるそうなので、それらを使うのが良いそうです。
今回はメールアドレスがあるかないかだけ判定します。

Webレイヤーを実装

それでは先ほどの関数を利用して認証ページのようなものを作成していきます。
なにはともあれまずはコントローラです。lib/hello_web/controllers/session_controller.exを作成します。




大枠は普通のコントローラと同じです。createでセッションを生成してPlug.ConnにユーザーIDを渡しています。(本来であればトークンの方が望ましい?)
configure_sessionあたりはセッションID固定化攻撃(Session Fixation)対策だそうです。

次に、ルーターを編集します。lib/hello_web/router.exです。

...
  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController
    resources "/sessions", SessionController, only: [:new, :create, :delete], singleton: true
  end
...

resources "/sessions"の行を追記しました。
コントローラには:new, :create, :deleteの3つしか実装していないので、resourcesの3つを利用するよう:onlyオプションを利用しています。
また、singletonオプションはURIにリソースのIDを作成しない設定だそうです。今回はセッションの作成用のURIなので/sessions/12345のようなIDは不要です。(セッション情報は接続情報に載っていくので)

あとは認証確認をPlugにしてルーター内で認証機能が必要なときに接続情報のパイプラインに乗せれるようにしておきます。同様にrouter.exの最後にでも書いときます。

...
  defp authenticate_user(conn, _) do
    case get_session(conn, :user_id) do
      nil ->
        conn
        |> Phoenix.Controller.put_flash(:error, "Login required")
        |> Phoenix.Controller.redirect(to: "/")
        |> halt()
      user_id ->
        assign(conn, :current_user, Hello.Accounts.get_user!(user_id))
    end
  end
end

use HelloWeb, :routerがあるのでrouter.exではimport Plug.Connが既になされています。なのでPlug.Conn.get_session/2get_session/2として利用できます。
この処理で:user_idがセッション情報内にあるかチェックしています。無ければput_flashでメッセージを送信してルートページへリダイレクトの後、Plug処理を停止という流れです。
あればセッション情報内に:current_userというキーでユーザーidを登録します。

このプラグは今は使いませんが、後々使うそうです。

ウェブレイヤー実装の最後にセッション生成用のビューを作成します。
lib/hello_web/views/session_view.exを作成して記載していきます。

defmodule HelloWeb.SessionView do
    use HelloWeb, :view
end

特に処理は不要なのでいたってシンプル。
テンプレートを作成します。lib/hello_web/templates/session/new.html.eex

<h1>Sign in</h1>

<%= form_for @conn, Routes.session_path(@conn, :create), [method: :post, as: :user], fn f -> %>
  <div class="form-group">
    <%= text_input f, :email, placeholder: "Email" %>
  </div>

  <div class="form-group">
    <%= password_input f, :password, placeholder: "Password" %>
  </div>

  <div class="form-group">
    <%= submit "Login" %>
  </div>
<% end %>

<%= form_for @conn, Routes.session_path(@conn, :delete), [method: :delete, as: :user], fn _ -> %>
  <div class="form-group">
    <%= submit "logout" %>
  </div>
<% end %>

シンプルなフォームです。
ここまで来ると、ようやくウェブ上で確認できます。
http://localhost:4000/sessions/newにアクセスします。

f:id:ysmn_deus:20190402105043p:plain

とりあえず表示は問題無さそうです。
試しに空送信してみると、きちんとエラーが表示されます。
パスワードは無視していますが、メールアドレスはデータベースを参照しています。試しに前回作成したメールアドレスでログインできるか入力します。

f:id:ysmn_deus:20190402105258p:plain

メールアドレスがあっていればルートページにリダイレクトされ、「Welcome back!」のメッセージが着いていると思います。

きりが良いのでココで終わらせたいのですが、逆にキリが悪い方が学習効率は上がる(ツァイガルニク効果)筈なのであとちょっとだけ進めます。

Cross-context dependencies

ユーザー認証はもう良さそうです(ホントはトークン発行にしたりなどありますが)。
お次はCMSたる所以のページをマネージする機能を実装していきたいと思います。

想定としては承認されたユーザー(管理者的な)はページを作成したり修正したりできるといったものです。現状ではAccountsというContextが存在してますが、CMSの機能はアカウント管理機能とは分離されていないと機能を実装したり拡張したりするときに非常にヤヤコシイことになります。ですのでContextを分けます。
ただし、タイトル的にはContext同士で依存関係をかくことにはなると思います。(100%分離するのは無理)

ということでCMSのContextを作成していくのですが、CMSの仕様を考えておきましょう。

  1. ページを作成したりアップデートしたりする
  2. ページは作成者に従属し、作成者に変更する権限がある
  3. 作成者の情報がページ上にあり、その役割("editior"や"writer"など)の情報もある

といったところでしょうか。
仕様やからページのリソースが必要なのは明確ですが、あとは「作成者」をどうするかです。
Accountsを拡張して役割を付与するのも手ですが、関係性がかなり複雑になるのが目に見えています。
ここではCMSのContextにAuthorというスキーマを作成し、このAuthorAccountsUserを関連付けるのが良さそうです。
基本的にユーザー情報と紐付けてなんかする、なんてのは山のようにあると思いますので、この辺の設計はこの章の設計を踏襲するのがよさそうです。というか、Accountsの設計はなんにでも流用できそうです。

さて、次回はmix phx.gen.htmlCMSのContextを作成して行きます。

Phoenix入門 (第13章 Contexts その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
引き続きContextsです。この分量だと3回に別れるかな・・・

In-context Relationships

Relationshipsということはようやくテーブルの関連付けなどでしょうか。

前回までに作成したAccountuserですが、メールアドレスなど登録情報が何もありません。(パスワードはありますが・・・)
そこで、ログイン用のメールアドレスを追加するのですが、これをuserテーブルに追加してしまうと外部認証(Googleアカウントとかでログインするやつ)に対応しようとすると大変なことになります。
なのでアカウント情報にメールアドレスのデータを紐付けて管理することにします。

Contextに追加する

そこでまたContextを作成していくのですが、前回作成したAccountにContextを追加していくようなことができるそうです。
phx.gen.htmlのようなコマンド、mix phx.gen.contextで追加していきます。

PS \hello> mix phx.gen.context Accounts Credential credentials email:string:unique user_id:references:users
You are generating into an existing context.
The Hello.Accounts context currently has 6 functions and 1 files in its directory.

  * It's OK to have multiple resources in the same context as     long as they are closely related
  * If they are not closely related, another context probably works better

If you are not sure, prefer creating a new context over adding to the existing one.

Would you like to proceed? [Yn] Y
* creating lib/hello/accounts/credential.ex
* creating priv/repo/migrations/20190401004125_create_credentials.exs
* injecting lib/hello/accounts.ex
* injecting test/hello/accounts/accounts_test.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate

今回はウェブ上でのCRUD操作は要らないのでmix phx.gen.contextで追加したようです。
(userのviewなどに機能を追加していく)
* injectinglib/hello/accounts.exファイルに追記されているのが分かります。

参照先が削除されたときの振る舞い(ON DELETE)

Credentialの情報(今回はメールアドレスだけ)はユーザーIDと1対1で関連付いているので、usersのデータが消去されたときには消えて欲しいです。
なのでその情報をmigrationsのテーブル追加スクリプトに追記します。
* creating priv/repo/migrations/20190401004125_create_credentials.exsを編集します(環境によってタイムスタンプは違う)。

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

  def change do
    create table(:credentials) do
      add :email, :string
      add :user_id, references(:users, on_delete: :delete_all), null: false # :nothing を :delete_all に

      timestamps()
    end

    create unique_index(:credentials, [:email])
    create index(:credentials, [:user_id])
  end
end

, null: falseも追記されてるのは、ユーザーと紐付いてない謎の認証情報が発生しないようにということでしょう。
これで良さそうなのでマイグレーションします。

PS \hello> mix ecto.migrate
Compiling 2 files (.ex)
Generated hello app
[info] == Running 20190401004125 Hello.Repo.Migrations.CreateCredentials.change/0 forward
[info] create table credentials
[info] create index credentials_email_index
[info] create index credentials_user_id_index
[info] == Migrated 20190401004125 in 0.0s

これでデータベース上のテーブルなどは良さそうです。

Contextで扱うスキーマの関係性

次にPhoenix側での関係性も記載しておきます。Contextのlib/hello/accounts/user.exに対応関係を追記します。

defmodule Hello.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Hello.Accounts.Credential # 忘れないで(戒め)

  schema "users" do
    field :name, :string
    field :username, :string
    has_one :credential, Credential # 追加

    timestamps()
  end
...

has_oneマクロはEctoの機能だそうで、他のスキーマとの関連性をEcto側で使える様にするものだそうです。
同様にcredential.exの方にも関連性を記載します。

defmodule Hello.Accounts.Credential do
  use Ecto.Schema
  import Ecto.Changeset
  alias Hello.Accounts.User # 忘れないで(戒め)

  schema "credentials" do
    field :email, :string
    belongs_to :user, User # 通常のフィールドからbelongs_toに変更

    timestamps()
  end
...

belongs_toマクロは先ほど同様Ectoのマクロで、Userに対して従属していることをEctoに知らせます。
この辺自動でやってくれないかなぁ。

preloadの設定

お次はgetなどの取得時にCredentialのデータを取っておくように書いておく設定をContextに書いておきます。
lib/hello/accounts.exを編集します。

...
  def list_users do
    User
    |> Repo.all()
    |> Repo.preload(:credential)
  end
...
  def get_user!(id) do
    User
    |> Repo.get!(id)
    |> Repo.preload(:credential)
  end
...

Repo.preload(:credential)を入れておくことで%Accounts.User{}の構造体の中にデータが載るようです。
あとなんか効率的にデータベースのデータが取得できる?かもしれないです。この辺はちょっとよく分かりませんでしたが、関連付いてて、十中八九使用するデータはpreloadしておくのが吉かもしれません。

入力のテンプレートに入力フォームを追加

ここまできてようやくウェブ上の方に機能を追加していきます。
まずは入力フォームにemailを記入できるようにlib/hello_web/templates/user/form.html.eexを編集します。

...
  <div class="form-group">
    <%= inputs_for f, :credential, fn cf -> %>
      <%= label cf, :email %>
      <%= text_input cf, :email %>
      <%= error_tag cf, :email %>
    <% end %>
  </div>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

分かりにくいですが、基本的に<%= ○○ %>を除けばElixirのコードです。
inputs_for関数?はf(ここではUserのフォーム)に関連付いた情報で:credentialの入力を作成する、という構文のようです。fn cf ->以降は他の入力フォームと同じです。
一応ソースに直すのであれば

inputs_for f, :credential, fn cf ->
  label cf, :email
  text_input cf, :email
  error_tag cf, :email
end

のようになってる感じです。各関数?マクロ?は入力のタグを追加するってところでしょうか。

出力のテンプレートに表示を追加

あとは表示を追加しておきましょう。
lib/hello_web/templates/user/show.html.eexを編集します。

<h1>Show User</h1>

<ul>

  <li>
    <strong>Name:</strong>
    <%= @user.name %>
  </li>

  <li>
    <strong>Username:</strong>
    <%= @user.username %>
  </li>

  <li>
    <strong>Email:</strong>
    <%= @user.credential.email %>
  </li>
...

このへんは楽勝ですね。
この時点でPhoenixは機能すると思います。一応mix phx.serverhttp://localhost:4000/users/newにアクセスしてみます。

f:id:ysmn_deus:20190401105646p:plain

('ω')。o(????????????)

UndefinedFunctionError at GET /users/new
function Credential.__struct__/0 is undefined (module Credential is not available)

Credentialが使えない、なんでや。
ちょっとソースを見直した結果、Contextのそれぞれ(lib/hello/accounts/user.excredential.ex)にエイリアスを書くのを忘れていた。アホス。
追記して再度アクセス。

f:id:ysmn_deus:20190401110102p:plain

ヨシ!

ContextのCreateとUpdateを変更(関連データのバリデーション)

とはいっても、現段階ではただ表示されているだけで実際に保存しようとすると

UndefinedFunctionError at GET /users/2
function nil.email/0 is undefined

になると思います。これはContextのupdate_userなどが古いままなのでメール情報を格納するステップが抜けてるので、「メール情報ないやんけ!」と怒られてるのでしょう。
この辺を追加します。Contextのlib/hello/accounts/accounts.exを編集します。

defmodule Hello.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false
  alias Hello.Repo

  alias Hello.Accounts.{User, Credential} # Accountsの後ろに.をつけ忘れてた
...
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Ecto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2)
    |> Repo.insert()
  end
...
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Ecto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2)
    |> Repo.update()
  end
...
#  alias Hello.Accounts.Credential # 消してもヨシ、上で呼んでるので冗長
...

Repo.insertなどする前のパイプラインにEcto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2)を追加しました。
Ecto.Changeset.cast_assoc/3はどうやら第一引数に大元の関連付いた構造体(今回だとユーザーの構造体)、第二引数に関連データを示すアトム(今回だと:credential、たぶんユーザー構造体に追加されたキーとして読み込んでる)、第三引数に関連データのchangesetを渡してバリデーションするといった流れでしょう。

これで行けるそうです。やってみます。

SyntaxError
lib/hello/accounts.ex:9: syntax error before: '{'

(╬´◓ω◔╬) 凡ミスです。alias Hello.Accounts{User, Credential}となっていたのでalias Hello.Accounts.{User, Credential}`に修正。

f:id:ysmn_deus:20190401112417p:plain

行けました。メールアドレスをブランクで保存しようとしても、きちんとバリデーションではじかれます。
もっときっちりするならCredentialでメールアドレスの形式でバリデーションしてもいいかもしれません。

もう一項目行きたかったんですがちょっと長くなってるのでこの辺で区切ります。

S3の静的ウェブサイトホスティング(2019年03月31日現在)

f:id:ysmn_deus:20190331130214p:plain

どうも、靖宗です。
S3で静的ウェブサイトを公開するときにちょっと詰まったのでメモ。
最初は下記のQiita記事を参考にさせていただきました。

qiita.com

また、当然のことだとは思いますが、アクセス権などをいじるので、その辺は自己責任でお願いします。

基本的な方針

基本的には

  • S3のバケットのルート(直下)にindex.htmlを配置する
  • CloudFrontは挟まない

という想定で行います。一般公開するならPVにもよるんでしょうがCloudFrontは挟んだ方が良いと思います。(Basic認証なんかもLambda使えばすぐですし。)

バケットの作成

なにはともあれバケットを作成しないことには始まりません。
S3のコンソールからバケットを作成します。

f:id:ysmn_deus:20190331130741p:plain

適当に名前を付けて次へ。
リージョンはどこでも良いと思うんですが、日本向けなら東京リージョンが良いんじゃ無いでしょうか。
プロパティとかはすっ飛ばして次へ。

f:id:ysmn_deus:20190331130855p:plain

ここで、パブリックアクセスの設定が出てきます。

f:id:ysmn_deus:20190331131104p:plain

ここでコントロールしてもいいんですが、一旦推奨設定を利用するという事で次へに行きます。
慣れてきたらここで後ほど説明する設定を適応しても良いと思います。

最後に確認画面が出てくるので問題無ければ「バケットを作成」
成功すれば一覧の所に先ほど決めた名前でバケットができてると思います。

f:id:ysmn_deus:20190331131314p:plain

ファイルのアップロード

早速ファイルをアップロードしていきます。
一応ルートディレクトリでなくてもホスティングできると思いますが、今回はルートに配置します。
適当に作ったindex.htmlを利用します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>S3</title>
  <link href="" rel="stylesheet">
</head>
<body>
hoge
</body>
</html>

S3にアップロード

f:id:ysmn_deus:20190331131705p:plain

f:id:ysmn_deus:20190331131923p:plain

f:id:ysmn_deus:20190331131939p:plain

f:id:ysmn_deus:20190331131953p:plain

深いことは考えずとりあえずアップロードしました。
ここでもパブリックアクセスなどの設定で後々の手順をスキップできるのですが、今回は手順を極力分離します。

静的ウェブサイトホスティングの機能を有効にする

ホスティング機能を有効にします。S3のバケットのページからプロパティのページへ移動します。

f:id:ysmn_deus:20190331132243p:plain

Static website hostingと書かれた箇所をクリックします。

f:id:ysmn_deus:20190331132331p:plain

f:id:ysmn_deus:20190331132445p:plain

何もしなければここで記載されている「エンドポイント」がアクセスするURLになります。
独自ドメインの設定なんかはまた別の機会に。(というかぐぐれば出てくるとは思います) 設定を「ウェブサイトのホスティングを無効にする」から「このバケットを使用してウェブサイトをホストする」に変更します。

f:id:ysmn_deus:20190331132549p:plain

インデックスドキュメントのところに先ほどアップロードしたindex.htmlを指定します。
たぶんhtmlとかいうディレクトリに格納してる場合はhtml/index.htmlとかでもいけるとは思います。

設定を変更したら保存を押して設定完了。

こうすると、先ほどのエンドポイントで指定したURLでウェブサイトのホスティング

できないんですよこれが。

バケットポリシーの変更

S3のバケットのページからアクセス権限のページへ移動します。

f:id:ysmn_deus:20190331133444p:plain

f:id:ysmn_deus:20190331133730p:plain

いままで通りにバケットを作成していれば、上記のようになっていると思います。
ここの「パブリックバケットポリシーを管理する」がFalseになっていないとバケットポリシーを変更できません。ですので、この設定と、「バケットにパブリックポリシーがある場合、パブリックアクセスとクロスアカウントアクセスをブロックする」という設定をFalseにします。二個目の奴はバケットポリシーが変更されてもアクセスをはじく設定なので消しときます。

f:id:ysmn_deus:20190331135442p:plain

f:id:ysmn_deus:20190331135455p:plain

f:id:ysmn_deus:20190331135546p:plain

これでバケットポリシーが変更できるようになりました。

お次はバケットポリシーを変更していきます。

f:id:ysmn_deus:20190331135708p:plain

ポリシーエディターでポリシーを設定します。 自分は下記のように設定しましたが、ここは適宜変更して下さい(バケット名など)

 {
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"AddPerm",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::バケット名/*"]
    }
  ]
}

保存して完了です。公開できる設定になっていれば、アクセス権限やバケットポリシーの所に「パブリック」の表示が出ていると思います。

これでエンドポイントにアクセスして表示されれば静的ウェブサイトホスティングができています!
今回はホスティング機能だけ記載しましたが、本来であればCloudFrontを経由してアクセスさせるのが適切だと思います。

補足

もしかしたら場合によってはindex.htmlのアクセス権限をいじる必要があるかもしれません。
先ほど試した限りでは大丈夫でしたが、もしうまくいかない方がいらっしゃったらその辺をいじってみて下さい。

Phoenix入門 (第13章 Contexts その1)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
最近AWSばっかり触っててご無沙汰でしたが、若干落ち着きそうなのでPhoenixの学習を再開していきます。
今回はContextsということですが、これもまた結構長そうな項目です。

Contexts

この章(機能?)いままでの機能を組み合わせるにはどうするか、などでしょうか。
確かに各々の機能にだけ注目してもなかなか実装できないことってありますよね。

Thinking about design

Elixirの標準ライブラリを利用する際に、内部実装を気にする必要は特にありません。Logger.info/1などは一度使い方を学べば特に内部で○○モジュールが動いてて・・・などと気にする人はそこまでいないと思います。(場合によっては必要だと思いますが)
Contextを利用することでPhoenixプロジェクトもElixirのモジュールのように外部から利用できるようにする機能という感じでしょうか。

この章では簡単なCMSを作成していくことで、このContextを学習しましょう!という方針のようです。

Adding an Accounts Context

まずはアカウントの実装からやっていくようです。
どんなシステムでもアカウントの登録や編集などはあると思いますし、わりかしこの辺のデザインがシステム全体に大きな影響を及ぼします。

前回のEctoの章で使ったプロジェクトを利用していくようですが、自分は新しく作り直します(作業してたプロジェクトがどっかいった)。
前回はユーザーのスキームを手動で作成しましたが、なにやらGeneratorsというもので生成するっぽいので、もし引き続きの人がいれば一旦消します。

$ rm lib/hello/user.ex
$ rm priv/repo/migrations/*_create_users.exs

マイグレーションのファイルも消してるっぽいです。
データベースのリセットも行っています。

mix ecto.reset

自分は新しく作り直したのでmix ecto.createまではいつも通りと同じです。

さて、ここまできてようやくContextを利用していくようです。GeneratorでアカウントのContextを作成していきます。
プロジェクトのルートフォルダでmix phx.gen.htmlとしていくと、どうやらCRUDのような機能まで実装してくれるっぽいです。

PS \hello> mix phx.gen.html Accounts User users name:string username:string:unique
* creating lib/hello_web/controllers/user_controller.ex
* creating lib/hello_web/templates/user/edit.html.eex
* creating lib/hello_web/templates/user/form.html.eex
* creating lib/hello_web/templates/user/index.html.eex
* creating lib/hello_web/templates/user/new.html.eex
* creating lib/hello_web/templates/user/show.html.eex
* creating lib/hello_web/views/user_view.ex
* creating test/hello_web/controllers/user_controller_test.exs
* creating lib/hello/accounts/user.ex
* creating priv/repo/migrations/20190331023135_create_users.exs
* creating lib/hello/accounts.ex
* injecting lib/hello/accounts.ex
* creating test/hello/accounts/accounts_test.exs
* injecting test/hello/accounts/accounts_test.exs

Add the resource to your browser scope in lib/hello_web/router.ex:

    resources "/users", UserController


Remember to update your repository by running migrations:

    $ mix ecto.migrate

今まではlib/hello_web/側のPhoenixのウェブ機能側にしか色々ファイルは自動生成されませんでしたが、lib/hello/側にもファイルが生成されました。これがContextでしょうか。
lib/hello/accounts/user.exスキーマの定義ファイルも生成されています。

とりあえずウェブ側の実装をしちゃいます。
lib/hello_web/router.exを編集します。

...
  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController
  end
...

先ほどのmix phx.gen.htmlでuser関連のファイルは生成されているので、大体これで終わっちゃいます。やべえ。
スキーマの追加などされているのでデータベースをマイグレーションして起動してみます。

PS \hello> mix ecto.migrate
Compiling 6 files (.ex)
Generated hello app
[info] == Running 20190331023135 Hello.Repo.Migrations.CreateUsers.change/0 forward
[info] create table users
[info] create index users_username_index
[info] == Migrated 20190331023135 in 0.0s
PS \hello> mix phx.server

追加したページのhttp://localhost:4000/usersにアクセスします。

f:id:ysmn_deus:20190331114745p:plain

入力フォームまで既に用意されています。

f:id:ysmn_deus:20190331114832p:plain

なんでもいいけどとっとと実装したいときとかめっちゃ便利そうですね。
バリデーションやエラー表示までデフォルトでやってくれます。

f:id:ysmn_deus:20190331114946p:plain

ちなみに成功する表示もバッチリです。

f:id:ysmn_deus:20190331115220p:plain

f:id:ysmn_deus:20190331115313p:plain
ちゃんとリストページにも追加されてる

リストページからは編集や削除もできます。ありがたや。
デフォルトで生成されるCRUDAPIとしてのベストプラクティスみたいなものという認識でも結構有り難い機能です。

Starting With Generators

ただ、一応今回の目的はPhoenixプロジェクトのウェブ表示の機能以外でもユーザーの情報が取り扱えるようにContexを利用してみることです。
これじゃPhoenixのウェブ表示便利機能やんけ!と思うかもしれませんが、ここでuser_controller.exを見てみます。

defmodule HelloWeb.UserController do
  use HelloWeb, :controller

  alias Hello.Accounts
  alias Hello.Accounts.User

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset = Accounts.change_user(%User{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: Routes.user_path(conn, :show, user))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
  ...
end

そうなのです、

  alias Hello.Accounts
  alias Hello.Accounts.User

で、AccountsのContextを利用してコントローラが実装されていることが分かります。
これによって、ウェブ表示の機能とデータベース周りの機能が分離されていることが分かります。
仮にデータベースがPostgreSQLからMySQLに変わろうがPhoenix側ではなんら問題無いですし、なんなら設計次第ではテーブル構造が変わっても吸収できる実装も可能ということです。

次にAccountのContextであるlib/hello/accounts.exを見ていきます。

defmodule Hello.Accounts do
  @moduledoc """
  The Accounts context.
  """

  import Ecto.Query, warn: false
  alias Hello.Repo

  alias Hello.Accounts.User

  @doc """
  Returns the list of users.

  ## Examples

      iex> list_users()
      [%User{}, ...]

  """
  def list_users do
    Repo.all(User)
  end
  ...
end

非常にシンプルなモジュールです。Repoに対する操作をラップするようなモジュールになっています。
このContextに関数を追加することで、操作をカスタマイズしたり、新しい機能を実装したりして、様々な箇所でその機能を共有するのでしょう。この辺はわりとオブジェクト指向っぽい(カプセル化)ですが、機能のみの実装ですので似て非なる物です。

ここで、ユーザー作成の関数Accounts.create_user/1を見てみます。

  @doc """
  Creates a user.

  ## Examples

      iex> create_user(%{field: value})
      {:ok, %User{}}

      iex> create_user(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

(すっかり忘れてましたがcreate_user(attrs \\ %{})\\ %{}はデフォルトの引数は%{}やで!という事を意味してます。)
スキーマの作成や更新はchangesetでやるというのは前回やりました。
changesetの実装を見てみます。

defmodule Hello.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Hello.Accounts.User


  schema "users" do
    field :name, :string
    field :username, :string

    timestamps()
  end

  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:name, :username])
    |> validate_required([:name, :username])
    |> unique_constraint(:username)
  end
end

前回はスルーしましたが、changesetの前に@doc属性がfalseになっています。
これはどうやらドキュメントにこの情報が載らないようにするための属性だそうで、「プライベートな(他のAPIからは参照されない)実装」というところを意味するそうです。
(実際に外部から使えなくなる訳では無い)
なので、別のモジュールからuserの作成や更新をするときは、必ずAccountのContextを通して行うという方針を採るようです。
このへんはローカルルールみたいなもんなんで指針だけ把握してれば良いんじゃ無いかと思います。

今回はこの辺で!

Phoenix入門 (第12章 Ecto その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
Ectoの続きです。前回はほとんど仕様みたいな感じになってたので実際に扱う所までできたらなぁと思います。

Ecto

Changesets and Validations

概要

Changesetsは前回みたスキーマが定義されているファイルにある関数で、データをデータベースに登録する前に型変換やバリデーションが行われる関数のようです。
バリデーションで不正な値をはじいたり、扱ってるデータのフィールドが更新されているか否かも取得できるみたいです。

def changeset(%User{} = user, attrs) do
  user
  |> cast(attrs, [:name, :email, :bio, :number_of_pets])
  |> validate_required([:name, :email, :bio, :number_of_pets])
end

changeset/2関数の中にはパイプラインが存在していて、userデータがcast/3validate_required/3という関数を通過しています。
cast/3は第一引数に構造体、第二引数にパラメータ(値の登録や更新に使われる)、第三引数にアップデートするカラムの指定を渡す仕様のようです。役割としてはスキーマのフィールドを取ってきてるだけのようです。
validate_required/3cast/3の返値を第一引数で受けて、第二引数に渡されたカラムのデータをバリデーションするようです。

実際に扱う

なにはともあれ動かして確認するのが一番です。iexで確認できるようなのでやってみます。
ややこしいので最初にalias HelloPhoenix.Userを使って、いちいちHelloPhoenix.User.changesetとか書かないでいいようにしておきます。

PS \hello_phoenix> iex.bat -S mix
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> alias HelloPhoenix.User
HelloPhoenix.User

空のUser構造体を利用してchangesetを使ってみます。

iex(2)> changeset = User.changeset(%User{}, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    name: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]},
    bio: {"can't be blank", [validation: :required]},
    number_of_pets: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>

なにか帰ってきましたがエラーが出てるっぽいです。まぁ空でバリデーションしてるんであたりまえなんですが。
changeset.valid?ではじかれてるかどうか判断できるようです。

iex(3)> changeset.valid?
false

changeset.errorsで何がマズいのか見れます。各フィールドのエラーが参照できてますね。

iex(4)> changeset.errors
[
  name: {"can't be blank", [validation: :required]},
  email: {"can't be blank", [validation: :required]},
  bio: {"can't be blank", [validation: :required]},
  number_of_pets: {"can't be blank", [validation: :required]}
]

全てが必須項目になってます。

バリデーションの編集

number_of_petsをオプション(必須ではない)にしてみましょう。
changesetのパイプラインのvalidate_requiredを編集します。

...
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio]) # これ
  end
...

iexで再コンパイルして実行します。

iex(5)> recompile()
Compiling 1 file (.ex)
:ok
iex(6)> changeset = User.changeset(%User{}, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    name: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]},
    bio: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>
iex(7)> changeset.errors
[
  name: {"can't be blank", [validation: :required]},
  email: {"can't be blank", [validation: :required]},
  bio: {"can't be blank", [validation: :required]}
]

number_of_petsのエラーは消えました。必須か否かはvalidate_requiredに渡すリストで制御されていることが確認できました。

不要なデータ(castの動作チェック)

changesetに渡すパラメータの中にスキーマに登録されていないデータが存在した場合にはどうなるでしょうか?(バリデーションではじかれるか無視される?)
実際にやってみます。

iex(8)> params = %{name: "Joe Example", email: "joe@example.com", bio: "An example to all", number_of_pets: 5, random_key: "random value"}
%{
  bio: "An example to all",
  email: "joe@example.com",
  name: "Joe Example",
  number_of_pets: 5,
  random_key: "random value"
}

random_key: "random value"という余分なキーペアを入れたマップを作成しました。
これをchangesetに渡します。

iex(9)> changeset = User.changeset(%User{}, params)
#Ecto.Changeset<
  action: nil,
  changes: %{
    bio: "An example to all",
    email: "joe@example.com",
    name: "Joe Example",
    number_of_pets: 5
  },
  errors: [],
  data: #HelloPhoenix.User<>,
  valid?: true
>

はじかれはしないようです。
バリデーションは正常でしょうか?

iex(10)> changeset.valid?
true

問題ナシ。では値は?

iex(11)> changeset.changes
%{
  bio: "An example to all",
  email: "joe@example.com",
  name: "Joe Example",
  number_of_pets: 5
}

random_key: "random value"は綺麗さっぱり消滅しました。おそらくcast/3のところでスキーマに無いパラメータは落とされるのでしょう。

他のバリデーション

RDBで様々な制約などが考えられますし、そもそもの制約(英語だけとか)もあるので、必須か否かのバリデーションだけでは心許ないです。
もちろん他のバリデーションもあるようなので見ていきます。まずは、データ長を指定するvalidate_lengthから。

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio])
    |> validate_length(:bio, min: 2)
  end

これで最小2文字というバリデーションがかかるようです。パイプラインで完結に書けるのはElixirの良いところですね。
実際に1文字とかを登録してバリデーションではじかれるかやってみます。

iex(12)> recompile()
Compiling 1 file (.ex)
:ok
iex(13)> changeset = User.changeset(%User{}, %{bio: "A"})
#Ecto.Changeset<
  action: nil,
  changes: %{bio: "A"},
  errors: [
    bio: {"should be at least %{count} character(s)",
     [count: 2, validation: :length, kind: :min]},
    name: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>
iex(14)> changeset.errors[:bio]
{"should be at least %{count} character(s)",
 [count: 2, validation: :length, kind: :min]}

長さ足りないぞ!って怒られてます。
最大長さはminのところをmaxにすればよさそうです。

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio])
    |> validate_length(:bio, min: 2)
    |> validate_length(:bio, max: 140)
  end

お次はフォーマットを決めるvalidate_format/3。例としてemailのフォーマットを@が入ってないと怒るようにします。

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio])
    |> validate_length(:bio, min: 2)
    |> validate_length(:bio, max: 140)
    |> validate_format(:email, ~r/@/)
  end

最後の引数に渡す正規表現でフォーマットを決定しているようです。
実際にはじかれるか確認します。

iex(22)> changeset = User.changeset(%User{}, %{email: "example.com"})
#Ecto.Changeset<
  action: nil,
  changes: %{email: "example.com"},
  errors: [
    email: {"has invalid format", [validation: :format]},
    name: {"can't be blank", [validation: :required]},
    bio: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>
iex(23)> changeset.errors[:email]
{"has invalid format", [validation: :format]}

長さの時もそうでしたが、一応changesに値は格納されるようです。(ただし、changeset.valid?はfalse)

Data Persistence

スキーマとかを見て来ましたが、これらを取得したり格納したりはまだでした。たぶんそんな話。
lib/hello_phoenix/repo.exを前回みましたが、ココの編集次第ではPostgreSQLなどだけでなくRESTful APIもEctoで扱えるみたいです。それは便利そう。

insert

とりあえずiexで触ってみます。最初にUserエイリアスも付けてますが、さっきやってるので本来は不要です。

iex(24)> alias HelloPhoenix.{Repo, User}
[HelloPhoenix.Repo, HelloPhoenix.User]
iex(25)> Repo.insert(%User{email: "user1@example.com"})
[debug] QUERY OK db=16.0ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", ~N[2019-03-22 02:27:19], ~N[2019-03-22 02:27:19]]
{:ok,
 %HelloPhoenix.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil,
   email: "user1@example.com",
   id: 1,
   inserted_at: ~N[2019-03-22 02:27:19],
   name: nil,
   number_of_pets: nil,
   updated_at: ~N[2019-03-22 02:27:19]
 }}
iex(26)> Repo.insert(%User{email: "user2@example.com"})
[debug] QUERY OK db=16.0ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", ~N[2019-03-22 02:28:00], ~N[2019-03-22 02:28:00]]
{:ok,
 %HelloPhoenix.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil,
   email: "user2@example.com",
   id: 2,
   inserted_at: ~N[2019-03-22 02:28:00],
   name: nil,
   number_of_pets: nil,
   updated_at: ~N[2019-03-22 02:28:00]
 }}

Repo.insert/1でデータを格納できるようです。様々な情報(何秒かかったとか)はデバッグ環境だから出ているものだそうで、基本的には返値の{:ok, %User{}}で成功か否かを判断するようです。
一応PostgreSQLに格納されてるか見ときます。

PS > docker exec -it ph_psql /bin/bash
root@c1b71c3bc403:/# psql -U elixir
psql (10.5 (Debian 10.5-1.pgdg90+1))
Type "help" for help.

elixir=# \connect hello_phoenix_dev
You are now connected to database "hello_phoenix_dev" as user "elixir".
hello_phoenix_dev=# select * from users;
 id | name |       email       | bio | number_of_pets |     inserted_at     |     updated_at
----+------+-------------------+-----+----------------+---------------------+---------------------
  1 |      | user1@example.com |     |                | 2019-03-22 02:27:19 | 2019-03-22 02:27:19
  2 |      | user2@example.com |     |                | 2019-03-22 02:28:00 | 2019-03-22 02:28:00
(2 rows)

2行追加されています。

all

今度はデータを取り出してみます。スキーマを指定すれば該当するテーブルからデータを取り出せるようです。
とりあえず全部取り出します。

iex(27)> Repo.all(User)
[debug] QUERY OK source="users" db=0.0ms
SELECT u0."id", u0."bio", u0."email", u0."name", u0."number_of_pets", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
  %HelloPhoenix.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    bio: nil,
    email: "user1@example.com",
    id: 1,
    inserted_at: ~N[2019-03-22 02:27:19],
    name: nil,
    number_of_pets: nil,
    updated_at: ~N[2019-03-22 02:27:19]
  },
  %HelloPhoenix.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    bio: nil,
    email: "user2@example.com",
    id: 2,
    inserted_at: ~N[2019-03-22 02:28:00],
    name: nil,
    number_of_pets: nil,
    updated_at: ~N[2019-03-22 02:28:00]
  }
]

取ってきたデータは既にElixirの構造体になっており、非常に便利です。

Ecto.Query

Ecto.Queryを利用すると様々な機能が使える上にSQLインジェクション攻撃防止や最適化もしてくれるようです。

iex(28)> import Ecto.Query
Ecto.Query
iex(29)> Repo.all(from u in User, select: u.email)
[debug] QUERY OK source="users" db=0.0ms
SELECT u0."email" FROM "users" AS u0 []
["user1@example.com", "user2@example.com"]

SQL文の様な書き方ができるようになりました。
from u in UserでUser構造体のテーブルの各要素をuとするとき、という条件を付け、第二引数のselect: u.emailでu(各要素)のemailを抽出する処理になるようです。
少々SQL文とは違いますが雰囲気一緒なので慣れればどうということは無さそうです。

別の例を実行します。

iex(30)> Repo.one(from u in User, where: ilike(u.email, "%1%"), select: count(u.id))
[debug] QUERY OK source="users" db=0.0ms queue=15.0ms
SELECT count(u0."id") FROM "users" AS u0 WHERE (u0."email" ILIKE '%1%') []
1

Repo.oneは結果が1個のクエリを発行する際に使われる関数のようで、where句やselect句を取っています。
ilikeやcountはPostgreSQLなどに準拠する物です。

Ecto.Queryを利用すればSQLの様な形で直接マップのリストなどを作成することができます。

iex(33)> Repo.all(from u in User, select: %{u.id => u.email})
[debug] QUERY OK source="users" db=0.0ms
SELECT u0."id", u0."email" FROM "users" AS u0 []
[%{1 => "user1@example.com"}, %{2 => "user2@example.com"}]

updateなど

Repoには当然のことRepo.update/1Repo.delete/1などが実装されています。その他にもRepo.insert_allRepo.update_allRepo.delete_allも実装されています。

その他詳細

とはいえココで全部の機能を書ききれる訳ではないのでより知りたい方はEctoのドキュメントを参照とのこと。

hexdocs.pm

近いうちにやります。

リレーションとか

どうやらこの章ではデータベースのリレーションなどには言及しないようです。
Contextの章(次章)で言及があるそうですが、クッソ長いや~~~ん・・・( ´゚д゚`)
英語の練習と思ってがんばります

Using MySQL

MySQLを利用するときの指南書。
たぶんMySQLじゃないとだめってシーン少ないと思うので素直にPostgreSQLを利用するのが良いんじゃないかとは思いますが、さっと見ておきます。

まずmix phx.newするときに明示的にmysqlを利用するオプションを付けるようです。たぶん付けなくてもあとで変更可能だとは思うんですがめんどくさいと思います。

mix phx.new hello_phoenix --database mysql

あとはmix deps.getして完了!簡単ですね。

では、既存のプロジェクトをMySQLに変更する場合。
まずmix.exsを編集します。

defmodule HelloPhoenix.MixProject do
  use Mix.Project

  . . .
  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.4.0"},
      {:phoenix_pubsub, "~> 1.1"},
      {:phoenix_ecto, "~> 4.0"},
      {:ecto_sql, "~> 3.0"},
      {:mariaex, ">= 0.0.0"}, # これを追加
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:gettext, "~> 0.11"},
      {:plug_cowboy, "~> 2.0"}
    ]
  end
end

次にconfig/dev.exsを編集。

config :hello_phoenix, HelloPhoenix.Repo,
username: "root",
password: "",
database: "hello_phoenix_dev"

必要に応じてconfig/test.exsconfig/prod.secret.exsも編集。
お次にlib/hello_phoenix/repo.ex

    adapter: Ecto.Adapters.Postgres

Ecto.Adapters.MySQLに変更。

以上で完了のはず!mix do deps.get, compileして、mix ecto.createすれば利用できるはずです。

なんにせよ途中でデータベースを変更するのはあまり推奨されることでは無いとは思います。
可能であればRESTful APIで分離して管理しておけばAPI側でデータベースを変更するのは楽だと思います。

Phoenix入門 (第12章 Ecto その1)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
今回はEcto、ということでデータベースラッパーのお話でしょうか。
Elixirでよく使われるライブラリなので個別に触りたいところでもありますが、とりあえず今回はPhoenixのドキュメントを読み進めます。

Ecto

ウェブシステムを開発する上で避けられないのがデータベース絡みのお話です。
(とはいえFirebaseなど便利なものも出てきているので、薄ぼんやりとした理解でもある程度のものができてしまう時代だとは思います。)
Ectoは現在

の5つのRDBMSに対応してるそうです。Mnesiaなんかは初耳です。
特に何も指定しない場合はPhoenixPostgreSQLをチョイスします。

Hello, Ecto

開発用のPostgreSQL

最初のUp and Runningでも触れましたが開発用のデータベースの設定はconfig/dev.exsに記載されてます。ユーザー名や参照先を変更したい場合はこのファイルを編集します。
デフォルトでは参照先はlocalhost、ユーザー名はpostgres、パスワードはpostgresとなっています。このpostgresのユーザーを作成するにはpsqlコマンドでrootユーザーとしてログインした後下記のコマンドで作成できます。

CREATE USER postgres;
ALTER USER postgres PASSWORD 'postgres';
ALTER USER postgres WITH SUPERUSER;

簡単に触ってみる

とりあえずなんか動かしてみます。mix phx.gen.schemaでEctoのスキーマを作成できるそうです。
EctoのスキーマはElixirのデータタイプをPostgreSQLのテーブルへと変換するルールのようなものでしょうか。
とりあえずドキュメントどおりやってみます。

PS \hello_phoenix> mix phx.gen.schema User users name:string email:string bio:string number_of_pets:integer
* creating lib/hello_phoenix/user.ex
* creating priv/repo/migrations/20190321014808_create_users.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate

ファイルが2個生成され、始めてlib/hello_phoenix/の方にファイルができました。なんか嬉しい。
1個目は先ほど定義したスキーマの情報が記載されています。2個目は名前や中身的に現在のデータベースに先ほど定義した情報のテーブルを作成するための記述でしょう。ORMとかでよくありそうな感じです。

スキーマを登録したらマイグレーション(データベース作ったりリレーションの整合性とったりとか)が必要な筈です。もしmix ecto.createしてなければおそらくそちらでスキーマの取り込みまで実行されると思いますが、基本的に開発中にどんどん追加していくと思いますので、mix ecto.migrateマイグレーションします。

PS \hello_phoenix> mix ecto.migrate
Compiling 1 file (.ex)
Generated hello_phoenix app
[info] == Running 20190321014808 HelloPhoenix.Repo.Migrations.CreateUsers.change/0 forward
[info] create table users
[info] == Migrated 20190321014808 in 0.0s

データベースにusersのテーブルが作成されているか確認します。
自分はdockerでpostgresを動かしてるのでdockerから確認します。

PS \hello_phoenix> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
c1b71c3bc403        postgres            "docker-entrypoint.s…"   3 weeks ago         Up 3 days           0.0.0.0:5432->5432/tcp   ph_psql
PS \hello_phoenix> docker exec -it ph_psql /bin/bash
root@c1b71c3bc403:/# psql -U elixir
psql (10.5 (Debian 10.5-1.pgdg90+1))
Type "help" for help.

elixir=# \connect hello_phoenix_dev
You are now connected to database "hello_phoenix_dev" as user "elixir".
hello_phoenix_dev=# \d
               List of relations
 Schema |       Name        |   Type   | Owner
--------+-------------------+----------+--------
 public | schema_migrations | table    | elixir
 public | users             | table    | elixir
 public | users_id_seq      | sequence | elixir
(3 rows)

テーブル作成されていることが確認できます。
usersだけでなくusers_id_seqというテーブルもできていますが、コレはusersのシーケンスだそうです。いままでDjangoなどのORMに頼りっきりでPostgreSQL使ってるのにシーケンスという概念を知りませんでしたが、どうやらユニークなIDを生成するオブジェクトだそうで、ユーザー情報を生成するときに連番の番号を高速に生成して登録したり検索の時によしなにしてくれるもんだと思っておけばいいんじゃないでしょうか(適当)
一応usersの中身も見ておきます。

hello_phoenix_dev=# \d users
                                            Table "public.users"
     Column     |              Type              | Collation | Nullable |              Default
----------------+--------------------------------+-----------+----------+-----------------------------------
 id             | bigint                         |           | not null | nextval('users_id_seq'::regclass)
 name           | character varying(255)         |           |          |
 email          | character varying(255)         |           |          |
 bio            | character varying(255)         |           |          |
 number_of_pets | integer                        |           |          |
 inserted_at    | timestamp(0) without time zone |           | not null |
 updated_at     | timestamp(0) without time zone |           | not null |
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

idは先ほど説明したシーケンスの物なのでいいとして、inserted_atupdated_atが着いてます。(ORM使いまくりマンとしては有り難いです)
これは先ほどmix phx.gen.schemaで生成された一個目のファイルusers.exに記載があります。

...
  schema "users" do
    field :bio, :string
    field :email, :string
    field :name, :string
    field :number_of_pets, :integer

    timestamps()
  end
...

この辺をカスタマイズしたい欲求はあまり無いと思いますが、変更するならこの辺をいじるっぽいです。
プライマリキーを変更する方法はまだ特に記載がありませんが、基本デフォルトでいい気はします。

The Repo

Phoenixでアプリケーションを生成した際に生成されるファイルlib/hello_phoenix/repo.exに関してです。
中身は非常にシンプルでものの数行しかありません。

defmodule HelloPhoenix.Repo do
  use Ecto.Repo,
    otp_app: :hello_phoenix,
    adapter: Ecto.Adapters.Postgres
end

生成したプロジェクトでEctoを使うで!アダプターはPostgresで!ってぐらいの役割なのでほとんど編集することは無いと思います。
あるとするならPostgreSQL以外の選択肢をチョイスした場合にアダプターを変更するぐらいでしょうか?

あとデータベースの設定はconfig/dev.exsを編集しましたが、必要に応じてテスト用の設定config/test.exsやデプロイ用の設定config/prod.secret.exsを編集するようです。

The Schema

一番最初にスキーマの作成をしましたが、EctoのスキーマはElixirのデータ構造とデータベースのテーブルを繋ぐ(変換する)役割を果たしています。
上記の例ではlib/hello_phoenix/user.exスキーマが作成されています。

defmodule HelloPhoenix.User do
  use Ecto.Schema
  import Ecto.Changeset


  schema "users" do
    field :bio, :string
    field :email, :string
    field :name, :string
    field :number_of_pets, :integer

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio, :number_of_pets])
  end
end

どうやらスキーマで定義した型は構造体として利用できるそうです。
データのキャストからバリデーションもこのスキーマのモジュールが担ってくれるようなので、開発している際はバリデーションをいちいち書く必要は無さそうです。

一旦この辺で区切ります。
次回はChangesetsやValidationsのお話。リレーションを貼ったりする方法とかあるのかな