How to conditionally include association definitions? - ruby-on-rails

I'm able to get the behavior I'd like if I create a Foo class, some STI classes that inherit it (Foo1, Foo2, etc.) and then to add different has_many :bars associations to each class. For example:
class Foo < ApplicationRecord
has_many :bars
end
class Foo1 < Foo
has_many :bars, -> { where(user_id: 1) }
end
I want to be able to call bars on an object and get a different association behavior depending on the state of the object. This works, but is there a way to do it without setting up STI?
I tried doing everything inside foo.rb but I seem to be loading my first has_many :bars definition even if I do something like this:
has_many :bars ... if some_method_returning_boolean?
has_many :bars ... if some_other_method_returning_boolean?
And even if this did work, it seems kind of clunky.
I also considered scopes, but as far as I understand scopes then I'd have to call foo.bars.something and foo.bars.something_else instead of relying on the state of the object foo to give me foo.bars differently. Also, it looks like scopes only cover part of a has_many definition but can't modify arguments like :source, :foreign_key, but I may be wrong.
Is there another way?

STI is the correct way of doing this. It's generally understood that any particular model class should have well-defined and consistent relationships with other records.
If you have relationships that only apply to certain kinds of records that's exactly what STI is for: Create a subclass that defines these relationships.
You see this all the time. An example would be that both "Parent" and "Student" are of base type "Person", but that the students have a belongs_to: parent association and the parents have a has_many: children relationship.
Of course this presumes parents are unlikely to become students, and students parents, an assumption that may not be correct. Whatever assumptions you make, I hope there's some serious thinking about if these restrictions are relevant or just overly paranoid.
It's generally bad form to switch types on a record even though it can technically be done. You could use this to adjust how a record's relationships are defined if you think this is strictly necessary.
Generally I'd advise you to stick with a consistent relationship structure. Those records which should not be related to anything don't need those methods physically removed, they can be there while not doing anything useful. They come along for free anyway.

Maybe you will be satisfacted by overwrite your bars method in this way ?
This super just return usual result from something_foo.bars.
class Foo
def bars
super.where(something: something_value) if your_condition
super.where(something_else: something_value) if other_condition
end
end

Related

A simple way to do inheritance in rails with relations

In Rails 4.2 I need some kind of inheritance for this structure:
User has many Reports, Report belongs to User
ImpactReport is a Report
ChallengeReport is a Report
PlanningReport is a Report
StImpactReport is an ImpactReport
I've read about single-table inheritance supported by Rails, but I don't think it would let me go two levels deep on inheritance, also StImpactReport has a bunch of fields that are not in ImpactReport
I've also seen a lot of articles about how to implement muti-table inheritance, but they all seem quite complex, when in my head there's a much simpler way to do it.
I think it could be done simply with the belongs_to relationship in conjunction with delegate and accepts_nested_attributes_for like this:
class Report < ActiveRecord::Base
end
class ImpactReport < ActiveRecord::Base
belongs_to :report
accepts_nested_attributes_for :report
delegate :user, to: :report
delegate :other_report_field, to: :report
end
and so on...
The advantages of this are:
I have separate tables for each model, but only the fields specific to that model are in their tables, common fields are in the table of the parent.
I can go as many levels deep as I like
I can have, say, an impact_report and a challenge_report that both share the same report attributes. When the attribute is changed in one the other is also changed. (this is good for my app)
Disadvantages:
It doesn't use class inheritance so methods of parents are not accessible to the children
My questions are these:
Are there any other disadvantages to this way of doing inheritance that I haven't thought of? Will it limit my application in some unforeseen way?
If I have a report object, what is the best way for me to access all the attributes of the children of this object?
If doing this structure, would it be better to use has_one instead of belongs_to?

Can I eager load attributes from one-to-one relationship for delegation without instantiating the related model?

Imagine this:
class House < ActiveRecord::Base
belongs_to :ground
delegate :elevation_in_meters, to: :ground
# attributes: stories, roof_type
end
class Ground < ActiveRecord::Base
has_one :house
# attributes: elevation_in_meters, geo_data
end
Then to eager load ground so that house.elevation_in_meters can be called without loading Ground I can do:
houses=House.includes(:ground).first(3)
The problem with this is, that the entire Ground object is actually instantiated with all attributes including the geo_data attribute - which I don't need in this case. The reason why I care is, that the query needs to be VERY performant, and geo_data is a pretty huge text field. I only need to read the delegated attributes, not write to them.
What approach could I take on eager loading the elevation_in_meters attribute from Ground without loading everything from Ground?
I'm on rails 4.1 btw
NOTE: Preferably I would like to have this eager loading behaviour by default for House, so that I do not need to specify it every time.
First off write a scope for the model you want to partially get and select the fields you like. Notice that I used the full name (with table name) and a string for the select. I'm not sure if you could just select(:elevation_in_meters,:geo_data) since I've copied this from our production example, and we use some joins with this scope that wont work without the table name. Just try it yourself.
class Ground < ActiveRecord::Base
has_one :house
attributes: elevation_in_meters, geo_data
scope :reduced, -> {
select('grounds.elevation_in_meters, grounds.geo_data')
}
end
With the scope present you can make a second belongs_to relation (don't be scared that it messes up your first one, since rails relations are basically just methods that are created for you), that calls the scope on your Ground model.
class House < ActiveRecord::Base
belongs_to :ground
belongs_to :ground_reduced, ->(_o) { reduced },
class_name: 'Ground', foreign_key: 'ground_id'
delegate :elevation_in_meters, to: :ground_reduced
# go for an additional delegation if
# you also need this with the full object sometimes
end
In the end you can just call your query like this:
houses = House.includes(:ground_reduced).first(3)
Technically it is not the proper answer to your question, since the Ground object is still instantiated. But the instance will only have the data you wanted and the other fields will be nil, so it should do the trick.
UPDATE:
As I just saw that you want to preferably have this behaviour as default, just add a scope for your House:
scope :reduced, -> { includes(:ground_reduced) }
You could then add this as your default scope, since your original relation will be untouched by this.
I know it's been a while but I just stumbled across this.
If you're only interested in the singular attribute you can also use a joins combined with a select and the attribute will magically be added to your House instance.
res = House.joins(:ground).select('houses.*, grounds.elevation_in_meters').first
res.elevation_in_meters # attribute is available on the object
To always have this attribute present, make it the default_scope for House, like so:
default_scope { joins(:ground).select('houses.*, grounds.elevation_in_meters') }
Depending on the nature of the tables you're joining you may need a distinct also.

In Rails, what do keywords belongs_to, has_many, etc, actually do?

I understand the concept of relational databases, primary/foreign keys, etc, however, I'm having trouble seeing the actual result of setting these properties up in my models. Do they generate helper functions or something similar? or do they simply work on a database level?
For example, if I have these two models (other properties omitted)
class Course < ActiveRecord::Base
has_and_belongs_to_many :schedules
has_many :sections
end
class Section < ActiveRecord::Base
belongs_to :course
end
I could simply get all sections for any given course like this:
Section.where(course_id: 1234)
However, I could do this without having set up the relations at all.
So my question is: Why do we do this?
Adding these methods let's you do things like this:
Section.find(5).course # <== returns a 'Course' model instance
Also it let's you join your queries more easily:
Section.joins(:course).where(course: {name: "English"}) # <== returns sections who have the 'english' course
Neither of these would be possible if you didn't set up the relations in the model.
Similarly:
Course.find(8).sections # returns an array of sections with 'course_id = 8'
It makes your calls more semantic, and makes things easier from a programmatic perspective :)
Relations are applied on instances of an object. So, these relations allow you to get related objects to an instance of another.
For example, say you have an instance of Section (called #section). You'd be able to get all Course objects for that section by doing:
#section.course if you have belongs_to :course set up.
Similarly, if you have an instance of Course, you can get all Section objects for that Course with:
#course.sections if you have has_many :sections.
TL;DR - these are helper scopes for instance variables of Course and Section.

Ruby on Rails Pattern for Collecting has_many Objects in a Tree of Nodes

I have an organizational hierarchy represented via awesome nested set on a Node model. Great, works great, the updates are expensive but the finds are super efficient.
Each Node model has_many on other models, let's call them Foo and Bar.
class Node < ActiveRecord::Base
acts_as_nested_set
has_many :foos
has_many :bars
end
class Foo < ActiveRecord::Base
belongs_to :node
end
Frequently, I want to find all of the foos or bars for a given subtree, looking down from a current node. Naively, I could do:
#foos = #node.self_and_descendants.collect(&:foos).compact
I could even use an ActiveRecord .includes(:foos) to avoid N+1 queries. What I really want is just to ask for #node.all_foos so I implement something like this:
class Node < ActiveRecord::Base
def all_foos
Foo.where(node_id: self_and_descendants.pluck(:id))
end
# More efficient?
def all_foos_alternately
Foo.where(node_id: self_and_descendants.includes(:foos).pluck(:id))
end
end
But, let's say that I want to “collect” more than just foos and bars, let's say that I have half a dozen or a dozen of this models. Now I'm littering my Node class or some lib with a bunch of all_* methods.
Should I be defining class methods on the foos and bars that accept a node as an argument and return all of the foos/bars for that subtree? But then the foos and bars need to understand/be aware of node.self_and_descendants.
Or the Foo class method could accept a collection of nodes, not needing to be aware of the nested set methods, but then I lose the easy interface via node.all_foos or the like.
What's the pattern or method I'm missing here? I've played with implementing a catch-all for node.all_* via method_missing but don't like the performance hit. What I'm trying to execute here is, at its core, a database lookup and, as such, it should be efficient and elegant.
Thank you to #adamsanderson for pointing me in the right direction with regards to joins and merge. Merge allows you to filter a joined association via another scope or relation. Brilliant!
def all_foos
Foo.joins(:node).merge(self_and_descendants)
end
I verified tests on one of my all_* methods in my application, rewrote the method using this joins.merge technique, and reran my tests. All green!
This answer does not address the design issue of how to apply this method across many different has_many relations but I will leave that as an exercise for the reader (me).
See also, with regards to merge:
http://blog.mitchcrowe.com/blog/2012/04/14/10-most-underused-activerecord-relation-methods
I am going to propose a solution that I don't think is very good in order to attempt to kick off discussion...
class Node < ActiveRecord::Base
acts_as_nested_set
has_many :foos
has_many :tree_foos, :through => :children
def all_foos
# Shoot, how do I write this so I still get a relation back?
foos + tree_foos
# Nope, how about...
Foo.where(id: foo_ids + tree_foo_ids)
end
end
Blech. I don't like that. And I'll still have to repeat this code across all the has_many model associations.

How many classes is too many? Rails STI

I am working on a very large Rails application. We initially did not use much inheritance, but we have had some eye opening experiences from a consultant and are looking to refactor some of our models.
We have the following pattern a lot in our application:
class Project < ActiveRecord::Base
has_many :graph_settings
end
class GraphType < ActiveRecord::Base
has_many :graph_settings
#graph type specific settings (units, labels, etc) stored in DB and very infrequently updated.
end
class GraphSetting < ActiveRecord::Base
belongs_to :graph_type
belongs_to :project
# Project implementation of graph type specific settings (y_min, y_max) also stored in db.
end
This also results in a ton of conditionals in views, helpers and in the GraphSetting model itself. None of this is good.
A simple refactor where we get rid of GraphType in favor of using a structure more like this:
class Graph < ActiveRecord::Base
belongs_to :project
# Generic methods and settings
end
class SpecificGraph < Graph
# Default methods and settings hard coded
# Project implementation specific details stored in db.
end
Now this makes perfect sense to me, eases testing, removes conditionals, and makes later internationalization easier. However we only have 15 to 30 graphs.
We have a very similar model (to complicated to use as an example) with close to probably 100 different 'types', and could potentially double that. They would all have relationships and methods they inheritated, some would need to override more methods then others. It seems like the perfect use, but that many just seems like a lot.
Is 200 STI classes to many? Is there another pattern we should look at?
Thanks for any wisdom and I will answer any questions.
If the differences are just in the behavior of the class, then I assume it shouldn't be a problem, and this is a good candidate for STI. (Mind you, I've never tried this with so many subclasses.)
But, if your 200 STI classes each have some unique attributes, you would need a lot of extra database columns in the master table which would be NULL, 99.5% of the time. This could be very inefficient.
To create something like "multiple table inheritance", what I've done before with success was to use a little metaprogramming to associate other tables for the details unique to each class:
class SpecificGraph < Graph
include SpecificGraphDetail::MTI
end
class SpecificGraphDetail < ActiveRecord::Base
module MTI
def self.included(base)
base.class_eval do
has_one :specific_graph_detail, :foreign_key => 'graph_id', :dependent => :destroy
delegate :extra_column, :extra_column=, :to => :specific_graph_detail
end
end
end
end
The delegation means you can access the associated detail fields as if they were directly on the model instead of going through the specific_graph_detail association, and for all intents and purposes it "looks" like these are just extra columns.
You have to trade off the situations where you need to join these extra detail tables against just having the extra columns in the master table. That will decide whether to use STI or a solution using associated tables, such as my solution above.

Resources