Francesco

Francesco

Best practice for pattern matching ecto changeset error constraints?

Hello!

I have a user table where I don’t allow two verified users with the same email to exist. This is enforced by a unique constraint on the table and a corresponding unique constraint on the changeset. When this case occurs I will get back a changeset that looks like:

#Ecto.Changeset<
  action: :insert,
  changes: %{...},
  errors: [
    email: {"has already been taken",
     [constraint: :unique, constraint_name: "users_verified_email_index"]}
  ],
  data: #Scribe.Accounts.User<>,
  valid?: false
>

I want to pattern match on this particular error constraint however this is a bit of a full on pattern match and it involves keyword list which means I have to get the number and order of items to match, i.e.

{:error, %Ecto.Changeset{ errors: [email: {_, [constraint: :unique, constraint_name: _]}] }} ->

Is there a better way to pattern match on these error constraints? Or is there an alternative way of approaching the problem?

Perhaps instead of trying to insert a user and acting on the error I should be checking if the user exists first in the user context function and if it does just return that user object and don’t try to insert anything. So to anyone using this new get_or_insert function they can assume that any error indicates that something bad happened instead of having to make sense of the error.

Thanks in advance for the help!

Most Liked

dimitarvp

dimitarvp

I usually do almost the same as you here, with the exception of making a function to search for a specific error in the list of changeset errors:

defp email_taken?({:email, {_, [constraint: :unique, constraint_name: _]}}), do: true
defp email_taken?(_), do: false

def do_stuff() do
  cs = insert_stuff()
  email_is_taken = Enum.any?(cs.errors, &email_taken?/1)
end

The advantage of this approach is that you can also check for other errors, the code looks a bit cleaner and more readable (IMO), and the whole thing is composable and the approach can be reused (you just need to add new checker functions).

stefanchrobot

stefanchrobot

I’m doing it this way:

  def create_changeset(...) do
    ...
    |> unique_constraint(:some_id, message: "already_taken")
  end

  # in the same module
  def already_taken?(%Ecto.Changeset{} = changeset) do
    case changeset.errors[:some_id] do
      {"already_taken", _} -> true
      _ -> false
    end
  end

I would advise against doing that, since you’re defeating the purpose of RDBMS-based validations that Ecto embraces. If you were to choose that approach, you’d still need to keep the code for handling failed constraint to make sure nobody inserts the user between your SELECT and INSERT, so you’re back where you started.

Last Post!

thojanssens1

thojanssens1

Unfortunately that code logic won’t work if you wanna use it in a transaction.

** (Postgrex.Error) ERROR 25P02 (in_failed_sql_transaction) current transaction is aborted, commands ignored until end of transaction block

Looks like once you got a constraint error, the transaction is aborted. Seems like you have to call get, and if nil then create the record. It’s less safe if another process created the record between the get and create call, but I don’t see any easy safer way.

Where Next?

Popular in Questions Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
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
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
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
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
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

Other popular topics Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
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
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
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement