0

I want to override the #build method for an ActiveRecord::Associations::CollectionProxy. I'm trying to write this custom #build method from within the Note class so that when note.topics.build is called it will run custom code. My overarching goals is that when I execute Note.new(topics_attributes), the custom build method is called. From my understanding, I need this to ensure that (a) associated objects(topics) with the same name don't get created and (b)if a topic does have the same name as another existing topic, the existing topic is retrieved. I can get (a) with the validates :name, uniqueness: true macro, but I don't think I can get (a) and (b) with existing macros (please correct me if that is wrong).

Here is my best attempt so far:

class Note < ApplicationRecord
  has_and_belongs_to_many :topics do 
    def build(attributes = {}, &block)
      super
      attributes.values.each do |topic_name| 
        if topic_name.present?
          new_topic = Topic.find_by(name: topic_name) || Topic.new(name: topic_name)
          if self.persisted?
            new_topic.notes << self unless self.topics.pluck(:id).include?(new_topic.id)
          else
            self.topics << new_topic
          end
        end
      end
    end
  end

This is more or less the logic I want. The problem is that self is referring to the Topic::ActiveRecord::Associations::CollectionProxy object, but I want it to refer to the instance of Note that .topics.build will be called on.

Any thoughts on how to get some variable to reference the Note instance? Or perhaps, there's a better strategy to get this to work?

The other strategy I tried was to write the custom method as a instance method for the Note class. Trying write a method def topics.build through an error along the line "topics method note defined for Note class".

1 Answers1

0

I ended up finding the answer here: Rails: Overriding ActiveRecord association method

But I still wanted to post because I searched a lot before I was able to find that answer and it's buried as the 3rd answer to that question, and it's an answer to a question that wasn't exactly asked in that post.

The key is using proxy_association.owner to reference the model that is being associated with. So in my case I would insert something like

note_instance = proxy_association.owner

in the code above, and then replace self with note_instance.

  • This looks pretty interesting. Could you say a little more (in your original question, perhaps) about your goal in overriding the `build` method? – jvillian Aug 25 '20 at 15:26
  • jvillian I added some more details to the original post: "I need this to ensure that (a) associated objects(topics) with the same name don't get created and (b) if a topic does have the same name as another existing topic, the existing topic is retrieved. I can get (a) with the `validates :name, uniqueness: true` macro, but I don't think I can get (a) and (b) with existing macros." Does that help explain? If my assumptions are wrong, and I can achieve these goals with existing macros, I'm also interested in that as a solution. – Charles Wisoff Aug 26 '20 at 16:19
  • So, something similar to `find_or_initialize_by`, I guess? I'm not saying that would be useful here. It just seems similar. Interesting... – jvillian Aug 26 '20 at 16:23
  • yea, exactly. I didn't know `find_or_initialize_by` was a thing I could do. That would replace the code `Topic.find_by(name: topic_name) || Topic.new(name: topic_name)` in the original question. – Charles Wisoff Aug 27 '20 at 20:59