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

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
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
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
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
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
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
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
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
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

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
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
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
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
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
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
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
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement