cnorris
cnorris

Reputation: 548

Modify the first related record in a changeset

Is there a way to change only the first item in a relationship via a changeset? Aka if I had:

s it possible to do something like:

schema "accounts" do 
    ...
    has_many(:emails, StockrtApi.Accounts.Email)
end

def changeset(account, params) do
    account
    ...
    |> cast_assoc(:emails, required: true, with: &Email.changeset(&1, &2))
    |> put_change(first_email_only, {primary: true}) # <- might have to go in email schema
end

Or what's the best way to approach this type of problem?

Upvotes: 0

Views: 55

Answers (1)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 120990

One should not rely on the order of anything when dealing with databases (unless an explicit ORDER BY clause is presented.) While most database drivers will return a predictably ordered set without an explicit ORDER BY clause, it is by no means guaranteed. That said, it’s always better to prefer explicity over implicity: just introduce another through association:

schema "accounts" do 
  ...
  has_many :emails, StockrtApi.Accounts.Email
  has_one :primary_email, through: [:primary_email, :email]
end

And put this association with a usual Ecto.Changeset.put_assoc/4.


As per a comment, when all you need is to ensure there is an email, do exactly this:

defp ensure_email(
  %Ecto.Changeset{errors: errors, changes: %{emails: emails}} = changes
) do
  case emails do  
    [] ->
      new_errors = [{:emails, {"can’t be empty", [validation: :emails]}}]
      %{changes | errors: new_errors ++ errors, valid?: false}
    _ -> changes
  end
end

and use it explicitly:

def changeset(account, params) do
  account
  ...
  |> cast_assoc(:emails, required: true, with: &Email.changeset(&1, &2))
  |> ensure_email()
end

That way you ensure that at least one email is present—just use the first one as primary.

Upvotes: 1

Related Questions