Phoenix の認証システム (phx.gen.auth) を使って、マルチユーザ版の Blog システムを、超簡単に作ってみるというのが本記事の趣旨です。ブログ投稿も phx.gen.html を使って一発で作成します。それら2つのテーブルを Ecto Association を使って one-to-many で結合します。ちなみに素朴なアイディアを実現するために、手早くザックリ作るのが趣旨ですので、細部は荒いままです。投稿と一覧表示のみ対応します。
- 1. phx.gen.auth で認証システムを生成する (usersテーブル)
- 2. phx.gen.html でブログ投稿システムを生成する (postsテーブル)
- 3. Ecto Association で postsテーブルにuser_id の外部キーを設定する
- 4. 生成されたコードを、Ecto Association を考慮して修正する
【関連過去記事】
1. 認証システム (usersテーブル)
まずプロジェクトの作成です。
mix phx.new blog
cd blog
認証システムを生成します。
mix phx.gen.auth Accounts User users
生成された設定などを反映させます。
mix deps.get
mix ecto.setup
PostgreSQL のポートが正しくないと、mix ecto.setup でエラーになります。私の Windows の環境ではデフォルトの 5432 ではなく 5433 だったので以下のように変更する必要がありました。
config :liveview_people, LiveviewPeople.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "liveview_people_dev",
port: 5433,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
2. 投稿システム (postsテーブル)
投稿システムを生成します。
mix phx.gen.html Posts Post posts title:string story:text
投稿システムを認証システムに組み込むために、router.ex に以下の一行を追加します。これで認証されたユーザのみが投稿システムのリソースにアクセスできるようになります。
scope "/", BlogWeb do
pipe_through [:browser, :require_authenticated_user]
---
resources "/posts", PostController # 追加
end
テーブルを作成します。
mix ecto.migrate
3. Ecto Association (one-to-many)
usersテーブルとpostsテーブルとの Associations を追加します。
postsテーブルを変更する migration を作成します。
mix ecto.gen.migration post_belongs_to_user
どのように変更するかを記述します。ここでは user_id を users テーブルを参照する外部キーとして追加するように変更します。
defmodule Blog.Repo.Migrations.PostBelongsToUser do
use Ecto.Migration
def change do
alter table(:posts) do
add :user_id, references(:users)
end
end
end
Post schema を修正(追加)します。ここでは belongs_to field を追加します。これにより post.user で users テーブルにアクセスできるようになります。
defmodule Blog.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :story, :string
field :title, :string
belongs_to :user, Blog.Accounts.User # 追加
timestamps()
end
---
end
User schema を修正(追加)します。これによる usersテーブルの変更はありませんが、user.posts によって posts テーブルへアクセスできるようになります。
defmodule Blog.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
has_many :posts, Blog.Posts.Post # 追加
timestamps()
end
---
migrate を実行して、実際にテーブルを変更します。
mix ecto.migrate
4.生成コードの修正
Ecto Association で、postsテーブルに外部キーの user_id を追加したことにより必要となる生成コードの修正を行います。今回はPost投稿とPost一覧表示に関する部分のみ修正します。
Post Schema の changeset の各行に user_id を追加します。
defmodule Blog.Posts.Post do
---
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :story, :user_id]) # user_id を追加
|> validate_required([:title, :story, :user_id]) # user_id を追加
end
end
post_controller.ex の create() 関数に以下の2行を加えます。Postを作成するときに外部キーの user_id も指定するようにします。
---
def create(conn, %{"post" => post_params}) do
user = conn.assigns.current_user # 追加
post_params = Map.put(post_params, "user_id", user.id) # 追加
case Posts.create_post(post_params) do
{:ok, post} ->
conn
|> put_flash(:info, "Post created successfully.")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
---
post_controller.ex の index() 関数を修正します。Post一覧を取得するときにログインユーザのPostのみを取得するように変更します。
def index(conn, _params) do
user = conn.assigns.current_user # 追加
posts = Posts.list_posts(user) # 修正
render(conn, "index.html", posts: posts)
end
posts.ex に list_posts/1 を追加します
defmodule Blog.Posts do
---
alias Blog.Accounts.User # 追加
---
def list_posts(user) do # 追加
user = Repo.get(User, user.id) |> Repo.preload(:posts)
user.posts
end
---
プリロード
ここでは user.posts へのアクセスを可能とするためプリロードを行っています。
belongs_to 、 has_many 、そして has_one マクロからの公開する関連レコードにアクセスできるようにするために、関連したスキーマを プリロード する必要があります。プリロードしない限り、それらの関連したデータにアクセスすることはできません。ここではフェッチされたレコードのプリロードを行っています。
Ecto Query
Ecto Query
list_posts(user)の実装は少し真面目に考えれば、以下の方が好ましいかもしれない。
def list_posts(user) do # 追加
query = from p in "posts",
order_by: [desc: p.inserted_at],
where: p.user_id == ^user.id,
select: %Post{:id => p.id, :title => p.title, :story => p.story}
Repo.all(query)
end
投稿の編集や削除なども同様に行えると想像しますが、今回の「超簡単に作る」という趣旨に反するのでこれ以上突っ込みません。
5. 動作確認
動作を見る前に、予習的に認証システムのメニューのコードを見ておきます。
<ul>
<%= if @current_user do %>
<li><%= @current_user.email %></li>
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
<li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
<% else %>
<li><%= link "Register", to: Routes.user_registration_path(@conn, :new) %></li>
<li><%= link "Log in", to: Routes.user_session_path(@conn, :new) %></li>
<% end %>
</ul>
http:localhost:4000 にアクセスすると通常のPhoenixのトップ画面ですが、赤い枠のところに認証システムのメニューが表示されます
http:localhost:4000/posts にアクセスするとログイン画面にリダイレクトされる
http://localhost:4000/posts にアクセスすると、認証されたユーザの画面になります。ちなみにログインしていない状態でアクセスすると、トップページにリダイレクトされます。
別ユーザでログインし直すと、このユーザのPost一覧が表示されます。
今回は以上です