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
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
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
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{}.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









