jmurphyweb

jmurphyweb

Ecto Multi or Changeset - Business logic validation?

Context: In my application, projects make offers to users.

I’ve just discovered Ecto.Multi and am really happy as it makes a lot of sense to me in certain circumstances. For example:

If user accepts an offer, then I need to update the offer AND attach user to the project

What I am still unclear on is where I should run the logic that validates other business logic which requires further database queries. For example:

  • whether a user has the correct user type to receive an offer
  • whether the user has any pending offers already
  • whether the newly assigned project role (from the offer) is already taken

Up until now I have kept this logic in the changeset pipeline, and as I have tried to keep changesets pure, it means passing in lots of arguments to the changeset function (eg, all project offers).

I wonder if I should rather be making use of Multi’s for this purpose:

offers.ex context:

def create_offer(params, project, current_user) do
  create_offer_multi =
    Multi.new()
    # run basic validation on the new offer form
    # if offer form is valid, go into calcs
    |> Multi.run(:offer, create_offer_changeset(params, project, current_user))
    |> Ecto.Multi.run(:valid_recipient, fn _repo, %{offer: offer} ->
      # run some validation that ensures recipient user:
      # - can receive an offer and
      # - they don't already have any other pending offers for this project
      Offers.validate_offer_recipient(project, offer)
    end)
    |> Multi.run(:add_project_role, fn _repo, %{offer: offer, valid_recipient: recipient} ->
      # if all successful, add user to project with the given role
      Projects.add_project_role(project, offer, recipient)
    end)

  case Repo.transaction(create_offer_multi) do
    {:ok, %{offer: offer}} -> {:ok, offer}
    error_res -> error_response(:create_offer, error_res)
  end
end

def create_offer_changeset(params, project, current_user) do
  recipient = Accounts.get_user_by_email(params["email"])

  params
  # run pure changeset casting & validation logic on offer
  |> Offer.create_offer_changeset(project)
  |> Ecto.Changeset.put_assoc(:project, project)
  |> Ecto.Changeset.put_assoc(:creator, current_user)
  |> Ecto.Changeset.put_assoc(:recipient, recipient)
  # if offer changeset is valid, then go and run a load of calculations
  # that need to access database and attach them to the changeset
  |> Offer.run_and_merge_calcs(project)
end

I have been working in Elixir/Phoenix for a while, but with no mentor and I am still quite confused:

How should I approach this sort of validation?
Are there any problems with this code? How could it be improved?

Any help greatly appreciated.

Most Liked

jeremyjh

jeremyjh

We use Ecto.Multi a lot for long chains of constraint checks. Many checks disqualify themselves and return {:ok, :not_applicable}, and of course there are some that are performed and succeed or find a violation of constraints and return a useful error message.

The main downside is that stack traces become a lot harder to read, and for many cases with chains are preferred for that reason. The choice between the two usually comes down to two things: are we doing database updates along the way (e.g. is it a chain of constraints and database operations), and is the logic complicated enough to span several modules. Ecto.Multi pipelines scales pretty well across thousands of lines of code, you can always just add another constraint and know that the failure will be handled the right way.

We put validations in Changeset functions two, but our rule of thumb there is to only do that for pure functions that operate on data always available in the struct itself (which may include some associations, but its not good to make assumptions about what will always be preloaded, or you will be preloading more than is necessary).

jeremyjh

jeremyjh

Yes this looks reasonable and is along the lines I was suggesting.

Where Next?

Popular in Questions Top

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 th...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
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
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
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

We're in Beta

About us Mission Statement