How to create new associations if they do not exist, and ignore if they already do?

Hi Friends,

I have been struggling with this one.

My question is closely related to adding tags to a post section of put_assoc docs.

But, the section assumes that all tags already exist in the DB, while in my case, some or all tags may be new tags, which I want to create afresh in the DB while storing posts (articles in my schema).

Here is the query that I currently have:

  def create_article(%{"tagList" => tagList} = attrs \\ %{}, current_user) do
    Ecto.build_assoc(current_user, :articles)
    |> IO.inspect()
    |> Article.changeset(attrs)
    |> IO.inspect()
    # |> Repo.preload([:tags])
    |> IO.inspect()
    |> Ecto.Changeset.put_assoc(:tags, tags)
    |> Repo.insert()
end

The above query works fine if all the tags in the tagList are new, but it does not if a tag is re-used.

Also, if I comment out Repo.preload([:tags]) line it throws the error below:

(UndefinedFunctionError) function Ecto.Changeset.__schema__/2 is undefined or private
        (ecto 3.7.1) Ecto.Changeset.__schema__(:association, :tags)
        (elixir 1.12.0) lib/enum.ex:2356: Enum."-reduce/3-lists^foldl/2-0-"/3

Any help appreciated.

Here’s my schema:

Article Schema

defmodule ConduitElixir.Articles.Article do
  use Ecto.Schema
  import Ecto.Changeset

  alias ConduitElixir.Tags.Tag
  alias ConduitElixir.Tags.ArticleTag
  alias ConduitElixir.Auth.User

  schema "articles" do
    field :body, :string
    field :description, :string
    field :title, :string

    many_to_many :tags, Tag, join_through: ArticleTag 
    belongs_to :user, User

    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :body, :description])
    |> validate_required([:title, :body])
  end
end

User Schema

defmodule ConduitElixir.Auth.User do
  use Ecto.Schema
  import Ecto.Changeset

  alias ConduitElixir.Articles.Article

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true
    field :hashed_password, :string
    field :username, :string
    field :bio, :string

    has_many :articles, Article

    timestamps()
  end

Tags schema

defmodule ConduitElixir.Tags.Tag do
  use Ecto.Schema
  import Ecto.Changeset

  alias ConduitElixir.Tags.ArticleTag
  alias ConduitElixir.Articles.Article

  schema "tags" do
    field :title, :string

    many_to_many :articles, Article, join_through: ArticleTag

    timestamps()
  end

  @doc false
  def changeset(tag, attrs) do
    tag
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end
defmodule ConduitElixir.Tags.ArticleTag do
  use Ecto.Schema
  import Ecto.Changeset

  alias ConduitElixir.Tags.Tag
  alias ConduitElixir.Articles.Article

  schema "article_tags" do
    belongs_to :tag, Tag
    belongs_to :article, Article 
  end

  @doc false
  def changeset(tag, attrs) do
    tag
    |> cast(attrs, [:article_id, :tag_id])
    |> validate_required([:article_id, :tag_id])
    |> unique_constraint([:article_id, :tag_id])
  end
end



Have you tried Ecto. Multi?

There’s an example of implementing article tags exactly like this in the free Ecto cookbook, under “Upserts and insert_all”: The Little Ecto Cookbook (self-published) (free)

4 Likes

@dom I guess much more than an answer, I was looking for a resource to learn more about Ecto, so thank you so much for sharing a link to the book. I will study it now.

1 Like