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
Repo.get!(Post, id) |> Repo.preload(:comments)
throws that error? – DogbertNew Version.Web.PostController.edit/2
. Can you post the source of that function? (And also preferably mark which one is the 33rd line ofpost_controller.ex
.) – Dogbert