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 - #2 by wfgilman

josevalim

josevalim

Creator of Elixir

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

Where Next?

Popular in Questions Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
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
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
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
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
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
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

Other popular topics Top

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
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
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
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
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
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
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 47930 226
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New

We're in Beta

About us Mission Statement