GenServerとは何でどういう時に使うのか？

# はじめに

今回は`GenServer`とは何か？　どのような時に`GenServer`を使うのか？を解説します。

関連テーマである。Webソケット通信の復習はこちらから↓

https://qiita.com/mito_mito/items/3cb51e768a54750b7e75


# `GenServer`とは
GenServerはWebSocketとは別のものです。
GenServerはあくまでeditor（構造体）のことを指します。
データを取得するときはGenServerに登録した時と同じpidを使用する必要があります。
そのpidは元々sessionが持っている状態ですが、mount/3以外からsessionにアクセスすることができません。
そこで、sessionが持っているpidをsocketに登録し、他のLiveViewのコールバック関数からアクセスするという使い方をします。

[参考hexdocs: GenServer](https://hexdocs.pm/elixir/GenServer.html#content)

# どのような時に`GenServer`を使うのか？

LiveViewは2回mountを行います。2回のmountで違うデータを取得してしまい、データの整合性が取れなくなってしまう時に、1回目のmountで取得したデータをGenServerに登録し、2回目のmount時にも使うようにします。

具体的には、ランダムに練習問題を生成するような学習アプリにおいて1回目のmountでランダムに取得した練習問題をGenServerに登録し2回目のmountはGenServerから取得し、socketにasaignして以後使用します。(GenServerで登録しないと1回目と2回目で取得する問題が変わってしまうため)


## データの流れ
- 1回目のmountでDBから必要な値を取得し、editor構造体を作る
- 作ったeditoer構造体をGenServerのストレージに入れる
- 2回目のmountの時にGenServerに登録したeditor構造体をとってくる

## LiveViewのライフサイクル
LiveViewのライフサイクルが理解できていないとなぜこのようなことをしなければいけないのか？がわからないと思うので、参考の図解も貼っておきます。

![IMG_0173.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743559/c541da8f-b164-6b7f-9363-99913f89b723.jpeg)


参考: [Elixir実践入門 第10章 LiveViewによるフロントエンドの開発](https://www.amazon.co.jp/Elixir%E5%AE%9F%E8%B7%B5%E5%85%A5%E9%96%80%E2%94%80%E2%94%80%E5%9F%BA%E6%9C%AC%E6%96%87%E6%B3%95%E3%80%81Web%E9%96%8B%E7%99%BA%E3%80%81%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%80%81IoT-WEB-DB-PRESS-plus%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA/dp/4297140144/ref=sr_1_1?crid=2VGWJTHUPQHD2&dib=eyJ2IjoiMSJ9.tXNV_U-MEzwo2AbKggaGvwSArjmxivVq3Y3KbuXYgJCAZrgexXBfjqRHm56i8y12WfFIxxn2RVfca6DLWywmP1tV4qODfAUu6BNGn6dBsgXOC1KBTPPoZdszxNZMDxNdgaDt-Bxq6ukod_tVGnvx_jiHbBICHGAuz6vZHE8Ng8gWMXw5CoUoK78tDMmOvx1JgAjOPQZ0ARoRnPHvR0_AwRB05o623dbWibB4wVTl5D2MlUgLMRuwiK9wuiwzNPqn2aU1rA5wFPK5d_xLa-r_4LpbrXOgFQQR7wyOV_VmoWQ.EokNGs8rydjM25LBsVbWIHTrJ0ep8u2sbxC3WsxZpiE&dib_tag=se&keywords=elixir%E5%AE%9F%E8%B7%B5%E5%85%A5%E9%96%80&qid=1710963148&sprefix=Elixir%E3%81%98%E3%81%A3%E3%81%9B%E3%82%93%2Caps%2C191&sr=8-1)


# 処理の流れ

## まずは日本語で
- 1回目のmount/3で`session`の`pid`とそれに紐づくeditors構造体を`GenServer`に登録
- 2回目のmount/3で`GenServer`から登録した`pid`とeditors構造体を取得
- 取得したデータを`socket`に`asign`する
- Webソケットが切断され、`terminate/2`が呼ばれるので`socket`に`asign`した`pid`とeditors構造体を`GenServer`に登録
- 再度接続が復活した時に、2回目のmount/3が呼ばれるので`GenServer`から登録した`pid`とeditors構造体を取得


# コードベースの解説

## mountでのpidをsetする処理とgetする処理

```elixir
  def mount(%{"log_id" => log_id}, session, socket) when socket.assigns.live_action === :random do
    editor =
      if connected?(socket) do
       # LiveViewの接続が確認できているので2回目のmount
        UseQuestion.get_editor(session["question_pid"])
      else
       # LiveViewの接続が確認できていないので1回目のmount
        log = Logs.get_incomplete_mb_ques_log(log_id)

        editor =
          socket.assigns.current_user
          |> MbQuesEditor.construct(log)
          |> MbQuesEditor.update_q_task_times(0, :start)

        UseQuestion.set_editor(session["question_pid"], editor)

        editor
      end

    {:ok, assign_use(socket, editor, session)}
  end

```

## mountで取得したデータをsocketにasignする

```elixir
  defp assign_use(socket, editor, session \\ %{}) do
    socket
    |> assign(:editor, editor)
    |> assign(:set_submit, false)
    |> assign(:button_disabled, true)
    |> assign(:button_class, "bg-gray-30")
    |> assign(:set_textbook, false)
    |> assign(:current_index, 0)
    |> assign(:set_exit, false)
    |> assign(:set_page, false)
    |> assign(:page, nil)
    |> assign(:question_pid, session["question_pid"])
    |> push_event("start_message", %{})
  end

```

## 中断の場合GenServerに該当するpidのデータを更新して保存する

```elixir
  @impl true
  def terminate(_reason, socket) do
    # 中断の場合はGenserverに値を保存しておく
    unless socket.assigns.editor.log.completed && socket.assigns.live_action == :answers do
      UseQuestion.set_editor(socket.assigns.question_pid, socket.assigns.editor)
    end
    :ok
  end

```
## GenServerの値をsetとget

```elixir

  @doc """
  GenServerに保存しているeditorを取得します。

  ## Examples

      iex> get_editor(pid)
      %{}
  """
  @spec get_editor(pid()) :: map()
  def get_editor(pid) do
    GenServer.call(pid, :get_editor)
  end

  @doc """
  GenServerにログとエディターを保存します。

  ## Examples

      iex> set_log_and_editors(pid, editor)
      :ok
  """
  @spec set_editor(pid(), map()) :: :ok
  def set_editor(pid, editors) do
    GenServer.cast(pid, {:set_log_and_editors, editors})
  end
end

```

# コードの補足

## `connected`とは

2回目のmountを確認しています。(Webソケットの接続)
これだけだとよくわからないと思うので順を追って解説します。

1回目のmountはHTTP接続および画面作成のDOM生成(テンプレート作成)。2回目はWebSocketに接続しましたという通知を送る役割で行います。
つまり、`connected`はWebSocketに接続しているかを確認するヘルパー関数なので実質的には2回目のmountかどうかを確認していることになります。

かなり拙い図解ですが...。
![スクリーンショット 2024-05-09 16.23.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743559/5b26ff45-8fb5-ec44-ef8c-8403b0906139.png)

[参考hexdocs: Phoenix.LiveView.connected/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#connected?/1)

## `terminate/2`とは

ページ終了時、ブラウザバック、Webソケットが切断された時に呼ばれる

[参考hexdocs: Phoenix.LiveView.terminate/2](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:terminate/2)

[参考hexdocs: GenServer.terminate/2](https://hexdocs.pm/elixir/GenServer.html#c:terminate/2)

# まとめ

GenServerは名前から別のサーバーのように認識していましたが、構造体ということがわかりました。