技術メモ

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

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のお話。リレーションを貼ったりする方法とかあるのかな

AWS Lambdaでrequestsなどのライブラリを使う

f:id:ysmn_deus:20190318182949p:plain

どうも、靖宗です。
たまにはAWSの話なんかも。

AWSでpipでインストールされるライブラリを使用する

よくある欲求で、なおかつウェブ上に結構ドキュメントはあると思いますが、自分のメモ用に。
基本的にAWS Lambdaでは標準ライブラリ以外は使えませんが、zipファイルに固めてアップロードすれば利用できることが知られています。

基本的にはvenv

仮想環境で開発されてる方には「当たり前やろ!」と怒られそうですが、Pythonのようにバージョンに左右される言語を利用するときは開発は極力venvなど環境をローカルとは切り離して開発するのが未然にトラブルを防ぐ手段です。
venvで必要なライブラリだけインポートして、それらをzipに固めます。venvは導入済みとし、今回はrequestsモジュールを使う関数を作成します。

(venv) project_name>pip install requests

とりあえず動かす

Lambdaで実行したいファイルを作成します。一応ここではindex.pyとしておきます。

import re
import requests


INFO_URL = "https://www.python.org/"


def lambda_handler(event, context):
    response = requests.get(INFO_URL)
    math_ob = re.search(r'<div class=\"shrubbery\">([\s\S]*?)</div>', response.text)
    if math_ob:
        return {
            'statusCode': response.status_code,
            'body': math_ob.group(1)
        }
    else:
        return {
            'statusCode': 500,
            'body': ""
        }

引数などにかかわらずpythonの公式サイトのニュースを取得してくるような関数になってます。
一応動作確認しておきます。同フォルダでPythonを仮想環境で実行。

(venv) >python
Python 3.7.2 ~~~
Type "help", "copyright", "credits" or "license" for more information.
>>> from index import *
>>> lambda_handler(None, None)
{'statusCode': 200, 'body':
...
>>>

正常に動作すればとりあえずヨシ。(ホントはテストとかを入れときたいですが、とりあえずこれで行きます。)

必要ファイルをローカルにインストール

します。まずはrequirements.txtを作成しておきます。

(venv) >pip freeze > requirements.txt

requirements.txtからローカルにライブラリをインストールします。

(venv) >pip install -r requirements.txt -t .

これを実行するとローカルに色々ファイルが出てくると思います。
ここまで来れば、後はzipに固めてLambdaにアップロードするだけです。MacLinuxではzipコマンドがあるかと思いますが、Windowsではないのでなにがしかの方法で圧縮して下さい。
なお、venvディレクトリは不要です。(サイズが大きいので間違えないように)
本来であればライブラリ名-バージョン-dist-infoみたいなディレクトリも要らないと思いますが、そこまでファイルサイズ大きくも無いですし、手間なら一緒に圧縮しても良いと思います。

Lambdaにアップロード

zipに固めたファイルをアップロードします。既存のLamndaか、新しく作ったLambdaの「関数コード」という所からアップロードします。

f:id:ysmn_deus:20190319000545p:plain

ランタイムをPythonにして置いて下さい(バージョンは適宜変更)。
また、上記のコードを利用していればハンドラの名前が異なっているので、ハンドラの名前も合わせて下さい。 (index,pyならindex..lambda_handler

適当なテストイベントを作成してテストを実行すれば、実行結果が表示されます。
きちんとライブラリが動作していれば実行結果は成功になり、ログに取得した情報が表示されるかと思います。

インストールされたライブラリをpipで-t .オプションを付けてインストールし直すことで、importなどの依存関係をそのまま引き継ぐことができます。

Phoenix入門 (番外編 LiveView その1 `mix phx.new`からThermostatが動くまで)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。 なにやらLiveViewなる機能がちらほらツイッターで見かけまして、一応触っておこうと思いました。
たぶん未履修分野には被らないはず・・・

下記サイトを参考にしております。

dockyard.com

github.com

LiveView

名前からも感じますが、どうやらLiveViewというのはリアルタイムの双方向性のあるレンダリング機能のようです。via WebSocketsとあるのでWebSocketでうまいこと実装されているんでしょう。
JavaScriptでフロント+バックエンド(Phoenix)やと複雑やしメンテナンスたいへんやろ?というモチベーションのようです。

主な機能としては

  • リアルタイムで値をアップデートする
  • クライアント側でのデータのバリデーション(バックエンドでの評価が要らない)
  • 入力のオートコンプリート
  • リアルタイム性のあるゲーム

JavaScript無しでできるそうです。まじか。

Programming Model

書き方は大まかにはController+Viewと通常のHTMLレンダリングに近いようです。
おそらく今までレンダリングしていたテンプレートがモジュールと化し、そのモジュールで定義する関数が様々なイベントのコールバック関数になるイメージでしょう。

プロジェクト作成

なにはともあれ動かしてみます。新しいプロジェクトでやってみましょう。
新しいプロジェクト作る練習にもなります。名前はhello_liveviewにしました。

PS > mix phx.new hello_liveview

hello_liveview/config/dev.exsを編集します。

...
# Configure your database
config :hello_liveview, HelloLiveview.Repo,
  username: "elixir",
  password: "elixir",
  database: "hello_liveview_dev",
  hostname: "localhost",
  pool_size: 10

データベースのユーザー情報などは適宜合わせて下さい。
再起動してコンテナが落ちてたので起動。

PS > cd hello_liveview
PS \hello_liveview> docker start ph_psql
ph_psql

mix ecto.createでデータ作成。

PS \hello_liveview> mix ecto.create
The database for HelloLiveview.Repo has been created

これで起動まではいく筈。とりあえず起動。

PS \hello_liveview> mix phx.server
...

問題ナシ。
参考にしたブログでもPageControllerを編集してるのでそれに則る。

LiveViewの機能を有効にする

Phoenix側の依存関係を追記する

LiveViewは標準機能ではなくオプションとして提供されています。
ですので依存関係をmix.exsに追記してmix deps.getしてやる必要があります。

...
  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
...
      {:plug_cowboy, "~> 2.0"},
      {:phoenix_live_view, github: "phoenixframework/phoenix_live_view"} #コレを追記
    ]
  end
...

mix deps.getする。

PS \hello_liveview> mix deps.get
* Getting phoenix_live_view (https://github.com/phoenixframework/phoenix_live_view.git)
...

ソケット通信の設定

もうちょっと設定が必要なよう。エンドポイントにsigning_saltなるものが必要だそうで。mix phx.gen.secret 32を実行して得られたお塩(salt)をconfig.exsのエンドポイントの設定に追記してやります。

PS \hello_liveview> mix phx.gen.secret 32
SECRET_SALT(なんか変な文字列がでます)

config.exsのエンドポイントの設定に追記します。

...
# Configures the endpoint
config :hello_liveview, HelloLiveviewWeb.Endpoint,
...
  pubsub: [name: HelloLiveview.PubSub, adapter: Phoenix.PubSub.PG2], # ここのカンマつけ忘れてエラーでてた・・・
  live_view: [
    signing_salt: SECRET_SALT(さっき出力した文字列)
  ]
...

config :hello_liveview,句の一番最後に追記しました。

テンプレートファイルを有効にする

ついでにLiveViewのテンプレートは.leexになるそうなので、この設定も同じconfig.exsに書いときます。

...
config :phoenix,
  template_engines: [leex: Phoenix.LiveView.Engine]

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

router.exのパイプラインに設定を追加する

お次にパイプラインに設定を追加しておきます。lib/hello_liveview_web/router.ex:browserパイプラインを編集します。

defmodule HelloLiveviewWeb.Router do
  use HelloLiveviewWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug Phoenix.LiveView.Flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug :put_layout, {HelloLiveviewWeb.LayoutView, :app}
  end
...

レイアウトに当てはめるplugを明示的に記載していますが、後述するコントローラからLiveViewを呼び出す場合は要らないと思います。

hello_liveview_web.exのviewとrouterでインポートの設定を追記

次にlib/hello_liveview_web.exのviewとrouterの箇所も編集します。

def view do
  quote do
    ...
    import Phoenix.LiveView, only: [live_render: 2, live_render: 3]
  end
end

def router do
  quote do
    ...
    import Phoenix.LiveView.Router
  end
end

LiveViewのレンダリング関数とルーティング機能のインポートを追記しました。
これは全てのViewでインポートされてしまうのでいちいちLiveViewの方で読み込んでも良いのかもしれませんが、LiveView多めのアプリケーションならこの手法で良いと思います。

Endpointの設定

まだ設定は続きます。(かなり手間なのでそのうちこの辺はmix phx.newのオプションとかになるんじゃないでしょうか?)
lib/hello_liveview_web/endpoint.exにLiveViewに使用されるソケット通信を追記します。

defmodule HelloLiveviewWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello_liveview

  socket "/live", Phoenix.LiveView.Socket,
    websocket: true
...

PhoenixGitHubではotp_app: :hello_liveviewは無いですが、Phoenixのデフォルトなので一応書いておきます。

LiveViewを入れとくフォルダの作成+ホットリロードの設定

LiveViewを入れておくフォルダlib/hello_liveview_web/liveを作成しておき、ホットリロードを有効にするためにconfig/dev.exsに追記しておきます。

...
# Watch static and templates for browser reloading.
config :hello_liveview, HelloLiveviewWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/hello_liveview_web/views/.*(ex)$},
      ~r{lib/hello_liveview_web/templates/.*(eex)$},
      ~r{lib/hello_liveview_web/live/.*(ex)$} #これ
    ]
  ]
...

JavaScript側の依存関係を追記する

以上でおそらくPhoenix自体の初期設定は終わりました。仕上げにレンダリングされるJavaScript周りに手を加えます。
バックで動いているJSの依存関係を編集します。assets/package.jsonを編集します。

...
  "dependencies": {
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html",
    "phoenix_live_view": "file:../deps/phoenix_live_view"
  },
...

上記依存関係を追記したらnpm installしておいて下さい。単純な事ですが僕はコレでハマりました(笑)

PS \hello_liveview> cd .\assets\
PS \hello_liveview\assets> npm isntall

ソケット通信開始の箇所を追記する

レンダリングされるJavaScript側からPhoenixへの通信を開始する箇所を追記します。
assets/js/app.jsの最後に下記を追記します。

import LiveSocket from "phoenix_live_view"

let liveSocket = new LiveSocket("/live")
liveSocket.connect()

これで初期設定は大体OKなはずです。

ThermostatViewの実装

試しにサンプルのThermostatViewを実装してみます。

追加の依存関係

ThermostatViewはcalendarというライブラリを使用しています。mix.exs:calendarの依存関係を追記しておきます。

...
  defp deps do
    [
      {:phoenix, "~> 1.4.1"},
      {:phoenix_pubsub, "~> 1.1"},
      {:phoenix_ecto, "~> 4.0"},
      {:ecto_sql, "~> 3.0"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:phoenix_live_view, github: "phoenixframework/phoenix_live_view"},
      {:calendar, "~> 0.17.4"}
    ]
  end
...

mix deps.getをお忘れ無きよう。(忘れててもmixが教えてくれますが。)

CSSの追加

ThermostatのCSSを追加します。この編は素直にassets/cssに保存しておきます。
ただ、レイアウトが違うのかちょっと位置が微妙なのでassets/css/thermostat.cssのmargin-topだけいじります。

...
.thermostat {
...
  margin-top: 250px;
}
...

assets/css/app.cssでインポートしておきます。

@import "./phoenix.css";
@import "./thermostat.css";

LiveViewのルーティング方法は2種類

LiveViewのルーティング方法は2種類あります。
1個目はrouter.exからコントローラを介さずに直接レンダリングする方法で、liveマクロを利用します。

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

    get "/", PageController, :index
    live "/thermostat", ThermostatView
  end
...

2個目は従来通りコントローラを介してレンダリングします。

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

    get "/", PageController, :index
    get "/thermostat", PageController, :thermo
  end
...

たぶんコントローラを介してレンダリングする方が柔軟性があると思いますのでそちらで実装していきたいと思います。
故に:brouserパイプラインのplug :put_layout, {HelloLiveviewWeb.LayoutView, :app}は不要ですので消していただいても問題無いかと思います。

ルーティング設定

上記に従ってlib/hello_liveview_web/router.exを編集します。

defmodule HelloLiveviewWeb.Router do
  use HelloLiveviewWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug Phoenix.LiveView.Flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", HelloLiveviewWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/thermostat", PageController, :thermo
  end
end

コントローラの設定

めんどくさいのでlib/hello_liveview_web//controllers/page_controller.exを編集して使います。

defmodule HelloLiveviewWeb.PageController do
  use HelloLiveviewWeb, :controller
  alias Phoenix.LiveView

  def index(conn, _params) do
    render(conn, "index.html")
  end

  def thermo(conn, _params) do
    LiveView.Controller.live_render(conn, HelloLiveviewWeb.ThermostatView, session: %{})
  end
end

live_renderlive_render/2もインポートしてるので最後のマップは要らなさそうな気もするんですがなんか渡さないと怒られたのでlive_render/3を利用しています。
ちょっとこの辺は後ほど再確認したいところです・・・

index/2thermo/2を比較すると、関数名こそ違えどほぼ同じ様な構造になっているので抵抗感無く使えるかと思います。

LiveViewの追加

lib/hello_liveview_web/live/thermostat_live.exを作成し、LiveViewを作って行きます。

defmodule HelloLiveviewWeb.ThermostatView do
    use Phoenix.LiveView
    import Calendar.Strftime
  
    def render(assigns) do
      ~L"""
      <div class="thermostat">
  
        <div class="bar <%= @mode %>">
          <a phx-click="toggle-mode"><%= @mode %></a>
          <span><%= strftime!(@time, "%r") %></span>
        </div>
        <div class="controls">
          <span class="reading"><%= @val %></span>
          <button phx-click="dec" class="minus">-</button>
          <button phx-click="inc" class="plus">+</button>
        </div>
      </div>
      """
    end
  
    def mount(_session, socket) do
      if connected?(socket), do: Process.send_after(self(), :tick, 1000)
      {:ok, assign(socket, val: 72, mode: :cooling, time: :calendar.local_time())}
    end
  
    def handle_info(:tick, socket) do
      Process.send_after(self(), :tick, 1000)
      {:noreply, assign(socket, time: :calendar.local_time())}
    end
  
    def handle_event("inc", _, socket) do
      {:noreply, update(socket, :val, &(&1 + 1))}
    end
  
    def handle_event("dec", _, socket) do
      {:noreply, update(socket, :val, &(&1 - 1))}
    end
  
    def handle_event("toggle-mode", _, socket) do
      {:noreply,
       update(socket, :mode, fn
         :cooling -> :heating
         :heating -> :cooling
       end)}
    end  
  end

render/1の所は通常のViewと同様にテンプレートを置いておけば書かなくても良いのかもしれませんが、とりあえずサンプル通りにしておきます。
上記の挙動から鑑みるに、下記のタイミングで再レンダリングがかかっています。

  • メッセージを受けた時
  • update/3を使用した時

厳密にはソケットのステートが変更された際(Phoenix側でsocketに値がアサインされた時)に再レンダリングするようです。

Thermostatの例では、マウント時に1秒後に:tickというメッセージを自分宛に発行しています。これを利用して:tickというメッセージを受けた際にhandle_info/2が呼び出される(このへんはGenServerを参照してください)ので、そのコールバック関数の中でソケットに値をアサインしています。(コレを1秒毎に繰り返す)

クライアントサイドではphx-click="hoge"というディレクティブを要素に追加するとクリック時にイベントが発生し、handle_event/3で受けれるといった仕組みのようです。
それぞれ呼び出されるイベント毎にソケットに値をアサインしてレンダリングを実行しているようです。

所感

実際にやってることとしては他のフレームワークとそう大差ない(通常のフレームワークであればおそらくソケット通信よりも非同期通信で処理するのが一般的だとは思いますが)ですが、裏を返せば他のフレームワークを学習しなくてもデータバインディングコンポーネントの再レンダリングができるので、ラピッドプロトタイピングがかなり捗りそうです。
既存の資産に縁が無い(初心者やバックエンドしかやったことのない人)なら、これはかなり有り難いんじゃ無いでしょうか?僕は有り難いです!

でももうちょっとすんなり入るようになってほしい・・・(´゚'ω゚`)