Phoenix で Blog を超簡単に作る

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 を考慮して修正する**

【関連過去記事】
* [Phoenix 認証システム - mix phx.gen.auth](https://qiita.com/sand/items/dda77cc3da3febf3c286)
* [Phoenix1.6の基本的な仕組み - phx.gen.html](https://qiita.com/sand/items/f42dc35c47589eab687e)
* [Elixir Ecto Association - Oiita](https://qiita.com/sand/items/5581497972473e308f05)


# 1. 認証システム (usersテーブル)

まずプロジェクトの作成です。
```
mix phx.new blog
cd blog
```

認証システムを生成します。
```
mix phx.gen.auth Accounts User users
```

生成された設定などを反映させます。
```
mix deps.get
mix ecto.setup
```


::: note warn
PostgreSQL のポートが正しくないと、mix ecto.setup でエラーになります。私の Windows の環境ではデフォルトの 5432 ではなく 5433 だったので以下のように変更する必要がありました。
```elixir:config\dev.exs
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** に以下の一行を追加します。これで認証されたユーザのみが投稿システムのリソースにアクセスできるようになります。
```elixir:lib/blog_web/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** テーブルを参照する外部キーとして追加するように変更します。

```elixir:priv/repo/migrations/20221024010750_post_belongs_to_user.exs
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 テーブル**にアクセスできるようになります。

```elixir:lib/blog/posts/post.ex
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 テーブル**へアクセスできるようになります。

```elixir:lib/blog/accounts/user.ex
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** を追加します。

```elixir:lib/blog/posts/post.ex
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** も指定するようにします。

```elixir:lib/blog_web/controllers/post_controller.ex
  ---
  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のみを取得するように変更します。

```elixir:lib/blog_web/controllers/post_controller.ex 
  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** を追加します


```elixir:lib/blog/posts.ex 
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
  ---
```

::: note info
**プリロード**
ここでは **user.posts** へのアクセスを可能とするためプリロードを行っています。
**belongs_to 、 has_many** 、そして **has_one** マクロからの公開する関連レコードにアクセスできるようにするために、関連したスキーマを プリロード する必要があります。プリロードしない限り、それらの関連したデータにアクセスすることはできません。ここではフェッチされたレコードのプリロードを行っています。
[Ecto Query](https://elixirschool.com/ja/lessons/ecto/querying_basics)
:::


::: note warn
Ecto Query
list_posts(user)の実装は少し真面目に考えれば、以下の方が好ましいかもしれない。

```elixir:
  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. 動作確認

動作を見る前に、予習的に認証システムのメニューのコードを見ておきます。
```elixir: lib/blog_web/templates/layout/_user_menu.html.heex
<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のトップ画面ですが、赤い枠のところに認証システムのメニューが表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/5da23bfa-be94-9158-94a9-5890e6888349.png)

http:localhost:4000/posts にアクセスするとログイン画面にリダイレクトされる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/2e496065-f08d-79c4-df1a-4dc89c5b0a1f.png)


ユーザ登録画面です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/c3005306-a24b-3d34-f5ce-3a936228de41.png)

ログイン画面です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/9eb5bdcf-6ea5-6e71-82d9-d8b24c342aed.png)

ログインすると認証システムのメニューが変わります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/f15e504f-9daf-888f-6118-1098507a769b.png)


http://localhost:4000/posts にアクセスすると、認証されたユーザの画面になります。ちなみにログインしていない状態でアクセスすると、トップページにリダイレクトされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/e074bc6d-366c-c613-a56c-5a164955d6ca.png)

投稿ページです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/3c63701b-91fc-5e4d-9120-9200c57a4baa.png)

別ユーザでログインし直すと、このユーザのPost一覧が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/8d40d021-3e02-2bf3-fbd5-dd4bd743eae0.png)


今回は以上です