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.

Where Next?

Popular in Questions 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
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
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
jaysoifer
Is there a way to rollback a specific migration and only that one ("skipping" all the other ones)? Would mix ecto.rollback -v 2008090...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
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
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
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
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
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
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39247 209
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35953 110
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I'm a nov...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
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

We're in Beta

About us Mission Statement