0
votes

I've installed fresh new Phoenix 1.3 and generated Blog context with a couple of schemas Post and Comment in it. My test project is called NewVersion (as I'm testing new version of the Phoenix framework).

So, my schemas are pretty standard, mostly generated by phx.gen.html:

Post.ex

defmodule NewVersion.Blog.Post do
  use Ecto.Schema
  import Ecto.Changeset
  alias NewVersion.Blog.Post


  schema "blog_posts" do
    field :body, :string
    field :title, :string
    has_many :comments, NewVersion.Blog.Comment, on_delete: :delete_all, foreign_key: :blog_post_id

    timestamps()
  end

  @doc false
  def changeset(%Post{} = post, attrs) do
    post
    |> cast(attrs, [:title, :body])
    |> validate_required([:title, :body])
    |> cast_assoc(:comments)
  end
end

comment.ex

defmodule NewVersion.Blog.Comment do
  use Ecto.Schema
  import Ecto.Changeset
  alias NewVersion.Blog.Comment


  schema "blog_comments" do
    field :body, :string
    belongs_to :blog_post, NewVersion.Blog.Post, foreign_key: :blog_post_id

    timestamps()
  end

  @doc false
  def changeset(%Comment{} = comment, attrs) do
    comment
    |> cast(attrs, [:body])
    |> validate_required([:body])
  end
end

blog.ex

defmodule NewVersion.Blog do
  @moduledoc """
  The boundary for the Blog system.
  """

  import Ecto.Query, warn: false
  alias NewVersion.Repo

  alias NewVersion.Blog.Post

  def list_posts do
    Repo.all(Post)
  end

  def get_post!(id), do: Repo.get!(Post, id)

  def create_post(attrs \\ %{}) do
    %Post{}
    |> Post.changeset(attrs)
    |> Repo.insert()
  end

  def update_post(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)
    |> Repo.update()
  end

  def delete_post(%Post{} = post) do
    Repo.delete(post)
  end

  def change_post(%Post{} = post) do
    Post.changeset(post, %{})
  end

  alias NewVersion.Blog.Comment

  def list_comments do
    Repo.all(Comment)
  end

  def get_comment!(id), do: Repo.get!(Comment, id)

  def create_comment(attrs \\ %{}) do
    %Comment{}
    |> Comment.changeset(attrs)
    |> Repo.insert()
  end

  def update_comment(%Comment{} = comment, attrs) do
    comment
    |> Comment.changeset(attrs)
    |> Repo.update()
  end

  def delete_comment(%Comment{} = comment) do
    Repo.delete(comment)
  end

  def change_comment(%Comment{} = comment) do
    Comment.changeset(comment, %{})
  end    

end

Everything works, but I want to create nested form for creating and editing posts with their comments and for that I want to preload comments to posts. In the guides (https://github.com/elixir-ecto/ecto/blob/master/guides/Associations.md) I've found an example for posts and tags which looks like this:

tag = Repo.get(Tag, 1) |> Repo.preload(:posts)

But when I change the function get_post! in my blog.ex similar way from:

  def get_post!(id), do: Repo.get!(Post, id)

to:

  def get_post!(id), do: Repo.get!(Post, id) |> Repo.preload(:comments)

I get an error: protocol Ecto.Queryable not implemented for %NewVersion.Blog.Post{meta: #Ecto.Schema.Metadata<:loaded, "blog_posts">, body: "This is the second Post", comments: [%NewVersion.Blog.Comment{meta: ...

Why is that? It seems that I strictly follow the docs but something is missing. I just don't see the difference between my code and what is provided there, of course I know what means to implement the protocol.

By the way if I change my function to this:

def get_post!(id), do: Repo.get!(Post |> preload(:comments), id) 

The error goes away. But still I want to get why the first approach doesn't work for me.

The full error message with stacktrace is:

[error] #PID<0.8148.0> running NewVersion.Web.Endpoint terminated
Server: localhost:4000 (http)
Request: GET /posts/2/edit
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Ecto.Queryable not implemented for %Ne
wVersion.Blog.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "blog_posts">, body:
 "Blog post 2", comments: [%NewVersion.Blog.Comment{__meta__: #
Ecto.Schema.Metadata<:loaded, "blog_comments">, blog_post: #Ecto.Association.Not
Loaded<association :blog_post is not loaded>, blog_post_id: 2, body: "third comm
ent", id: 3, inserted_at: ~N[2017-05-22 12:49:37.506000], title: "Comment 3", up
dated_at: ~N[2017-05-22 12:49:37.506000], votes: 1}, %NewVersion.Blog.Comment{__
meta__: #Ecto.Schema.Metadata<:loaded, "blog_comments">, blog_post: #Ecto.Associ
ation.NotLoaded<association :blog_post is not loaded>, blog_post_id: 2, body: "Comment for blog post 2", id: 2, inserted_at: ~N[2
017-05-22 12:14:48.590000], title: "Comment 2 for post 2", updated_at: ~N[2017-0
5-22 12:14:48.590000]}], id: 2, inserted_at: ~N[2017-05-22 12:14:47.96
2000], title: "Post 2", updated_at: ~N[2017-05-22 12:14:47.962000]}
        (ecto) lib/ecto/queryable.ex:1: Ecto.Queryable.impl_for!/1
        (ecto) lib/ecto/queryable.ex:9: Ecto.Queryable.to_query/1
        (ecto) lib/ecto/query/builder/preload.ex:154: Ecto.Query.Builder.Preload
.apply/3
        (new_version) lib/new_version/web/controllers/post_controller.ex:33: New
Version.Web.PostController.edit/2
        (new_version) lib/new_version/web/controllers/post_controller.ex:1: NewV
ersion.Web.PostController.action/2
        (new_version) lib/new_version/web/controllers/post_controller.ex:1: NewV
ersion.Web.PostController.phoenix_controller_pipeline/2
        (new_version) lib/new_version/web/endpoint.ex:1: NewVersion.Web.Endpoint
.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (new_version) lib/new_version/web/endpoint.ex:1: NewVersion.Web.Endpoint
.plug_builder_call/2
        (new_version) lib/plug/debugger.ex:123: NewVersion.Web.Endpoint."call (o
verridable 3)"/2
        (new_version) lib/new_version/web/endpoint.ex:1: NewVersion.Web.Endpoint
.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:15: Plug.Adapters.Cowboy.Hand
ler.upgrade/4
        (cowboy) d:/test/new_version/deps/cowboy/src/cowboy_protocol.erl:442: :c
owboy_protocol.execute/4

edit function from PostController:

  def edit(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    IO.inspect post
    changeset = Blog.change_post(post)
    render(conn, "edit.html", post: post, changeset: changeset)
  end
1
Are you absolutely sure this expression: Repo.get!(Post, id) |> Repo.preload(:comments) throws that error?Dogbert
Yes, there are no more changes in my code so far. Only this line, after which the error appearsAndreyKo
Can you post the complete error message including the stacktrace?Dogbert
Added stacktrace to the bottom of the question.AndreyKo
Looks like the error is triggered from New Version.Web.PostController.edit/2. Can you post the source of that function? (And also preferably mark which one is the 33rd line of post_controller.ex.)Dogbert

1 Answers

2
votes

As I've already written in the comments to my question, the initial mistake was that I was not following Ecto docs precisely and instead of the function

def get_post!(id), do: Repo.get!(Post, id) |> preload(:comments)

I should have (notice missing Repo.)

def get_post!(id), do: Repo.get!(Post, id) |> Repo.preload(:comments)

But what was worse than this is that my question didn't provide the actual code with mistake which was misleading.

The conclusion: the question is not valuable, there is no problem with preloading associations. The phoenix community support is great and novice friendly, Thank you, guys! Because of you I feel that phoenix framework is really good choice for me.

Edit:

After some thinking about how come that

def get_post!(id), do: Repo.get!(Post |> preload(:comments), id) 

worked, and

def get_post!(id), do: Repo.get!(Post, id) |> preload(:comments)

didn't, I decided to elaborate the answer for anybody's sake.

The reason is because the preload function is taken from Ecto.Query module and not from the NewVersion.Repo module (NewVersion is just the name of my project).

And this function expects to get Ecto.Queryable as a first argument. In the first case the atom Post (which is just the alias for NewVersion.Blog.Post) is provided and if you look into https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/queryable.ex you can see that the Ecto.Queryable protocol is provided for atoms. As for the second expression, the Repo.get!(Post, id) returns Post struct instead of Post atom and there is no Ecto.Queryable protocol provided for it.