Understanding how changesets are implemented and used

Hi all, I’m currently reading a book on web development in Phoenix and I’m having trouble understanding some of the content. I was hoping you’d be able to help.

I have the following module which defines a Schema for Ecto to work with:

defmodule Vocial.Votes.Poll do
  use Ecto.Schema
  import Ecto.Changeset
  alias Vocial.Votes.Poll
  alias Vocial.Votes.Option

  schema "polls" do
    field :title, :string
    has_many :options, Option
    timestamps()
  end

  def changeset(%Poll{}=poll, attrs \\ %{}) do
    poll
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end

I’m struggling to understand what’s going on in the changeset method. Here are my questions:

  1. %Poll{}=poll is this saying that the first argument is the default/empty struct Poll? If so, then is this the same as writing poll=%Poll{}?
  2. As you can see %Poll{} is based on alias Vocial.Votes.Poll which is also the name of the module where it sits, so it’s referencing itself. However, there is no struct definition here, so I’m not sure what Elixir would expect from a %Poll{} object. Is the struct somehow read from the schema?
  3. Lastly, the validate_required(:title) is there to ensure that we pass the title among the attributes, right? If so, then there is another function in the codebase which I would have thought should break it, but it doesn’t – the function is def new_poll, do: Poll.changeset(%Poll{}) – no title is passed, yet the validator doesn’t complain. Why is that?

Thank you in advance! :slight_smile:

  1. No, it’s pattern match and ensure poll is of type Poll
  2. I would instead use
alias __MODULE__

and yes… struct is from schema definition

  1. Are You sure the returned changeset is valid? with no errors?
1 Like

Hey, thanks for replying!

  1. So it’s just a type check, right? When I suggested initially that it’s the same as poll=%Poll{} thinking that it’s a default parameter, but I forgot that defaults in Elixir are set with \\.
  2. Ah, nice, that a great suggestion, didn’t realise you can do that!
  3. I think so, unless I’m missing something, but here is some additional code on that:
def new_poll do
    Poll.changeset(%Poll{}) # no title!
end
...
def new(conn, _params) do
    poll = Votes.new_poll()
    conn
    |> render("new.html", poll: poll)
end

It’s not complaining on the ui side, because the form has not yet been validated…

but a closer look at the changeset will reveal the changeset content

Try to add

<%= inspect @poll %>

somewhere in your template.

1 Like

Yes, you should see the errors when inspecting @poll. Just an additional note that Phoenix forms work with the action value in order to display errors in the UI. You will probably see that action is currently nil. However, calling something like Repo.update or Repo.insert on that invalid changeset will return a changeset with action set to :update or :insert. If Phoenix has a changeset with the action set it displays errors on the form for the appropriate inputs.

You can do the same manually by calling Ecto.Changeset.apply_action(changeset, :update) (or whatever action makes sense). If there are errors, a tuple will be returned with {:error, changeset}. This provides for a lot of flexibility and power to validate data outside database calls.

I’m sure that Phoenix book will be great from a high-level perspective, but I would also suggest reading this page about Ecto changesets; it is well-written and helps fill in a lot of the gaps… https://hexdocs.pm/ecto/Ecto.Changeset.html

3 Likes

@baldwindavid, @kokolegorille, ah, you are absolutely right! If I add inspection to the page, I get the following:

#Ecto.Changeset<action: nil, changes: %{}, errors: [title: {"can't be blank", [validation: :required]}], data: #Vocial.Votes.Poll<>, valid?: false>

It’s somewhat unintuitive that it starts off with an error which then gets updated/corrected.

Thanks for the explanation!