https://avo.cool logo
I'm hitting a wall with a HABTM relation
# general
m
I'm hitting a wall with a HABTM relation. Granted, it's a pretty "complicated" one because it's between namespaced models. But Avo raises an error that makes little sense to me: > Avo relies on the 'inverse_of' option to establish the inverse association and perform some specific logic. > Please configure the 'inverse_of' option for the 'has_and_belongs_to_many :surveys_question_options' association in the 'Surveys::Response' model. but it's there:
Copy code
rb
class Surveys::Response < ApplicationRecord
  belongs_to :user
  belongs_to :surveys_question, class_name: "Surveys::Question"

  has_and_belongs_to_many :surveys_question_options,
                          inverse_of: :surveys_response,
                          class_name: "Surveys::QuestionOption",
                          foreign_key: :surveys_response_id,
                          association_foreign_key: :surveys_question_option_id
end
Copy code
rb
class Avo::Resources::SurveysSingleChoiceResponse < Avo::BaseResource
  # self.includes = []
  # self.attachments = []
  self.model_class = ::Surveys::SingleChoiceResponse
  # self.search = {
  #   query: -> { query.ransack(id_eq: params[:q], m: "or").result(distinct: false) }
  # }

  def fields
    field :id, as: :id
    field :user, as: :belongs_to, readonly: true
    field :surveys_question, as: :belongs_to, readonly: true
    field :surveys_question_options, as: :has_and_belongs_to_many, readonly: true
  end
end
l
Hi @millions-exabyte-27721
This error still raising after cinfiguring
inverse_of
?
How does the inverse associations looks on
Surveys::QuestionOption
?
m
yes... the offending line is in
Avo::AssociationsController
Copy code
rb
    def set_reflection
      @reflection = @record.class.reflect_on_association(association_from_params)

      # Ensure inverse_of is present on STI
      if !@record.class.descends_from_active_record? && @reflection.inverse_of.blank? && Rails.env.development?
        raise "Avo relies on the 'inverse_of' option to establish the inverse association and perform some specific logic.\n" \
          "Please configure the 'inverse_of' option for the '#{@reflection.macro} :#{@reflection.name}' association " \
          "in the '#{@reflection.active_record.name}' model."
      end
    end
it even says STI šŸ˜„
give me a second
I'm going to set a breakpoint there
look:
Copy code
irb(#<Avo::AssociationsController...):001> @reflection
=>     |
web    | #<ActiveRecord::Reflection::HasAndBelongsToManyReflection:0x00000003018de9c0
web    |  @active_record=Surveys::Response(id: integer, user_id: integer, surveys_question_id: integer, type: string, answer: text, created_at: datetime, updated_at: datetime),
web    |  @association_foreign_key=nil,
web    |  @association_primary_key=nil,
web    |  @class_name="Surveys::QuestionOption",
web    |  @counter_cache_column=nil,
web    |  @foreign_key=nil,
web    |  @inverse_of=nil,
web    |  @inverse_which_updates_counter_cache=nil,
web    |  @inverse_which_updates_counter_cache_defined=false,
web    |  @join_table=nil,
web    |  @klass=Surveys::QuestionOption(id: integer, answer: string, surveys_question_id: integer, created_at: datetime, updated_at: datetime, position: integer),
web    |  @name=:surveys_question_options,
web    |  @options=
web    |   {:inverse_of=>:surveys_response, :class_name=>"Surveys::QuestionOption", :foreign_key=>:surveys_response_id, :association_foreign_key=>:surveys_question_option_id},
web    |  @plural_name="surveys_question_options",
web    |  @scope=nil>
what's interesting is that neither the foreign_key, inverse_of, or association_foreign_key instance variables are set
l
Could you please share the
Surveys::SingleChoiceResponse
model associations?
m
but they're in the options
l
True, what rails version are you using?
m
8.0.1
those just inherit from `Survey::Response`:
Copy code
rb
class Surveys::Response < ApplicationRecord
  belongs_to :user
  belongs_to :surveys_question, class_name: "Surveys::Question"

  has_and_belongs_to_many :surveys_question_options,
                          inverse_of: :surveys_response,
                          class_name: "Surveys::QuestionOption",
                          foreign_key: :surveys_response_id,
                          association_foreign_key: :surveys_question_option_id
end
l
Got it
so is STI since
Surveys::SingleChoiceResponse
inherit from
Surveys::Response
m
yep
l
And how is configured on the
Surveys::QuestionOption
=
m
FWIW, the assocation seems to be set up alright:
Copy code
rb
Surveys::SingleChoiceResponse.first.surveys_question_options
  Surveys::SingleChoiceResponse Load (0.4ms)  SELECT "surveys_responses".* FROM "surveys_responses" WHERE "surveys_responses"."type" = 'Surveys::SingleChoiceResponse' ORDER BY "surveys_responses"."id" ASC LIMIT 1 /*application='FsgWebportal'*/
  Surveys::QuestionOption Load (1.7ms)  SELECT "surveys_question_options".* FROM "surveys_question_options" INNER JOIN "surveys_question_options_responses" ON "surveys_question_options"."id" = "surveys_question_options_responses"."surveys_question_option_id" WHERE "surveys_question_options_responses"."surveys_response_id" = 5 /* loading for pp */ LIMIT 11 /*application='FsgWebportal'*/
=>
[#<Surveys::QuestionOption:0x00000001256171e8
  id: 7,
  answer: "ƖVP",
  surveys_question_id: 12,
  created_at: "2025-03-03 10:06:22.495005000 +0000",
  updated_at: "2025-03-03 10:06:22.495005000 +0000",
  position: 2>]
Copy code
rb
class Surveys::QuestionOption < ApplicationRecord
  belongs_to :surveys_question, class_name: "Surveys::Question"
  has_and_belongs_to_many :surveys_responses,
                          inverse_of: :surveys_question_option,
                          class_name: "Surveys::Response",
                          foreign_key: :surveys_question_option_id,
                          association_foreign_key: :surveys_response_id

  acts_as_list scope: :surveys_question
end
that also gives me a correct collection:
Copy code
rb
Surveys::QuestionOption.find(7).surveys_responses
  Surveys::QuestionOption Load (11.5ms)  SELECT "surveys_question_options".* FROM "surveys_question_options" WHERE "surveys_question_options"."id" = 7 LIMIT 1 /*application='FsgWebportal'*/
  Surveys::Response Load (0.6ms)  SELECT "surveys_responses".* FROM "surveys_responses" INNER JOIN "surveys_question_options_responses" ON "surveys_responses"."id" = "surveys_question_options_responses"."surveys_response_id" WHERE "surveys_question_options_responses"."surveys_question_option_id" = 7 /* loading for pp */ LIMIT 11 /*application='FsgWebportal'*/
=>
[#<Surveys::SingleChoiceResponse:0x00000001277f8488
  id: 5,
  user_id: 36,
  surveys_question_id: 12,
  type: "Surveys::SingleChoiceResponse",
  answer: nil,
  created_at: "2025-03-03 12:15:17.986980000 +0000",
  updated_at: "2025-03-03 12:15:17.986980000 +0000">]
l
Hmmm, I think the inverse_of is not correct
I think you need to configure those as plural
and match exaclty the inverse association name
Copy code
has_and_belongs_to_many :surveys_responses, inverse_of: :surveys_question_options
instead
Copy code
has_and_belongs_to_many :surveys_responses, inverse_of: :surveys_question_option
and the same for the other class
m
ah!
l
Copy code
has_and_belongs_to_many :surveys_question_options,
                          inverse_of: :surveys_responses
Instead
Copy code
has_and_belongs_to_many :surveys_question_options,
                          inverse_of: :surveys_response
m
you're my hero.
in my defense,
inverse_of
for HABTM seems to be largely undocumented
l

https://www.youtube.com/watch?v=bWcASV2sey0ā–¾

m
anyhow, that works šŸŽ‰
l
AWESOME!
m
phew šŸ˜…
l
šŸ˜„
m
bring the visinata!
l
Maybe we should add a sentence like: "If inverse_of is already configured, ensure it is set correctly." Do you think it would have made a difference?
m
well, it would be valuable if it contained a hint what "correctly" is. I know it's none of Avo's business, but you could even say that "ensure it uses the plural form when ..."
l
> I know it's none of Avo's business We want to improve the development experience through this kind of helpers
Thanks for brainstorming this with me
I'll give it a try and improve that message
m
perfect!
to be fair, using STI in combination with namespaced models, I'm to blame for shooting myself in the foot šŸ˜„
l
There are some nested levels of complexity there šŸ˜„