ruby-hyperloop / hyper-mesh

The project has moved to Hyperstack!! - Synchronization of active record models across multiple clients using Pusher, ActionCable, or Polling
https://hyperstack.org/
MIT License
22 stars 12 forks source link

active_record finders ignores STI class #39

Open sfcgeorge opened 7 years ago

sfcgeorge commented 7 years ago

If single table inheritance is used on the back end, hyper-mesh won't instantiate the correct subclass, only ever the parent class.

# field.string :type
class User < ActiveRecord
  def self.meaningful
    find(42)
  end
end

class Employer < User
end

Employer.create.id # 42
Employer.meaningful.class #=> User (should be Employer)
User.meaningful.class #=> User (should also be Employer!)

The AR method becomes should also be added to allow switching class:

User.find.becomes(User)
Employer.find.becomes(User)

Basically Rails always automatically converts into the most specific STI sub-class.

Fairly sure this will fix #38 too

sfcgeorge commented 7 years ago

I've found 2 places in the code where the class is changing to the superclass.

Wrong self in finders

https://github.com/ruby-hyperloop/hyper-mesh/blob/5e811858548f31a73a33e3e856e65c97b57c20e8/lib/reactive_record/active_record/class_methods.rb#L45 Instance eval changes self so you're not passing in the self you might think. Easy fix:

def find(id)
  base_class.instance_eval {ReactiveRecord::Base.find(self, primary_key, id)}
end

base_class used unnecessarily

https://github.com/ruby-hyperloop/hyper-mesh/blob/5e811858548f31a73a33e3e856e65c97b57c20e8/lib/reactive_record/active_record/class_methods.rb#L5-L18

I assume there's a reason why base_class is used all over the place. But it does what it says and gives you the superclass so you loose access to the methods in the actual model you're trying to use. I don't see why base_class is necessary at all, as with inheritance all the superclass methods are available in the child. And if you're able to call base_class then you must have inherited from Base so there's no point checking this condition.

Maybe there's another kind of AR internals object that you can end up with where it's necessary to call superclass to get something useful? But checking for those edge cases might be better than always calling base_class.

Redefining base class to return self seems to work fine. Async loading and saving on the client works in STI models.

Alternative - remove calls to base_class

https://github.com/ruby-hyperloop/hyper-mesh/blob/5e811858548f31a73a33e3e856e65c97b57c20e8/lib/reactive_record/active_record/reactive_record/base.rb#L78

Commenting out model = model.base_class has the same effect as redefining it. So if there are places when base_class is actually needed to do it's thing then they could be left in and everywhere else removed.

Alternative - use always assignment

https://github.com/ruby-hyperloop/hyper-mesh/blob/5e811858548f31a73a33e3e856e65c97b57c20e8/lib/reactive_record/active_record/reactive_record/base.rb#L97

Without the base_class fix the model is initially memoized as User and then the server correctly returns Employer but it's never updated. Changing this to a regular assignment updates the class correctly but doesn't help with the initial page load and is probably undesirable for caching reasons.

TLDR monkeypatch

The below patch got STI models working correctly for me. The model starts of as Employer and stays that way. So far it doesn't seem to have broken anything else but probably has as I don't know what the reason behind base_class really is.

if RUBY_ENGINE == "opal"
  module ActiveRecord
    module ClassMethods
      def base_class
        unless self < Base
          raise ActiveRecordError, "#{name} doesn't descend from ActiveRecord"
        end

        if superclass == Base || superclass.abstract_class?
          self
        elsif respond_to?(:each)
          superclass.base_class
        else
          self
        end
      end

      def find(id)
        clss = self # instance_eval changes self
        base_class.instance_eval do
          ReactiveRecord::Base.find(clss, primary_key, id)
        end
      end
    end
  end
end