maxguzenski

maxguzenski

With Phoenix on a umbrella we lost the ability to join several functions together

Im porting a phoenix app to a umbrella, where I have a phoenix project and a domain project. Phoenix project dont “see” ecto, justo call domain modules.

I fully understand the advantages of decoupling these projects, but at the same time I ended up losing the ability to join several functions and modules to form a result. How have you deal with it?

example:

from phoenix without umbrella, I could do that on a controller:

assoc(user, :friendships)
|> Friendship.accepted
|> limit(5)
|> select([f], f.friend_id)
|> order_by([f], [desc: :id])
|> Repo.all

with phoenix into a umbrella:

Friendships.list_accepted(user, limit: 5, select: :friend_id, order: [desc: :id])

My concern is that in the end it seems that I’m doing “sql” in the function parameters, just to not use Ecto directly.

Most Liked

michalmuskala

michalmuskala

It’s very interesting to read this, because my experience is exactly the opposite. Initially we had functions returning changesets (multis weren’t a thing back then) and exposed query bits as functions, so you could easily compose them in the controllers.

This proved to be not such a great idea in the long run for several reasons:

  • we have several endpoints where the JSON data doesn’t map 1-to-1 to the ecto schemas, so there are transformations on both data going into the database and out, and errors returned from validations (in case the differences are big enough we have some raw schemas acting as a sort of “form models”).
  • with multis you most often need to format the result in some way, having this done in the controller felt “dirty”.
  • we have two repos and two databases, so we want to keep the knowledge of what goes where outside of the controllers
  • it became really hard to do some one-off actions by hand.
  • there was a lot of logic in the controllers - what functions to call in what order, especially around the queries became problematic.

We migrated to the “one function per use case” style and it works much better. It’s immediately clear from looking at the controller what is happening (since those functions have meaningful names), and all controllers became very simple - matching on the result of the use case and dispatching appropriately.
I don’t agree that passing “raw” maps as options to those functions is http leaking into the system - anything that is not beam will give you “raw” arguments with strings instead of atoms, etc. If you provided a CLI interface you’d also get those.
I usually try to style all the domain “mutation” functions in a similar way:

foo(resource_or_id, params, actor)

Each of those functions may call other components to do the “dirty” work, but they all return {:ok, resource} or {:error, map}, where the errors map is already properly converted, processed and ready for rendering.

pdilyard

pdilyard

In my experience, it is pretty easy to grow a service layer when your logic gets complex enough to have one.

So, for example, you might have a setup like this:

apps/
  api/
  db/
  post_service/

Where api is just Phoenix controllers and views, db is just Ecto.Schema definitions + pure functions, and post_service might be where your business logic is contained. So then api would call functions in post_service, and handle the results of those.

My very humble opinion: that’s a lot of overhead for something as simple as a blog (in my example), and something you could very easily refactor as complexity increases. All you would have to do is move your dependency on db to your service layer.

Now, something I do really like to do from the beginning is build a set of modules around Ecto.Multi. Again, these functions are pure and simply return an %Ecto.Multi{} struct that you can run through Repo.transaction/1 in your controllers or elsewhere. See this post for more info on that: https://blog.danielberkompas.com/2016/09/27/ecto-multi-services/

pdilyard

pdilyard

I’ve got a similar setup, and here’s my solution:

Umbrella app structure:

apps/
  db/
    lib/
      schemas/
        user.ex
        post.ex
    mix.exs
  api/
    web/
      controllers/
        user_controller.ex
        post_controller.ex
    mix.exs

In apps/api/mix.exs (your api app depends on your db app):

# ...
defp deps do 
  {:db, in_umbrella: true},
end
# ...

In apps/db/lib/schemas/post.ex for example:

defmodule DB.Post do
  use Ecto.Schema
  import Ecto.Query

  schema "posts" do
    belongs_to :author, DB.User
    field :slug, :string
    field :title, :string
    # ...
    timestamps
  end

  def changeset(post, params \\ %{}) do
    # ...
  end

  def where_author_id(query, user_id) do
    from p in query,
      where: p.author_id == ^user_id
  end

  def where_slug(query, slug) do
    from p in query,
      where: p.slug == ^slug
  end

  def sort(query) do
    from p in query,
      order_by: [desc: p.inserted_at]
  end
end

Then, in your controllers, you can just do this:

alias DB.{Post, Repo}
# ...
def index(conn, _params) do
  Post
  |> Post.sort()
  |> Repo.all()
end

def show(conn, %{"slug" => slug}) do
  Post
  |> Post.where_slug(slug)
  |> Repo.one()
end

def author_index(conn, %{"author_id" => author_id}) do
  Post
  |> Post.where_author_id(author_id)
  |> Post.sort()
  |> Repo.all()
end
# ...

As you can see, the queries compose together quite nicely. Tip: Keep any side effects out of your schemas (e.g. don’t use Repo in schema modules). All those pure functions make it really nice to compose things together.

Also, because your api app depends on your db app, and your db app depends on ecto, you actually do have access to Ecto.* modules in your api app. So, in your controllers, you can still do things like pattern match on %Ecto.Changeset{}.

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call t...
New
yawaramin
In the Dialyzer docs ( dialyzer — OTP 29.0.2 (dialyzer 6.0.1) ), there is a way to turn off a specific warning for a function: -dialyzer...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31265 143
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52673 488
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement