When updated a table the associated table is removed

ecto
phoenix
associations

#1

Hello,

Why is that my update function nillify the association entity.

I have two entities: Post and User

So create function controller is like this:

def create(conn, %{"post" => post_params}) do
  current_user = get_session(conn, :current_user)
  post_params = Map.merge(%{"user" => current_user}, post_params)
  case Blog.create_post(post_params) do
    {:ok, post} ->
      conn
       |> put_flash(:info, "Post created successfully.")
       |> redirect(to: Routes.post_path(conn, :show, post))
    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

which associates the user to the post.

Post schema

def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :content, :is_published])
    |> validate_required([:title, :content])
    |> put_slug()
    |> put_assoc(:user, attrs["user"])
end

My problem is when updating a post the user associated with the post gets nillify:

UPDATE "posts" SET "content" = $1, "user_id" = $2, "updated_at" = $3 WHERE "id" = $4 ["Loading...", nil, ~N[2018-12-02 13:35:59], 4]

controller:

def update(conn, %{"id" => id, "post" => post_params}) do
    post = Blog.get_post!(id)
      
    case Blog.update_post(post, post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post updated successfully.")
        |> redirect(to: Routes.post_path(conn, :show, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", post: post, changeset: changeset)
    end
  end

My question is how to retain the user associated to the post?

Should I write a new params that Map.merge the post_params and the current_user which is the same as I did in the create function?

Please ask clarification if my explanation is not clear. Thank you.


#2

Fom Ecto Changeset documentation nil value can be used to remove association with put_assoc…

A better way is to remove put_assoc from your changeset…

def changeset(post, attrs) do
    post
    |> cast(attrs, [:title, :content, :is_published])
    |> validate_required([:title, :content])
    |> put_slug()
end

and put this in your context.

  def change_post(user, post) do
    post
    |> Post.changeset(%{})
    |> put_assoc(:user, user)
  end

Then use change_post inside the new action, and update_post with the update action. change_post will use put_assoc, but not update_post.


#3

Hi,
Thank you it works, but I have a question regarding a changeset with an association.

So my question is everytime I used the change_post function I will always supply it with the current_user struct. Is there a way to just passed an Post struct in the function?

Kinda confused about it.

  def index(conn, _params) do
    posts = Blog.list_posts()
    render(conn, "index.html", posts: posts)
  end

  def new(conn, _params) do
    current_user = get_session(conn, :current_user)
    changeset = Blog.change_post(current_user, %Post{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"post" => post_params}) do
    current_user = get_session(conn, :current_user)
    post_params = Map.merge(%{"user" => current_user}, post_params)
    case Blog.create_post(post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post created successfully.")
        |> redirect(to: Routes.post_path(conn, :show, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do

    post = Blog.get_post!(id)
           Blog.increment(post)
    render(conn, "show.html", post: post)
  end

  def edit(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    current_user = get_session(conn, :current_user)
    changeset = Blog.change_post(current_user, post)
    render(conn, "edit.html", post: post, changeset: changeset)
  end

  def update(conn, %{"id" => id, "post" => post_params}) do
    post = Blog.get_post!(id)
    current_user = get_session(conn, :current_user)
    post_params = Map.merge(%{"user" => current_user}, post_params)
    case Blog.update_post(post, post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post updated successfully.")
        |> redirect(to: Routes.post_path(conn, :show, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", post: post, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    {:ok, _post} = Blog.delete_post(post)

    conn
    |> put_flash(:info, "Post deleted successfully.")
    |> redirect(to: Routes.post_path(conn, :index))
  end

#4

It is more explicit to pass the current_user with the change_post function. The DB part has no idea what a session is, it would be hard to get the current_user from this context.


#5

Thanks, I would apply this way instead.


#6

Just a follow-up question as I encountered an issue on the create_action.

Why is that using this code will not saved the associated user for the post

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

meanwhile this code works with I explicitly defined the put_assoc inside the create_post function:

def create_post(attrs \\ %{}) do
    %Post{}
    |> Post.changeset(attrs)
    |> put_assoc(:user, attrs["user"])
    |> Repo.insert()
end

I’m just wondering why the first create_post function doesn’t associate the user to the post when saving the post to the database. I used the change_post function meaning it should already been associated but I got nil instead.


#7

I forgot to mention I use the same principle for create.

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

#8

I see. thanks again.

I see put_user in your function instead of put_assoc, are you the one who create the put_user function? @kokolegorille


#9

Yes, but it’s just a call to put_assoc…