はじめに
今回はGenServer
とは何か? どのような時にGenServer
を使うのか?を解説します。
関連テーマである。Webソケット通信の復習はこちらから↓
GenServer
とは
GenServerはWebSocketとは別のものです。
GenServerはあくまでeditor(構造体)のことを指します。
データを取得するときはGenServerに登録した時と同じpidを使用する必要があります。
そのpidは元々sessionが持っている状態ですが、mount/3以外からsessionにアクセスすることができません。
そこで、sessionが持っているpidをsocketに登録し、他のLiveViewのコールバック関数からアクセスするという使い方をします。
どのような時に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のライフサイクルが理解できていないとなぜこのようなことをしなければいけないのか?がわからないと思うので、参考の図解も貼っておきます。
参考: Elixir実践入門 第10章 LiveViewによるフロントエンドの開発
処理の流れ
まずは日本語で
- 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する処理
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する
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のデータを更新して保存する
@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
@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かどうかを確認していることになります。
参考hexdocs: Phoenix.LiveView.connected/2
terminate/2
とは
ページ終了時、ブラウザバック、Webソケットが切断された時に呼ばれる
参考hexdocs: Phoenix.LiveView.terminate/2
参考hexdocs: GenServer.terminate/2
まとめ
GenServerは名前から別のサーバーのように認識していましたが、構造体ということがわかりました。