スポンサーリンク

2015年7月29日

[Elixir+Phoenix]Create Micropost model

Goal

Micropostモデルを作成する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.13.1
PostgreSQL: postgres (PostgreSQL) 9.4.4

Wait a minute

ようやっとユーザ以外のモデルを作成します。
今回の内容は特に難しいことはないと思います。
気楽にやっていきましょう~

Index

Create Micropost model
|> Migration of Micropost model
|> User has many Micropost, Also Micropost belongs to User
|> Delete together
|> Validation
|> Extra

Migration of Micropost model

マイクロポストのモデルを作成するため、マイグレーションします。
データモデルは以下のようになります。
モデル名: Micropost
テーブル名: microposts
作成するカラム: content(string)、user_id(integer)
自動生成されるカラム: id(integer)、inserted_at(timestamp)、updated_at(timestamp)
Caution:
今回のマイグレーションではインデックスを作成します。
参考: hexdocs - v0.14.3 Ecto.Migration.index/3
また、concurrently:オプションを利用します。
このオプションを理解するためには、PostgreSQLの資料を読む必要があります。
参考: CREATE INDEX - インデックスの同時作成
インデックスに指定するカラムは以下の二つです。
インデックス: user_id(integer)、inserted_at(timestamp)
インデックスは、こんな感じのソースコードになりますね。
create index(:microposts, [:user_id, :inserted_at], concurrently: true)
まずは、コマンドで自動生成をします。
>mix phoenix.gen.model Micropost microposts content:string user_id:integer
* creating priv/repo/migrations/timestamp_create_micropost.exs
* creating web/models/micropost.ex
* creating test/models/micropost_test.exs
ファイル: priv/repo/migrations/timestamp_create_micropost.exs
マイグレーションファイルを以下のように編集する。
defmodule SampleApp.Repo.Migrations.CreateMicropost do
  use Ecto.Migration
  @disable_ddl_transaction true

  def change do
    create table(:microposts) do
      add :content, :string
      add :user_id, :integer

      timestamps
    end

    create index(:microposts, [:user_id, :inserted_at], concurrently: true)
  end
end
マイグレーションを実行。
>mix ecto.migrate

14:20:27.485 [info]  == Running SampleApp.Repo.Migrations.CreateMicropost.change/0 forward

14:20:27.485 [info]  create table microposts

14:20:27.504 [info]  create index microposts_user_id_inserted_at_index

14:20:27.523 [info]  == Migrated in 0.3s
DBの方を確認しましょう。問題なしですね。
Description:
@disable_ddl_transaction属性は、
トランザクションの外部で実行するように強制できる属性です。
記述していないと以下のようなエラーがDBのログに書き出されます。
2015-07-29 14:06:19 JST ERROR:  CREATE INDEX CONCURRENTLYはトランザクションブロックの内側では実行できません
2015-07-29 14:06:19 JST ステートメント:  CREATE INDEX CONCURRENTLY "microposts_user_id_inserted_at_index" ON "microposts" ("user_id", "inserted_at")

User has many Micropost, Also Micropost belongs to User

ユーザは複数のマイクロポストを持ち、マイクロポストはユーザに属する設定を実施する。
作成したMicropostモデルと、既に作成しているUserモデルを紐づけます。
既に何度かご紹介している機能です。
  • Ecto.Schema.has_many/3
  • Ecto.Schema.belongs_to/3
この二つを利用します。
表示する部分を作成していないので、設定だけになりますが・・・
次の記事にて、行いますのでしばしお待ちを・・・
ファイル: web/models/user.ex
schema "users" do
  field :name, :string
  field :email, :string
  field :password_digest, :string
  field :password, :string, virtual: true

  has_many :microposts, SampleApp.Micropost

  timestamps
end
ファイル: web/models/micropost.ex
schema "microposts" do
  field :content, :string

  belongs_to :user, SampleApp.User, foreign_key: :user_id

  timestamps
end
Description:
多対1の関係性を忘れてしまった方!!
以下の記事を見ると思い出せるかも?
参考: Ectoで1対多の関係性を検証する

Delete together

ユーザが削除されたら、そのユーザのマイクロポストも削除します。
そうでなくては、ユーザの登録が消えているのに、
マイクロポストだけが残ってしまいますね。
UserControllerのDeleteアクションに処理を追加するだけです。
ファイル: web/controllers/user_controller.ex
deleteアクションを以下のように修正します。
def delete(conn, %{"id" => id}) do
  user = Repo.get(SampleApp.User, id)
  from(m in SampleApp.Micropost, where: m.user_id == ^user.id) |> Repo.delete_all
  Repo.delete(user)

  conn
  |> put_flash(:info, "User deleted successfully.")
  |> redirect(to: static_pages_path(conn, :home))
end

Validation

マイクロポストに対して、Validationを追加します。
ファイル: web/models/micropost.ex
def changeset(model, params \\ :empty) do
  model
  |> cast(params, @required_fields, @optional_fields)
  |> validate_length(:content, min: 1)
  |> validate_length(:content, max: 140)
end

Extra

多対1の簡単な動作テストをしてみます。
iex上で実行します。
Caution:
user_idは、存在しているユーザのidを指定して下さい。
Exsample:
マイクロポストのデータを作成。
iex(1)> changeset = SampleApp.Micropost.changeset(%SampleApp.Micropost{}, %{content: "hogehoge", user_id: 7})
...
iex(2)> SampleApp.Repo.insert(changeset)
...
Exsample:
紐づいたデータが取得できるか確認。
iex(3)> SampleApp.User |> SampleApp.Repo.get(7) |> SampleApp.Repo.preload [:microposts]
...
%SampleApp.User{__meta__: %Ecto.Schema.Metadata{source: {nil, "users"},
  state: :loaded}, email: "nuke@nuke.com", id: 7,
 inserted_at: #Ecto.DateTime<2015-07-23T10:27:19Z>,
 microposts: [%SampleApp.Micropost{__meta__: %Ecto.Schema.Metadata{source: {nil,
     "microposts"}, state: :loaded}, content: "hugehuge", id: 3,
   inserted_at: #Ecto.DateTime<2015-07-29T06:30:46Z>,
   updated_at: #Ecto.DateTime<2015-07-29T06:30:46Z>,
   user: #Ecto.Association.NotLoaded<association :user is not loaded>,
   user_id: 7}], name: "nuke", password: nil,
 password_digest: "S3RtK2pSN1hnSExtSzhUeVpCWjZWUT09LS1wdVZ2MmlpSEtYMjhlZHhXRkp1cUd3PT0=--37D3577AE91D1F5DFB99AD354463046F82733790",
 updated_at: #Ecto.DateTime<2015-07-23T10:27:19Z>}
ついでに、画面上からユーザを削除してみて下さい。
紐づいたマイクロポストも削除されていることが確認できると思います。

Speaking to oneself

migrationのup/downとchangeについて初めて知りましたよ。
そういう意味があったんですね~(無知)
参考: tanihiro.log -【Rails】migrationのchangeとup/downって何が違うの?
今回、Ecto.rollbackを初めて使いました。
@disable_ddl_transaction属性を忘れていまして・・・一度、戻したんですよ。
便利です。rollback機能。

Bibliography

人気の投稿