Programming Phoenix – Video to Category Relationship

I’m currently working my way through the Programming Phoenix book, which I have to say, I am thoroughly enjoying.

That’s not to say it’s all plain sailing. If only I were that good.

I got to chapter 7, and got myself quite stuck.

I’m aware the book was written for an earlier version of Phoenix, and in an attempt to force myself to “learn the hard way”, I decided that whenever I hit on a deprecated function, I would use the given replacement.

Ok, so in the book on page 99 (Chapter 6: Building Relationships), there is the following code:

  @required_fields ~w(url title description)
  @optional_fields ~w(category_id)

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end

When using this code I got a deprecation warning:

warning: `Ecto.Changeset.cast/4` is deprecated, please use `cast/3` + `validate_required/3` instead
    (rumbl) web/models/video.ex:34: Rumbl.Video.changeset/2
    (rumbl) web/controllers/video_controller.ex:33: Rumbl.VideoController.create/3
    (rumbl) web/controllers/video_controller.ex:1: Rumbl.VideoController.action/2
    (rumbl) web/controllers/video_controller.ex:1: Rumbl.VideoController.phoenix_controller_pipeline/2

Seeing as I made the commitment to using the newer functions, this seemed like as good a time as any to do so.

The suggested replacement: cast/3 (docs) is where my confusion started. Here’s the signature:

cast(data, params, allowed)

And here’s the signature for cast/4 :

cast(data, params, required, optional)

You may then be wondering why in the original code from the book then that we only have three arguments being passed in?

Good question.

Really there are four, though one is being passed in as the first argument by way of Elixir’s pipe operator – which passes the outcome of the previous statement into the first argument of the next:

  model
 |> cast(params, @required_fields, @optional_fields)

As seen in the code from the Programming Phoenix book, cast/3 expects some params, then a list of required_fields, and another list of optional_fields.

One nice thing happening in the original code is the use of Attributes (@ symbol) as constants.

Using the ~w sigil is a way to create a list from the given string. Ok, so lots of things happening here in a very little amount of code. This is one of the benefits, and drawbacks (when learning) of Elixir, in my experience.

With PHPStorm I’m so used to having the code-completion and method signature look ups (cmd+p on mac) that learning and remembering all this stuff is really the hardest part. Maybe ‘intellisense’ has made me overly lazy.

Anyway, all of this is nice to know but it’s not directly addressing ‘problem’ I faced (and I use the inverted commas there as this isn’t a problem, it’s my lack of understanding).

We’ve gone from having required and optional fields, to simply just allowed fields.

My replacement code started off like this:

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:url, :title, :description])
    |> assoc_constraint(:category)
    |> validate_required([:url, :title, :description])

Everything looked right to me.

I had the required fields, but now as a list, rather than the ~w sigil approach.

I had specified my three required fields by way of validate_required.

And as best I could tell, I was associating with the :category.

But no matter what I did, my Video was never associated with a Category on form submission.

I could see the Category was being submitted from my form:

%{"category_id" => "3", "description" => "Quality techno", "title" => "Richie Hawtin @ ENTER Ibiza Closing Party 2014, Space Ibiza",
  "url" => "https://www.youtube.com/watch?v=-5t2gH0l99w"}

But the generated insert statement was missing my category data:

INSERT INTO "videos" ("description","title","url","user_id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) ...

Anyway, it turns out I was over thinking this.

All I needed to do was add in :category to the list passed in as the allowed argument and everything worked swimmingly:

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:url, :title, :description, :category_id])
    |> assoc_constraint(:category)
    |> validate_required([:url, :title, :description])
  end

With the outcome of the next form submission:

INSERT INTO "videos" ("category_id","description","title","url","user_id","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7) ...
[debug] QUERY OK db=1.1ms

Easy, when you know how.

This took me an embarrassingly long time to figure out. I share with you for two reasons:

  1. Learning the hard way makes you learn a whole bunch more than simply copy / pasting, or typing out what you see on the book pages.
  2. I couldn’t find anything about this after a lot of Googling, so if some other poor unfortunate soul is saved a lot of headscratching from this in the future then it was all worthwhile writing.

That said, the new edition of Programming Phoenix is due for release very soon (so I believe), so likely this will have been addressed already.

Published by

Code Review

CodeReviewVideos is a video training site helping software developers learn Symfony faster and easier.

7 thoughts on “Programming Phoenix – Video to Category Relationship”

  1. I was not even getting the deprecation warning, and there is still nothing about this in the book errata. I had no idea for weeks why my changeset.changes were empty (I was trying it in the REPL).

    Thanks for the post.

  2. I would suggest leaving the required and optional lists and doing something like this:

    def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, @required_fields ++ @optional_fields)
    |> assoc_constraint(:category)
    |> validate_required(@required_fields)
    end

    1. @required_fields ++ @optional_fields

      That’s really cool, I didn’t know that is even possible. It’s little things like that that make me love Elixir’s syntax.

      Thanks for sharing

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.