现代 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_id
、maxreps
和 goal
作为参数并完全绕过 GenServer。
看了 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_id
、maxreps
和 goal
作为参数并完全绕过 GenServer。