Heisenbug in STI of rails active record - ruby-on-rails

I have this generic message model, that is supposed to be subclassed involving STI and subclasses are supposed to be attachable to different other models.
But AR's STI magic is kind of flickering - on the same request it works sometimes and sometimes not.
Rails server log shows that sometimes when Payload accesses it's reservation_messages association, the IN query part contains only some of three possible classes.
Where should i preload subclasses so they would be always visible to AR's STI?
So what i have:
# models/message.rb
class Message < ActiveRecord::Base
end
##############
# models/reservation_message.rb
class ReservationMessage < Message
belongs_to :payload, foreign_key: :record_id
end
# tried to load subclasses manually but it doesn't help
require_relative 'reservation_email'
require_relative 'reservation_comment'
#########################
# models/reservation_email.rb
class ReservationEmail < ReservationMessage
end
#########################
# models/reservation_comment.rb
class ReservationComment < ReservationMessage
end
#########################
# models/payload.rb
class Payload < ActiveRecord::Base
has_many :reservation_messages, foreign_key: :record_id
end
# tried to load subclasses manually but it doesn't help
require_relative 'reservation_email'
require_relative 'reservation_comment'
#########################
UPDATE 1
Made custom initializer, where i just required all 4 message classes.
Looks like it works for now.

Related

Processing ActiveRecord::Reflections -- some model reflections coming up empty (Rails 4.2)

Rails & ActiveRecord 4.2.1, Ruby 2.2.0
EDIT: Please note this question is mostly looking for a discussion of how the Reflections aspect of ActiveRecord works in order to better understand AR and Rails.
I am working on a Concern that looks into an ActiveRecord model's associations and creates callbacks based on those associations. Tests on individual files or examples pass. However, when testing the full suite or running the dev server, it fails because certain models return nil for reflections. The gist:
module CustomActivityMaker
extend ActiveSupport::Concern
module ClassMethods
# #param {Array[Symbols]} assns
# 'assns' params is a list of a model's associations
def create_activity_for(assns)
assns.each do |assn|
r = self.reflect_on_association(assn)
if r.is_a?(ActiveRecord::Reflection::ThroughReflection)
# For a through association, we just track the creation of the join table.
# byebug
r.source_reflection.klass # <== source_reflection unexpectedly ends up being nil
else
r.klass
end
...
end
end
end
end
Invoked like so:
# models/contact.rb
class Contact < ActiveRecord::Base
has_many :emails
has_many :addresses
has_many :phone_numbers
has_many :checklists
has_many :checklist_items, through: :checklists
create_activity_for :checklist_items
...
end
# models/checklist.rb
class Checklist < ActiveRecord::Base
belongs_to :contact
has_many :checklist_items
...
end
# models/checklist_item.rb
class ChecklistItem < ActiveRecord::Base
belongs_to :checklist
has_one :matter, through: :checklist
...
end
The error is shown in the comment note in CustomActivityMaker. When using byebug, the variable r is an ActiveRecord::Reflection::ThroughReflection. The call to source_reflection should be an ActiveRecord::Reflection::HasManyReflection, with 'klass' giving me the ChecklistItem class.
However :source_reflection comes up nil. Using byebug to inspect the error, the through class Checklist does not have any reflections:
Checklist.reflections # <== {}
Of course, this is not the result if I make an inspection in the console or when running an individual test.
I'm not understanding the Rails loading process, and how and when it builds ActiveRecord reflections, and when and how I can reliably access them. Any insight?
I couldn't find any resources to guide me through Rails' inner-workings, so instead I went to look at a popular gem that likewise needs to parse through ActiveRecord::Reflections, ActiveModel::Serializer, since it too would likely have to deal with Rails not loading things as it wished. There, I found:
included do
...
extend ActiveSupport::Autoload
autoload :Association
autoload :Reflection
autoload :SingularReflection
autoload :CollectionReflection
autoload :BelongsToReflection
autoload :HasOneReflection
autoload :HasManyReflection
end
Adding this to my concern solved my issues. From the ActiveSupport::Autoload docs: "This module allows you to define autoloads based on Rails conventions (i.e. no need to define the path it is automatically guessed based on the filename) and also define a set of constants that needs to be eager loaded".

How to make a plain ruby object assignable as active record association

I have an Audit class which is backed by ActiveRecord.
class Audit < ActiveRecord::Base
belongs_to :user, polymorphic: true
end
I have a User class which is a plain ruby object with some ActiveModel functionality included. It's not a database model because my users are actually stored in a different database and served over an API.
class User
include ActiveModel::Conversion
include ActiveModel::Validations
extend ActiveModel::Naming
def self.primary_key
'id'
end
def id
42
end
def persisted?
false
end
end
I'm trying to assign a user to an audit like this:
audit = Audit.new
audit.user = User.new
audit.save!
From a data perspective, this should work ok. To define a polymorphic association, we need to put values into two columns on the audits table. We can set audits.user_id to the value 42 and audits.user_type to the string "User".
However, I hit an exception:
undefined method `_read_attribute' for #<User:0x007f85faa49520 #attributes={"id"=>42}> (NoMethodError)
active_record/associations/belongs_to_polymorphic_association.rb:13:in `replace_keys'
I traced that back to the ActiveRecord source and it seems to be defined here. Unfortunately, the fact that it's ActiveRecord rather than ActiveModel means that I can't include that mixin into my class.
I tried defining my own _read_attribute method but I go down a rabbit hole of having to redefine more and more ActiveRecord functionality like AttributeSet and so on.
I also realise that I can workaround the problem by assigning Audit#user_type and Audit#user_id. That is unsatisfactory however because, in reality, I would have to fork a gem and edit it to do that.
How can I modify my User class so that I can cleanly assign it to an audit.
P.S. Here's a sample app so you can try this yourself.
Instead of hacking deeper and deeper to replicate ActiveRecord functionality, you may want to consider actually inheriting from ActiveRecord::Base instead of including ActiveModel. Your only constraint is that you don't have a table. There's a gem for that:
activerecord-tableless
This class works with your sample app:
require 'active_record'
require 'activerecord-tableless'
class User < ActiveRecord::Base
has_no_table
# required so ActiveRecord doesn't try to create a new associated
# User record with the audit
def new_record?
false
end
def id
42
end
end

Rails model inherit business logic only

In a rails 4 app I have two models with the same schema but different tables. They are FeedEntry and HistoricalFeedEntry. I would like HistoricalFeedEntry to only inherit functionality I add to FeedEntry. The models look like so:
class FeedEntry < ActiveRecord::Base
def self.published_at_cutoff
# date cutoff before which entries are old
Time.now - 1*7*24*60*60
end
end
class HistoricalFeedEntry < FeedEntry
end
When I enter the rails console and do HistoricalFeedEntry.all I get all the results from the FeedEntry table. What I would like is to only inherit published_at_cutoff (and other methods defined by me). Thanks.
You can use a mix-in for such a thing. Create a module which contains the business logic, class methods and instance methods. And "include" it in each of the models. Something like below:
class FeedEntry < ActiveRecord::Base
include FeedEntryBusinessLogic
end
module FeedEntryBusinessLogic
def self.published_at_cutoff
# date cutoff before which entries are old
Time.now - 1*7*24*60*60
end
end
class HistoricalFeedEntry < ActiveRecord::Base
include FeedEntryBusinessLogic
end
Because you are using Rails 4, you can use Concerns ( which are similar ). Read:
https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns

Rails autoloading and gem classes

So I've got a gem which contains an ActiveRecord Model
# one of 30 or so models used by a suite of applications
class MyModel < ActiveRecord::Base
# some library code
end
# in Rails app, I want to add some behavior to this model using good old ruby class reopening
class MyModel < ActiveRecord::Base
scope :active, { where active: true }
end
The problem is that my app-native version of this model is only autoloaded thanks to Rails. Since it was loaded by the library and the constant is registered, it will never go to the local model to add the locally defined behavior.
Short of having an initializer with a hard-coded list of requires to the local version of the model, how might I get rails to marry the two definitions of the class to end up with one class that has all the behavior?
The simplest solution would be to:
In your gem, change the model name to something more generic (MyModelTemplate) and make it abstract:
class MyModelTemplate < ActiveRecord::Base
self.abstract_class = true
# some library code
end
Make your real model inherit MyModelTemplate:
class MyModel < MyModelTemplate
end

set_table_name doesn't work if model is in a deep namespace - bug in rails 2.3.14?

I'm using Rails 2.3.14
UPDATE 3 the trick I found in update 2 only works for the first access of the association... so, I stick the set_table_name lines in my application controller. This is so weird. I hope this gets fixed. =\
UPDATE 2 if I manually / nastily / hackily set teh tables for the troublesome classes at the bottom of my environment file, I stop getting the errors.
app/config/environment.rb:
Page::Template::Content.set_table_name "template_page_contents"
Page::Template::Section.set_table_name "template_page_sections"
Why do I have to do this? is rails broken for this particular use case?
UPDATE: it appears that set_table_name isn't working when it's initially called in my Page::Template::Content class.
but if I call it right before I use the association, it works...
ap Page::Template::Content.table_name # prints "contents"
Page::Template::Content.set_table_name "template_page_contents"
ap Page::Template::Content.table_name # prints "template_page_contents"
return self.page_template_contents.first # doesn't error after the above is exec'd
ORIGINAL QUESTION:
TL; DR: I have a has_many relationship that I'm trying to access, but Rails thinks that the table that the has_many relationship uses is a different table
I've been getting this error, where when I try to access a Page::Template::Content that belongs_to a Page::Template via a has_many relationship.
Mysql2::Error: Unknown column 'contents.template_page_id' in 'where clause': SELECT * FROM `contents` WHERE (`contents`.template_page_id = 17) LIMIT 1
Looking at the error logs, I figured I needed to starting using some print statements to find out why rails was trying to find the associated objects in the wrong table.
gems_path/activerecord-2.3.14/lib/active_record/associations/association_collection.rb:63:in `find'
I just decided to print the #reflection object since everything seems to be happening around that. Here is how I did that:
require "awesome_print" # best console printer (colors, pretty print, etc)
def find(*args) # preexisting method header
ap "-------" # separator so I know when one reflection begins / ends, etc
ap #reflection # object in question
... # rest of the method
Last '#reflection' that printed before the error:
"-------"
#<ActiveRecord::Reflection::AssociationReflection:0x108d028a8
#collection = true,
attr_reader :active_record = class Page::Template < LibraryItem {...},
attr_reader :class_name = "Page::Template::Content",
attr_reader :klass = class Content < LibraryItem {...},
attr_reader :macro = :has_many,
attr_reader :name = :page_template_contents,
attr_reader :options = {
:foreign_key => "template_page_id",
:class_name => "Page::Template::Content",
:extend => []
},
attr_reader :primary_key_name = "template_page_id",
attr_reader :quoted_table_name = "`contents`"
>
there are a couple things wrong in the above block of code.
:klass should be Page::Template::Content
:name should be :contents
:quoted_table_name should be `contents`
How my models are set up:
app/models/page.rb:
class Page < LibrayItem
belongs_to :template_page, :class_name => "Page::Template"
app/models/page/template.rb
class Page::Template < Library Item
set_table_name "template_pages"
has_many :page_template_contents,
:class_name => "Page::Template::Content",
:foreign_key => "template_page_id"
app/models/page/template/content.rb
class Page::Template::Content
set_table_name "template_page_contents"
belongs_to :template_page,
:class_name => "Page::Template",
:foreign_key => "template_page_id"
class Page::Template
...
return self.page_template_contents.first
the class the association is choosing (unrelated to my page / template structure above):
class Content < LibraryItem
set_table_name "contents"
# no associations to above classes
So... what is causing this and how do I fix it?
Your issue is not due to set_table_name but rather due to how Rails finds the target model class in an association and the fact that you have a top-level model (Content) which shares its name with a model nested in a deeper namespace (Page::Template::Content). The weird difference in behaviour is most likely due to differences in what classes are actually loaded at the time the association is examined (remember that by default in development mode Rails loads model classes on demand when they are first referenced).
When you define an association (whether you specify the class name of the target model of the association explicitly as you have done or accept the default), Rails has to turn the name of the target model class into a Ruby constant so that it can refer to that target class. To do this, it invokes the protected class method compute_type on the model class that is defining the association.
For example, you have
class Page::Template < LibraryItem
set_table_name "template_pages"
has_many :page_template_contents,
:class_name => "Page::Template::Content",
:foreign_key => "template_page_id"
end
You can test in the Rails console how compute_type works for this class:
$ ./script/console
Loading development environment (Rails 2.3.14)
> Page::Template.send(:compute_type, "Page::Template::Content")
=> Page::Template::Content(id: integer, template_page_id: integer)
I see the result is, as I expect, a reference to the class Page::Template::Content.
However, if I restart the Rails console but this time cause the model class Content to be loaded first (by referencing it) and then try again, I see this (I didn't bother creating a table for the Content model, but that doesn't change the significant behavior):
$ ./script/console
Loading development environment (Rails 2.3.14)
>> Content # Reference Content class so that it gets loaded
=> Content(Table doesn't exist)
>> Page::Template.send(:compute_type, "Page::Template::Content")
=> Content(Table doesn't exist)
As you can see, this time I get a reference to Content instead.
So what can you do about this?
Well, first of all, you should realize that using namespaced model classes in Rails (certainly in 2.3) is a real pain. It's nice to organize your model classes in a hierarchy but it certainly does make life more difficult. As you can see above, if you have a class in one namespace that has the same name as a class in another namespace, this gets more hairy.
If you still want to live with this situation, I can make a couple of suggestions. One of the following may help:
Turn on class caching in development mode. This will cause all model classes to be preloaded rather than loading them on demand as is the default. It will make your code above more predictable, but may make development somewhat unpleasant (since classes will no longer be reloaded with each request).
Explicity require dependent classes in classes with problematic associations. For example:
class Page::Template < LibraryItem
require 'page/template/content'
set_table_name "template_pages"
has_many :page_template_contents, ...
end
Override the compute_type method in your classes where you know referencing associated class may go awry so that it returns the namespaced class no matter what. Without knowing your class hierachy in more detail I cannot give a full suggestion on how to do this, but a quick and dirty example follows:
class Page::Template < LibraryItem
set_table_name "template_pages"
has_many :page_template_contents,
:class_name => "Page::Template::Content",
:foreign_key => "template_page_id"
def self.compute_type(type_name)
return Page::Template::Content if type_name == "Page::Template::Content"
super
end
end

Resources