How to add subsequent children to parent via many-to-many

I have a many-to-many relationship between Newsletter and Subscriber schemas that are joined through a Subscription schema, which is defined as follows:

schema "subscriptions" do
    belongs_to :newsletter, Newsletter
    belongs_to :subscriber, Subscriber

    timestamps()
  end

I want to be able to add multiple Subscribers to a Newsletter and I’m running into some difficulties.

First, I tried to use put_assoc/3 but this requires on_replace to be set and I’m hesitate to use :delete while adding the new Subscriber to the preloaded ones on the Newsletter to the Changeset as the number of subscribers could grow quite large and it doesn’t feel right to add all the existing ones to the changeset.

build_assoc/3 on the other hand won’t work if I have an existing Subscriber (a Subscriber can subscribe to more than one Newsletter).

So, I’m left with doing the following:

%Subscription{newsletter_id: newsletter.id, subscriber_id: subscriber.id}
|> Repo.insert()

This isn’t the end of the world, but I was wondering if there is a better way to handle this? Is it normal to use put_assoc/3 and pass the existing children to the changeset in this instance?

This is only true if you are going to update the association at any point. If a subscription can’t have its subscriber changed, then there is no need to explicitly set on_replace since its default is :raise. I would make a dedicated create_changeset that put_assocs the assocations. You could get away with setting on_replace: :update and ensure you always pass the same newsletter and subscriber, but I wouldn’t recommend this if the relationship is supposed to be immutable.

I was previously trying to use put_assoc/3 with the Newsletter struct, but if I’m understanding you correctly, you’re saying that I should instead use it twice on the Subscription struct?

So, basically, this?

# Subscription.ex

def changeset(newsletter, subscriber) do
    %Subscription{}
    |> change()
    |> put_assoc(:newsletter, newsletter)
    |> put_assoc(:subscriber, subscriber)
    |> unique_constraint([:newsletter_id, :subscriber_id],
      message: "Subscriber is already subscribed to newsletter"
    )
end

Which, I can then call from my Newsletters context:

@doc """
  Subscribe an existing subscriber to a newsletter.
  """
  def subscribe_to_newsletter(newsletter, subscriber) do
    Subscription.changeset(newsletter, subscriber)
    |> Repo.insert()
  end

Yes, that is what I meant! I thought that’s what you were talking about!

I’ve never actually used put_assoc going from parent to child before, though I can certainly imagine it being useful if you have a set of small number of children that always get updated together. But in your particular case I always just use put_assoc on the child. What you have with setting the ids is effectively the same thing! You could even do %Subscription{newsletter: newsletter, subscriber: subscriber} if you want it a little terser. I usually just use put_assoc though and my create changesets look exactly like yours :slight_smile:

1 Like

On thing, though, if you’re going to just call your create changeset changeset, you’re going to need to pattern match newsletter and subscriber or else you’ll likely have multiple confusing changeset/2 functions. I always just call it create_changeset (even though I don’t love that name but it’s stuck).

1 Like

Good idea. Updating the function signature to be the following and putting it here for anyone who stumbles upon this thread:

def changeset(%Newsletter{} = newsletter, %Subscriber{} = subscriber), do: ...
1 Like

Well, I upvoted but I still don’t recommend this, lol. Just even from a docs point of view, changeset/2 is very overloaded. Doing it this way gives zero signal that it is only meant to be used for creating a record. Totally up to you, of course!

1 Like

This phrasing definitely makes it sound like you’d want to use put_assoc on a Newsletter changeset, but what about this alternate form:

I want to be able to add multiple Subscriptions to a Newsletter

This is closer to what really happens (a row is added to subscriptions) and makes it clearer where extra parameters like “subscription length” etc could go in the future.


If newsletter and subscriber are both already-valid, then you likely don’t need to use a changeset for this. The unique_constraint could be useful, but it depends on how you use the resulting error to give the user feedback. If there’s no way to give the user feedback about a duplicated subscription, it may even be preferable to crash instead of silently failing.

1 Like

I’ve still not yet come up with a consistent naming convention for my changesets for this project, so this is a good opportunity to revisit that. I suppose going with {some_action}_changeset as you suggested is a good way forward. Thank you.

1 Like