From 323ef31d281edef8b5e8ab9096f3a8450fa6d648 Mon Sep 17 00:00:00 2001 From: Rob Di Marco Date: Fri, 13 Apr 2018 17:07:59 -0400 Subject: [PATCH] Spec to illustrate error thrown when including fast_jsonapi When trying to use the fast_jsonapi gem, we started getting an exception in unrelated `ActiveRecord` code like: ``` Failure/Error: def #{name}_id association(:#{name}).reader.try(:id) NoMethodError: undefined method `has_scope?' for nil:NilClass # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/reflection.rb:872:in `has_scope?' # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/reflection.rb:873:in `has_scope?' # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/association.rb:288:in `skip_statement_cache?' # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/singular_association.rb:39:in `find_target' # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/association.rb:157:in `load_target' # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/association.rb:53:in `reload' # ./vendor/bundle/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/associations/singular_association.rb:7:in `reader' # ./lib/extensions/has_one.rb:15:in `city_id' ``` This error was only showing when we had required in the `fast_jsonapi` and was happening in code that was not doing JSON serialization. The reason for the error was that we were making a call to an `ActiveRecord` `select` where we were explicitly selecting the ID value for a `has_one` that was coming through another relation. The `HasOne` monkey patch in `lib/extensions/has_one.rb` is the cause of this error. The spec above illustrates the problem. If you run it, you will see the above exception. If you comment out the code in `lib/extensions/has_one.rb`, you will see the new spec passes. --- spec/lib/extensions/active_record_spec.rb | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/spec/lib/extensions/active_record_spec.rb b/spec/lib/extensions/active_record_spec.rb index e31ffe78..ac45f9ad 100644 --- a/spec/lib/extensions/active_record_spec.rb +++ b/spec/lib/extensions/active_record_spec.rb @@ -24,16 +24,50 @@ supplier_id int, FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ); + + create table cities ( + name varchar(30), + id int primary key + ); + + create table zip_codes ( + code varchar(5), + id int primary key, + primary_city_id int, + FOREIGN KEY (primary_city_id) REFERENCES cities(id) + ); + + create table locations ( + zip_code_id int, + city_id int, + id int primary key, + is_primary int, + FOREIGN KEY (city_id) REFERENCES cities(id), + FOREIGN KEY (zip_code_id) REFERENCES zip_codes(id) + ); + + create table object_with_cities ( + zip_code_id int, + id int primary key, + FOREIGN KEY (zip_code_id) REFERENCES zip_codes(id) + ); SQL # Insert records @account_id = 2 @supplier_id = 1 @supplier_id_without_account = 3 + @city_id = 1 + @zip_code_id = 1 db.execute_batch <<-SQL insert into suppliers values ('Supplier1', #{@supplier_id}), ('SupplierWithoutAccount', #{@supplier_id_without_account}); insert into accounts values ('Dollar Account', #{@account_id}, #{@supplier_id}); + + insert into cities values ('Beverly Hills', #{@city_id}); + insert into zip_codes values ('90210', #{@zip_code_id}, #{@city_id}); + insert into locations values (#{@zip_code_id}, #{@city_id}, 1, 1); + insert into object_with_cities values (#{@zip_code_id}, 1); SQL end @@ -47,12 +81,45 @@ class Account < ActiveRecord::Base belongs_to :supplier end + class Location < ActiveRecord::Base + belongs_to :zip_code + belongs_to :city + scope :primary, -> { where(is_primary: 1) } + end + + class ZipCode < ActiveRecord::Base + # has_one :primary_loc, -> { where(is_primary: 1) }, class_name: 'Location' + has_many :locations + # has_many :cities, through: :locations + end + + class City < ActiveRecord::Base + has_many :locations + has_many :zip_codes, through: :locations + end + + class ObjectWithCity < ActiveRecord::Base + belongs_to :zip_code + + has_one :primary_loc, class_name: 'Location', through: :zip_code + has_one :city, class_name: 'City', through: :primary_loc + end + ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => @db_file ) end + context 'with a belongs with a different foreign key' do + it 'can serialize the results' do + obj = ObjectWithCity.group('cities.id').select('cities.id city_id').joins(zip_code: { locations: :city }).first + + # Commenting out the code in lib/extensions/has_one.rb will make this pass + expect(obj.city_id).to be_present + end + end + context 'has one patch' do it 'has account_id method for a supplier' do