dotdotdotPaul

dotdotdotPaul

Ecto: Validating belongs_to association is not nil?

Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’m spoiled by ActiveRecord, where I can just set the association to either a persisted or unpersisted object, and write a validation that ensures the child is “there”.

My example is a table/model, let’s call it Rating, and it belongs_to a Place (ie. field is place_id in the ratings table).

I figure there are three ways the set this association: One, we specify the place_id in the changeset directly. Two, we put_assoc an existing Place struct after the changeset options. Three, we have a Map with the Place parameters in the changeset under the :place key (and then use “cast_assoc”). So this is what I’ve got:

defmodule Rating do
  use MyApp.Web, :model
  schema "ratings" do
    field :rating, :integer
    belongs_to :place, MyApp.Place
    timestamps()
  end
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:rating, :place_id])
    |> cast_assoc(:place)
    |> assoc_constraint(:place)
    |> validate_required([:rating])
  end

In a test, I have this:

changeset = Rating.changeset(%Rating{}, { rating: 5 })
refute changeset.valid?, "Expected error on place constraint"  # 1
assert {:error, problem } = Repo.insert(changeset)  # 2

The refute fails, because no error is generated. I found some notes that “valid?” may not actually do any of the database queries necessary to ensure the parent object actually exists, so I thought maybe that would happen during the actual insert(), so I commented that line out and asserted on the next. However, that fails, too, and I can see that I get back :ok as a status, and a record saved with place_id nil.

If I validate place_id is required, then I can’t make this work where I either put_assoc an existing record, or pass in a Map of parameters.

Is it not possible to set up a singular changeset function to validate the belongs_to reference in all three ways it could be passed in? If assoc_constraint isn’t checking for non-nil associations, what do I need to make that work?

…Paul

PS> The migration has “add :place_id, references(:places, on_delete: delete_all)” if that matters.

Marked As Solved

wojtekmach

wojtekmach

Hex Core Team

it’s pretty hacky, but perhaps this would work for you?

defmodule Rating do
  # ...

  def changeset(rating, params \\ %{}) do
    cast(rating, params, ~w(rating place_id))
    |> validate_required(~w(rating)a)
    |> cast_or_constraint_assoc(:place)
  end

  defp cast_or_constraint_assoc(changeset, name) do
    {:assoc, %{owner_key: key}} = changeset.types[name]
    if changeset.changes[key] do
      assoc_constraint(changeset, name)
    else
      cast_assoc(changeset, name, required: true)
    end
  end
end

I’d stick to exposing two different changeset functions though (and probably have a 3rd private changeset function that has common stuff)

Also Liked

jeremyjh

jeremyjh

It does do that - if it is passed in as place_id it is validated with assoc_constraint; otherwise it is checked with cast_assoc required: true.

However the code as listed is really not complete because it does not handle updates to the record that do not include a change to the place; e.g. record is loaded from the db, place_id is set, but because place_id is not changed cast_assoc fails on it. In my project I’m using a slightly modified version that will pass the changeset if it contains the key (place_id) already:

    def cast_or_constraint_assoc(changeset, name) do
      {:assoc, %{owner_key: key}} = changeset.types[name]
      #assoc id was directly set? confirm its valid
      if changeset.changes[key] do
        assoc_constraint(changeset, name)
      else
        #assoc key is already present, and not changed? do nothing
        if Map.get(changeset.data, key) do
          changeset
        else
          #we need to insert a new assoc (or errors)
          cast_assoc(changeset, name, required: true)
        end
      end
    end
wfgilman

wfgilman

I think you want the following given your schema:

def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:rating, :place_id])
  |> validate_required([:rating]
  |> assoc_constraint(:place)
end

For a :belongs_to association, use assoc_constraint/3 for validation. It let’s Ecto check whether the Place to which the rating belongs exists. cast_assoc/3 would go on the Place schema to check Rating. Don’t use validate_required/3 to check association constraints (as instructed here).

I struggled with the different changeset validations in the same context. I outlined my findings here: Ecto Association vs Foreign Key Constraints

josevalim

josevalim

Creator of Elixir

I understsand now, thank you. @wojtekmach sounds like a good way to go about this.

josevalim

josevalim

Creator of Elixir

Don’t cast_assoc accept a required: true option?

ndac_todoroki

ndac_todoroki

For people looking for more validation, the code below works pretty well for me.
Using this code helps when you want to deal with a new Map or an existing Struct or an existing id was given for the association…

def put_or_cast_or_constraint_assoc(changeset, name) do
  {:assoc, %{owner_key: key, related: type}} = changeset.types[name] |> IO.inspect

  if changeset.changes[key] do
    assoc_constraint(changeset, name)
  else
    case val = changeset.params[name |> Atom.to_string] do
      %correct_type{} when correct_type == type ->
        put_assoc(changeset, name, val)
      %wrong_type{} ->
        add_error(changeset, name, "Wrong struct given to #{key}", given: wrong_type, wanted: type)
      %{} ->
        cast_assoc(changeset, name, required: true)
      _ ->
        add_error(changeset, name, "No valid #{name} nor #{key}")
    end
  end
end

Where Next?

Popular in Questions Top

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
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
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
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
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
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
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
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
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New

Other popular topics Top

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
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
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
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
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
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
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New

We're in Beta

About us Mission Statement