lucassperez

lucassperez

Is it possible to have Ecto unique_constraint with errors on multiple fields?

If I have some fields in one of my database tables that can’t simultaneously have the same values, I would create an index and then put the unique_constraint in my changeset, right?

Lets say I have a table of people with fields first_name and last_name. Two people can have the same first name or the same last name, but not both, otherwise they’d be the same person (I know that is not how real life works, it is just an example to make my question less abstract).

I made a migration with create unique_index(:people, [:first_name, :last_name]) to create the index in database level.

Then in my changeset pipeline, I put unique_constraint([:first_name, :last_name]).

If now I try to add “Lucas Perez” twice, the second insert would return a changeset like:

action: :insert,
changes: %{first_name: "Lucas", last_name: "Perez"},
errors: [
  first_name: {"has already been taken", [constraint: :unique, constraint_name: "people_first_name_last_name_index"]}
],
valid?: false

And if I check the constraints of my changeset (changeset.constraints), I’d have something like this:

[
  %{
    constraint: "people_first_name_last_name_index",
    error_message: "has already been taken",
    ...
    }
]

So far so good, this is exactly what is documented here, no surprises.

But is it possible to have both first_name and last_name in the fields of my constraints? So that the errors list would have both fist_name and last_name?

I even tried to call the unique_constraint twice in my changeset pipeline, but it ends using the last one.

changeset
|> unique_constraint([:first_name, :last_name], name: :people_first_name_last_name_index)
|> unique_constraint([:last_name, :first_name], name: :people_first_name_last_name_index)

If I do this, the changeset.constraints will have both constraints, each with a different field, but after attempting to insert the same person twice, the changeset.errors list will only have the last_name field.

I thought about creating a custom validation, but it is not very good, since the indexes/constraints are only checked at the moment of the database modification, which protects against concurrency. Also, a custom validation would traverse the whole table everytime I create a changeset, and not just at the moment of the database modification.

Is it possible to achieve this? Thanks a lot in advance!

Most Liked

lucassperez

lucassperez

First of all, thanks! So many responses, and very fast!

Yes, that is indeed an option. I did it, but in the end we decided to just use a custom name for the error key, like this:
unique_constraint(:custom_name, name: :people_first_name_last_name_index) and acted accordingly when this error key appeared.

If I understand correctly, this would have the same effect as a custom name to our constraint?

I see now, that is a good thing to have clear in my mind

This does makes sense with what I was seeing in my attempt to duplicate the unique_constraint call. Thanks for the clarifications!

Like I said, we decided to just use a custom name and be happy with it. Thanks for the responses, very insightful. :grin:

TimoMoss

TimoMoss

I don’t know if it’s still relevant, but I’d like to share my recent experience. I don’t know exactly when this option appeared, but it worked for me:

|> unique_constraint([:first_name, :last_name], 
      message:
        "The combination: first_name and last_name must be unique" )
stefanchrobot

stefanchrobot

I guess the more common case is a unique constraint on something like (account_id, username) and that’s probably the reason it works that way.

I think you can easily detect the unique constraint violation and manually add the error to the other field.

If you’d want to do this in a generic way, then Ecto.Changeset.constraints should be useful.

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
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
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
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
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
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement