I was recently working on a project where I faced a dilemma of choosing between two ways of getting same results. Here is the class structure:
class Book < ApplicationRecord
belongs_to :author
end
class Author < ApplicationRecord
has_many :books
end
An author has first name, last name. I want to get the full name of the author for a given book as an instance method.
In simple active record terms, since book is associated with author, we can get the author name for a book as follows:
For example in Book class, we have:
class Book < ApplicationRecord
belongs_to :author
def author_name
"#{author.first_name} #{author.last_name}"
end
end
And we get the result!
But, according to the target of minimizing dependencies (POODR Book), future ease of change and better object oriented design, the book should not know properties of an author. It should interact with an author object by interfaces.
So Book should not be the one responsible for getting the Author name. The author class should.
class Book < ApplicationRecord
belongs_to :author
def author_name
get_author_name(self.author_id)
end
private
#minimizing class dependecies by providing private methods as external interfaces
def get_author_name(author_id)
Author.get_author_name_from_id(author_id)
end
end
class Author < ApplicationRecord
has_many :books
#class methods which provides a gate-way for other classes to communicate through interfaces, thus reducing coupling.
def self.get_author_name_from_id(id)
author = self.find_by_id(id)
author == nil ? "Author Record Not Found" : "#{author.first_name.titleize} #{author.last_name.titleize}"
end
end
Now, book is just interacting with the public interface provided by Author and Author is handling the responsibility of getting full name from its properties which is a better design for sure.
I tried running the queries as two separate methods in my console:
class Book < ApplicationRecord
def author_name
get_author_name(self.author_id)
end
def author_name2
"#{author.last_name} + #{author.first_name}"
end
end
The results are shown below:
Looks like both run the same queries.
My questions are
Does rails convert author.last_name called inside the Book class to
the same SQL query as Author.find_by_id(author_id).last_name called inside
Author class (through message passing from Book class) in case of bigger data size?
Which one is more performant in case of bigger data size?
Doesn't calling author.last_name from Book class violates design
principles ?
It's actually much more common and simplier to use delegation.
class Book < ApplicationRecord
belongs_to :author
delegate :name, to: :author, prefix: true, allow_nil: true
end
class Author < ApplicationRecord
has_many :books
def name
"#{first_name.titleize} #(last_name.titleize}"
end
end
As to performance, if you join the authors at the time of the book query you end up doing a single query.
#books = Book.joins(:author)
Now when you iterate through #books and you call individually book.author_name no SQL query needs to be made to the authors table.
1) Obviously not, it performs JOIN of books & authors tables. What you've made requires 2 queries, instead of 1 join you'll have book.find(id) and author.find(book.author_id).
2) JOIN should be faster.
3) Since last_name is a public interface, it absolutely doesn't violate design principles. It would violate principles if you were accessing author's last name from outside like that: Book.find(1).author.last_name - that's a bad thing. Correct is: Book.find(1).authors_last_name - and accessing author's name inside Model class.
Your provided example seems to be overcomplicated to me.
According to the example you shared, you only want to get full name of the book's author. So, the idea of splitting responsibility is correct, but in Author class should be simple instance method full_name, like:
class Author < ApplicationRecord
has_many :books
def full_name
"#{author.first_name.titleize} #{author.last_name.titleize}"
end
end
class Book < ActiveRecord::Base
belongs_to :author
def author_name
author.full_name
end
end
Note, there're no direct queries in this code. Once you'll need the author's name somewhere (in a view, in api response, etc), Rails will make the most optimized query possible (depends on your use case though, it may be ineffective for example, if you call iterate over books and call author in a loop)
I prefer the second approach because the full_name is property of author not a book. If the book wants to access that information, it can using book.author&.full_name (& is for handling cases of books with no authors).
but I would suggest a refactoring as below:
class Book < ApplicationRecord
belongs_to :author
end
class Author < ApplicationRecord
has_many :books
def full_name
"#{firstname} #{lastname}"
end
end
Does rails convert author.last_name called inside the Book class to the same SQL query as Author.find_by_id(author_id).last_name called inside Author class (through message passing from Book class) in case of bigger data size?
Depend upon the calling factor, like in your example both will generate the same query. But if you have a include\join clause while getting the Book/Author, both will generate different queries.
As per the rails convention, Author.find_by_id(author_id).last_name is not recommended as it will always fire a query on database whenever the method is called. One should use the rails' association interface to call the method on related object which is smart to identify the object from memory or fetch it from database if not in memory.
Which one is more performant in case of bigger data size?
author.last_name is better because it will take care of joins, include, and memoization clauses if used and avoid the N+1 query problem.
Doesn't calling author.last_name from Book class violates design principles?
No, you can even use delegate like #Steve Suggested.
In my experience, it's a balancing act between minimizing code complexity and minimizing scalability issues.
However, in this case, I think the simplest solution that would separate class concerns and minimize code would be to simply use: #book.author.full_name
And in your Author.rb define full_name in Author.rb:
def full_name
"#{self.first_name} #{self.last_name}"
end
This will simplify your code a lot. For example, if in the future you had another model called Magazine that has an Author, you don't have to go define author_name in the Magazine model as well. You simply use #magazine.author.full_name. This will DRY up your code nicely.
I'm trying to create a mixin that allows an ActiveRecord model to act as a delegate for another model. So, doing it the typical way:
class Apple < ActiveRecord::Base
def foo_species
"Red delicious"
end
end
class AppleWrapper < ActiveRecord::Base
belongs_to :apple
# some meta delegation code here so that AppleWrapper
# has all the same interface as Apple
end
a = Apple.create
w = AppleWrapper.create
w.apple = a
w.foo_species
# => 'Red delicious'
What I want is to abstract this behavior into a Mixin, so that given a bunch of data models, I can create "Wrapper" classes that are also ActiveRecords, but that each wrapper corresponds to a specific class. Why? Each of the data models have calculations, aggregations with other models, and I want the "Wrapper" classes to contain fields (in the schema) that correspond to these calculations...so in effect. the Wrapper acts as a cached version of the original data model, with the same interface.
I will have to write out each Wrapper...so for Apple, Orange, Pear, there is a different Wrapper model for each of them. However, I just want to abstract out the wrapper behavior...so that there's a class level method that sets what the Wrapper points to, a la:
module WrapperMixin
extend ActiveSupport::Concern
module ClassMethods
def set_wrapped_class(klass)
# this sets the relation to another data model and does the meta delegation
end
end
end
class AppleWrapper < ActiveRecord::Base
include WrapperMixin
set_wrapped_class Apple
end
class OrangeWrapper < ActiveRecord::Base
include WrapperMixin
set_wrapped_class Orange
end
How would I set this up? And would this have to be a STI type relation? That is, does the Wrapper class have to have a wrapped_record_id and a wrapped_record_type?
You can use belongs_to in your set_wrapped_class method.
def set_wrapped_class(klass)
belongs_to klass.to_s.downcase.to_sym
end
I would like to use form_for except I have class table inheritance models using the citier gem. They are defined as such:
class Fruit < ActiveRecord::Base
# calories:integer
# color:string
end
class Apple < Fruit
# is_sauce:boolean
end
class Banana < Fruit
# is_peeled:boolean
end
The problem is that I want the first part of my form to fill out attributes for my Fruit model. Then depending on a select field on what type of fruit (Apple, Banana), I then want to fill out attributes for that particular model yet I still want validations with the form_for helper. Any suggestions on how I can approach this... or additional clarification? Thanks.
What I ended up doing was asking for the model before creating the form. Then using many partials.
Short version: Where should I store environment-specific IDs? ENV['some-variable']? Somewhere else?
Long version:
Let's say I have a model called Books and a book has a Category. (For the sake of this question, let's say a book only has one category.)
class Book < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :books
end
Now let's say one category is called 'erotica.' And I want to suppress erotica books in my type ahead. That seems straight forward. But in production and in development 'erotica' has a different ID. I don't want my code to be ID dependent. I don't want it to be string dependent (in case 'erotica' is renamed pr0n or whatever).
I think I should have something like
def suppress_method
suppress_category_id = look_up_suppression_id
...
end
but where should 'look up' look?
Thanks!
Taking this approach will be brittle, what if you want to suppress multiple categories? Erotica and Politics? The best design here is for you to actually add 'suppressed' as a boolean to category in a migration, and maintain that in your application's administration interface. After you've done that you can add a named scope like:
class Category < ActiveRecord::Base
named_scope :not_suppressed, :conditions=>{:suppressed=>false}
# or for rails 3
scope :not_suppressed, where(:suppressed=>false)
end
Then just update your type ahead code to do:
Category.not_suppressed.find ...
Rather than
Category.find
I have a rails app with several models.
I have a function that I want to access from several models.
What's the best place to put this code and how can I make it accessible from the models that need to get at it?
My understanding is that helpers are just for views. Is this correct?
It seems wrong to create a plug-in and put it in the vendor folder - this is my code and integral to my app. Is this correct?
Thanks.
The simplest solution would be to create a module under lib and mix this into the models that need it, for instance, in lib/fooable.rb:
module Fooable
def do_foo
end
end
And then in your various models:
class MyModel < ActiveRecord::Base
include Fooable
end
No need to require fooable.rb, the Rails autoloading mechanism will find it for you as long as it's named using correct conventions.
In order to lessen the repetition of code, you could also create a main class which would include that module and the simply inherit from it from every model you'd like to share the behaviour.
Something like:
module Fooable
def do_foo
end
end
class ParentModel < ActiveRecord::Base
include Fooable
end
class Product < ParentModel end
class User < ParentModel end
class Item < ActiveRecord::Base end
Thus, in that example, both Product and User would share the do_foo functionality and Item would not.