🥾

Elixir + Phonenix でWebアプリ作る時の基本まとめ:モデル

2021/06/24に公開

Elixir + Phonenix でWebアプリ作る時の基本まとめ

  1. 環境構築
  2. データベースの準備とWebアプリの起動
  3. scaffoldぽいやつで最初のコントローラ、モデル、ビューを作る
  4. モデル

モデルを単体で生成する

前回は $ mix phx.gen.html で、ちょうどRailsのScaffoldのようにモデル、コントローラ、ビューを一度に作ったけど、実際の開発現場ではそれぞれ単体で作る場合も多いと思う。
今回はモデルを作ってみる。

モデル、つまりデータベースへの接続周りはEctoてやつのお仕事範囲になるらしい。
以下のようなコマンドを実行する。

$ mix phx.gen.schema City cities prefectures_code:string city_code:string name:string population:integer

priv/repo/migrations/20210624132057_create_cities.exsのようなファイルができているので、これを見てみると...

priv/repo/migrations/20210624132057_create_cities.exs
defmodule Kuroneko.Repo.Migrations.CreateCities do
  use Ecto.Migration

  def change do
    create table(:cities) do
      add :prefectures_code, :string
      add :city_code, :string
      add :name, :string
      add :population, :integer

      timestamps()
    end

  end
end

燦々と輝く create table(:cities)が。早速マイグレーションしよう。

$ mix ecto.migrate

なにもエラーが起きていなければテーブルが作成されている。

モデルのコードを見てみる

無事テーブルは作られたようなので、モデルのコードを見てみよう。
対象は lib/kuroneko/city.ex にある。

lib/kuroneko/city.ex
defmodule Kuroneko.City do
  use Ecto.Schema
  import Ecto.Changeset

  schema "cities" do
    field :city_code, :string
    field :name, :string
    field :population, :integer
    field :prefectures_code, :string

    timestamps()
  end

  @doc false
  def changeset(city, attrs) do
    city
    |> cast(attrs, [:prefectures_code, :city_code, :name, :population])
    |> validate_required([:prefectures_code, :city_code, :name, :population])
  end
end

対話モードを使ってモデルで遊んでみる

elixirにはiexというコマンドがあり、対話モードでコードを実行できる。Rubyのirbみたいなもん。

$ iex -S mix

空のチェンジセットを作る

> alias Kuroneko.City

> changeset = City.changeset(%City{}, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    prefectures_code: {"can't be blank", [validation: :required]},
    city_code: {"can't be blank", [validation: :required]},
    name: {"can't be blank", [validation: :required]},
    population: {"can't be blank", [validation: :required]}
  ],
  data: #Kuroneko.City<>,
  valid?: false
>

空のチェンジセットができたようだが、中身がないのでエラーだと言っている。

> changeset.errors
[
  prefectures_code: {"can't be blank", [validation: :required]},
  city_code: {"can't be blank", [validation: :required]},
  name: {"can't be blank", [validation: :required]},
  population: {"can't be blank", [validation: :required]}
]

こうすると、明確に「ここがダメだよ」って教えてくれる。

値を設定してチェンジセットを作る

改めて、きちんとパラメータを設定してチェンジセットを作ってみる。

> changeset = City.changeset(%City{}, %{prefectures_code: "32", city_code: "32201", name: "松江市", population: 206200})
#Ecto.Changeset<
  action: nil,
  changes: %{
    city_code: "32201",
    name: "松江市",
    population: 206200,
    prefectures_code: "32"
  },
  errors: [],
  data: #Kuroneko.City<>,
  valid?: true
>

問題ないか確認したらtrueが帰ってきたので問題ないようだ。

> changeset.valid?
=> true

レコードの保存

では、これを保存してみよう。

> alias Kuroneko.Repo
> Repo.insert(changeset)

結果は以下のように表示された。

{:ok,
 %Kuroneko.City{
   __meta__: #Ecto.Schema.Metadata<:loaded, "cities">,
   city_code: "32201",
   id: 1,
   inserted_at: ~N[2021-06-24 14:00:52],
   name: "松江市",
   population: 206200,
   prefectures_code: "32",
   updated_at: ~N[2021-06-24 14:00:52]
 }}

どうやら問題なく保存されたようだ。

レコードの取得

全てのレコードを取得するにはRepo.all/1を使う。

Repo.all(City)

[
  %Kuroneko.City{
    __meta__: #Ecto.Schema.Metadata<:loaded, "cities">,
    city_code: "32201",
    id: 1,
    inserted_at: ~N[2021-06-24 14:00:52],
    name: "松江市",
    population: 206200,
    prefectures_code: "32",
    updated_at: ~N[2021-06-24 14:00:52]
  }
]

フィールドを指定して取得。

import Ecto.Query
Repo.all(from city in City, select: [city.id, city.name])

[[1, "松江市"]]

ここでもう一つレコードを追加してみよう。

changeset = City.changeset(%City{}, %{prefectures_code: "31", city_code: "312011", name: "鳥取市", population: 193700})
Repo.insert(changeset)

where句で条件指定して取得する。

Repo.all(from city in City, select: [city.id, city.name], where: [prefectures_code: "32"])         

[[1, "松江市"]]

where句で大小の比較

Repo.all(from city in City, select: [city.id, city.name], where: city.id < 2)             

[[1, "松江市"]]

like条件

Repo.all(from city in City, select: [city.id, city.name], where: ilike(city.name, "松%"))                

[[1, "松江市"]]

パイプを使ったクエリ

City \                                     
|> where([city], [prefectures_code: "31"]) \
|> select([city], {city.name}) \            
|> Repo.all                                 

[{"鳥取市"}]

パイプを使って複数条件でのAND検索

City \                                      
|> where([city], [prefectures_code: "31"]) \
|> where([city], [city_code: "312011"]) \   
|> where([city], city.population > 100000) \
|> select([city], {city.name}) \            
|> Repo.all                                 

[{"鳥取市"}]

条件の人口だけを変えてみよう。

City \                                      
|> where([city], [prefectures_code: "31"]) \
|> where([city], [city_code: "312011"]) \   
|> where([city], city.population > 200000) \
|> select([city], {city.name}) \            
|> Repo.all                                 

[]

(T-T)

OR検索

 City \                                         
|> where([city], [prefectures_code: "31"]) \   
|> or_where([city], [prefectures_code: "32"]) \
|> select([city], {city.name}) \               
|> Repo.all                                    

[{"松江市"}, {"鳥取市"}]

ソート

City \                                      
|> order_by([city], [asc: city.population]) \
|> select([city], {city.name}) \             
|> Repo.all                                  

[{"鳥取市"}, {"松江市"}]

便利は便利なんだけど、こういうの書いてるときに「もうSQLでいいんじゃないか」という気持ちにたまになる。

本日はここまで。お疲れ様でした。

Discussion