Phoenixで論理削除①：phx.gen.XXXXでの自動生成CRUDのdeleteをupdateに変更

[Elixir Digitalization Implementors／fukuoka.ex／kokura.ex](https://fukuokaex.connpass.com/)のpiacereです
ご覧いただいて、ありがとうございます :bow:

Phoenixのphx.gen.html／phx.gen.jsonで生成されるDBアクセッサにて「論理削除」、つまり、deleteでは無く、updateにより論理削除を立てる方法が、どこにも解説されていなくて、でも実務では頻出するコードなので、コラム化してみました

ちなみにLiveViewでも、本コラムと全く同じ方法で対応が可能です（LiveViewのSSRとの互換性対応が素晴らしいです）

内容が、面白かったり、役に立ったら、「LGTM」よろしくお願いします :wink:

<h3>:ocean::ocean::ocean: Advent Calendar、fukuoka.ex1位、Elixir2位達成ヽ(=´▽`=)ﾉ :ocean::ocean::ocean:</h3>

fukuoka.ex Advent Calendar、Webテクノロジーカテゴリで堂々1位 … 各コラムぜひお読みください
https://qiita.com/advent-calendar/2020/fukuokaex
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8a22a81f-2917-6072-10bd-525e0fc24b75.png)](https://qiita.com/advent-calendar/2020/neosvr_reso)

そして、プログラミング言語カテゴリは、1位がRust、2位がElixir、3位がGoとモダン言語揃い踏みでのトップ3、熱いネー:laughing:
https://qiita.com/advent-calendar/2020/elixir
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ea185203-c525-c778-dc10-6d2ecc391616.png)](https://qiita.com/advent-calendar/2020/elixir)

# 本コラムの検証環境

本コラムは、以下環境で検証しています（Windowsで実施していますが、Linuxやmacでも動作する想定です）

- Windows 10
- Elixir 1.11.3 　 　　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99elixir%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>
- Phoenix 1.5.7　　　　※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacere_ex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>
- PostgreSQL 10.1 　　 ※<b>[最新版のインストール手順はコチラ](https://qiita.com/piacere_ex/items/a7558adc6856e3577dc6#postgresql%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)</b>

# ステップ１：DBアクセッサのdeleteを論理削除updateに改修

phx.gen.html／phx.gen.jsonで生成されるDBアクセッサは、下記ファイルとして生成されます

``lib/【PJ名】/【コンテキスト名】.ex``

コンテキスト名とは、phx.gen.XXXXの第1引数で指定される、スキーマ定義モジュールを内蔵するフォルダ名、およびRepo呼出モジュール名のことです

たとえば、basicというPhoenix PJで、phx.gen.html（もしくはphx.gen.json）によりpostsというテーブルを下記コマンドで作るとします（ここでは、DB作成／マイグレート／ルーティング追加は割愛）

```
> mix phx.gen.html Posts Post posts title:string description:text deleted_at:datetime
```

この場合、DBアクセッサは、下記の通り、生成されます

```elixir:lib/basic/posts.ex
defmodule Basic.Posts do
  @moduledoc """
  The Posts context.
  """
…
  def update_post(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)
    |> Repo.update()
  end
…
  def delete_post(%Post{} = post) do
    Repo.delete(post)
  end
…
```

これを、Repo.deleteから、論理削除日時（deleted_at）のRepo.updateに変更します

なお、秒より下の桁（millisecond、microsecond）が入っていると、エラーになるので、NaiveDateTime.truncateで削っておきます

```elixir:lib/basic/posts.ex
…
  def delete_post(%Post{} = post) do
#    Repo.delete(post)  # comment-out here
    %Ecto.Changeset
    { 
      data: post, 
      changes: %{ deleted_at: NaiveDateTime.utc_now() |> NaiveDateTime.truncate( :second ) }, 
      valid?: true
    }
    |> Repo.update()
  end
…
```

update_postで行っている、``Post.changeset(attrs)`` の結果と同じデータ構造を自己生成して渡す点がポイントです

なお、論理削除の解除は、下記のように作ります（呼び出し側は、ここでは割愛）

```elixir:lib/basic/posts.ex
…
  def restore_delete_post(%Post{} = post) do
    %Ecto.Changeset
    { 
      data: post, 
      changes: %{ deleted_at: nil }, 
      valid?: true
    }
    |> Repo.update()
  end
…
```

# ステップ２：UIの改修（phx.gen.html時のみ）

deleted_atは、通常のデータ追加／編集の際には不要なので、UI上から削除します

```erb:lib/basic_web/templates/post/form.html.ex
<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>
…
  <% # comment-out start %>
  <%= # label f, :deleted_at %>
  <%= # datetime_select f, :deleted_at %>
  <%= # error_tag f, :deleted_at %>
  <% # comment-out end %>
…
```

# ステップ３：changesetの改修

changesetによる入力時バリデーションチェックにて、validate_requiredの必須チェックがdeleted_atにもかかっていますが、不要なので、これを解除します

```elixir:lib/basic/posts/post.ex
defmodule Basic.Posts.Post do
  use Ecto.Schema
  import Ecto.Changeset
…
  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :description, :deleted_at])
    |> validate_required([:title, :description])  # comment-out here  # , :deleted_at])
  end
…
```

# ステップ４：動作確認

``http://localhost:4000/posts`` にアクセスして、データを投入し、Deleteリンクをクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/6c0d0217-8a48-7349-ce74-99bc3b989c0e.png)

deleted_atに現在時刻（UTC）が入り、論理削除済みとなりました（なお、現状の削除UIでは、一度論理削除すると、論理削除状態を解除して、元に戻すことができません）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a8847431-db7e-8386-73d8-c81daf82ccaa.png)

なお、論理削除済みのデータをEditしても、deleted_atは更新されなくなっているため、論理削除状態は維持され、Edit側の改修は不要です（ただし、この後、解説する注意点はあるかもなので、引き続きご覧ください）

# 論理削除を設計・運用する際の注意点

論理削除の運用は、論理削除済みデータに纏わる下記ポイントで地味にハマる方も多いカテゴリなので、実務の案件で実装する際には、ご注意ください

- 論理削除済みデータと同じ内容のデータ投入時、重複チェックを解除するのをお忘れなく
- 論理削除済みなのに、機能が生き残っているケースを生まないように
  - 論理削除対象だけで無く、リレーション元の非表示化／無効化／従属削除も考慮すること
- 一覧表示／個別表示は、アクセス権限でモードが変わる
  - 対ユーザ向けには見えないようにし、対管理者向けには見えるモードも用意する
  - Ectoレベルで消すか？表示上のみ消すか？

# 最後に

今回は、Phoenixのphx.gen.～で生成されるCRUDで「論理削除」を実装してみました

Elixir開発の実務でも良く出てくるテクニックなので、覚えておきましょう

また論理削除だけに限らず、Ecto.Repoにおけるupdate自体のカスタマイズや、独自タイミングでのupdate実施にも応用できるテクニックなので、Phoenix／Ectoの腕前を上げる参考にしてください

[次回は、削除UIのトグル化](https://qiita.com/piacerex/items/9d32bdc3fd1955ff969c)を行います

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow: