现代 Phoenix 网络应用程序中的 OTP 和 Ecto 代码分离

OTP and Ecto code separation in a modern Phoenix web app

看了 this 的谈话后,我明白了如何分离 Web 界面和 OTP 应用程序,但是 OTP 应用程序和 Ecto 代码应该如何分离?

目前我正在编写一个调用 Ecto 函数或 Ecto 函数的包装函数的 OTP 应用程序, handle_call/3 回调中:

@doc """
Generates a workout.
iex> Pullapi.Database.delete_workouts()
iex> Pullapi.Database.delete_sets()
iex> result = Pullapi.GenServerWorker.handle_call({:initial_workout, 1, 20, 25}, nil, %{})
iex> {:reply, [{:ok, %Pullapi.Set{__meta__: _, action: action, id: _, inserted_at: _, order: _, units: _, updated_at: _}}| rest], %{}} = result
iex> action
"Pullups"
"""
def handle_call({:initial_workout, user_id, maxreps, goal}, _from, state) do
  # insert Goal
  %Pullapi.Goal{user_id: user_id, units: goal}
  |> Pullapi.Database.insert_if_not_exists


  # get Inital config
  config = Application.get_env(:pullapi, Initial)

  # retrieve id from inserted Workout row
  result = %Pullapi.Workout{user_id: user_id} |> Pullapi.Database.insert_if_not_exists

  case result do
    {:ok, workout} ->
        %Pullapi.Workout{__meta__: _, id: workout_id, inserted_at: _, updated_at: _, user_id: _} = workout

        inserted_sets = maxreps
        |> (&(&1*config[:max_reps_percent]/100  |> max(1))).()
        |> round
        |> Pullapi.Numbers.gaussian(
             config[:standard_deviation],
             config[:cap_percent],
             config[:cut]
           )
        |> Pullapi.Database.make_pullup_sets(workout_id)
        |> Pullapi.Database.add_rest_sets(config[:rest_intervals])
        |> Enum.map(&Pullapi.Repo.insert/1)

    {:error, _}  ->
        inserted_sets = []
  end

  {:reply, inserted_sets, state}
end

这种方法是否将两者耦合得太紧密了?

使用数据库,因为 GenServer 回复是使用先前生成的用户特定数据计算的 - 我希望应用程序能够在重启后继续存在。

您的代码示例根本没有触及 GenServer 状态,这可能意味着它不需要首先位于 GenServer 内部。

实际上,将它放在 GenServer 中可能是一个非常糟糕的主意,因为您可能将所有数据库操作置于单个进程之后,这现在将成为您系统的瓶颈。

这里的一般准则是不要将进程用于代码组织目的,而是用于当您需要表达某些运行时 属性,例如并发、全局状态或容错时。

为了更准确地回答您的问题,请将您的域 API 视为常规模块和函数,可能需要与许多进程对话才能完成工作。这些流程越小、越集中,代码通常就会越干净。如果您需要一个流程来保持状态,请关注它的状态,而不是直接向它添加业务逻辑。如果您需要一个进程充当锁,请隔离地实现锁,与您的用例和域分离。等等等等

Spawn 但不是 Spawn 文章可能会有所帮助。我是合著者的 Adopting Elixir book 也探讨了这些主题。

编辑:特别是对于您的示例,您可以将上面的所有代码移动到一个名为 initial_workout/3 的函数中,该函数接收 user_idmaxrepsgoal 作为参数并完全绕过 GenServer。