Search code examples
ruby-on-railsrubyactiverecordhas-many-throughhas-many

Is there a way of accessing records from a non persisted association?


I have the following classes:

class Post < ApplicationRecord                                                                                                                                                                                                                
  has_many :post_tags                                                                                                                                                                                                                         
  has_many :tags, through: :post_tags
end

class PostTag < ApplicationRecord                                                                                                                                                                                                             
  belongs_to :post                                                                                                                                                                                                                            
  belongs_to :tag
end

class Tag < ApplicationRecord                                                                                                                                                                                                                 
  has_many :post_tags                                                                                                                                                                                                                         
  has_many :posts, through: :post_tags
end

and the following test (rspec)

RSpec.describe Post, :model do
  let(:post) { Post.new }
  let(:post_tag) { PostTag.new tag: tag }
  let(:tag) { Tag.new }

  before do
    PostTag.destroy_all
  end

  # passes
  it 'sets tags when setting tags association' do
    post.tags << tag
    expect(post.tags).not_to be_empty
  end

  # fails
  it 'sets tags when setting post_tags association' do
    post.post_tags << post_tag
    expect(post.tags).not_to be_empty
  end
end

Is there a way to set the PostTag association (has_many) and be able to get a collection proxy of Post instances when calling posts (has_many :thorugh) association without saving the record?

more context:

I want to implement a duplicate method and I need to duplicate the associations too, the join model has extra information like post_tag.name that I don't want to lose on duplication

class Post < ApplicationRecord                                                                                                                                                                                                                
  has_many :post_tags                                                                                                                                                                                                                         
  has_many :tags, through: :post_tags

  def duplicate
    new_instance = dup
    new_instance.post_tags = post_tags.map(&:dup)
    new_instance
  end
end

I tried to do it in the way of the test that passes (setting the tags via the has_many :through) association, but that means I will lose data that is stored in the PostTags instances. Is there a way to set the post_tags and get the tags without persisting?


Solution

  • I think I can confidently say the answer is 'no'.

    it 'sets tags when setting post_tags association' do
      post.post_tags << post_tag # post_tag has no ID yet, it's not saved.
      expect(post.tags).not_to be_empty
    end
    

    post.tags relies on has_many :tags, through: :post_tags

    So the SQL of post.tags will be something like:

    SELECT "tags".* FROM "tags" INNER JOIN "post_tags" ON "tags"."id" = "post_tags"."post_id" WHERE "post_tags"."post_id" = 22
    

    This requires post_tags and tags to be records in the database that can be retrieved.

    I bet this test would pass if you saved the records first:

    RSpec.describe Post, :model do
      let(:post) { Post.new }
      let(:post_tag) { PostTag.new tag: tag }
      let(:tag) { Tag.new }
    
      it 'sets tags when setting post_tags association' do
        post_tag.save
        tag.save
    
        post.post_tags << post_tag.reload
        expect(post.tags).not_to be_empty
      end
    end
    

    But this isn't the behavior you want, I'm guessing.

    Is there an issue with multiple posts having the same post_tags?

    def duplicate
      new_instance = dup
      new_instance.post_tags = post_tags # don't make dupes .map(&:dup)
      new_instance
    end