Elixir + Phonenix でWebアプリ作る時の基本まとめ:モデル
Elixir + Phonenix でWebアプリ作る時の基本まとめ
モデルを単体で生成する
前回は $ 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
のようなファイルができているので、これを見てみると...
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
にある。
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