Reputation: 1247
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
Looks like both run the same queries.
My questions are
Upvotes: 2
Views: 223
Reputation: 555
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.
Upvotes: 1
Reputation: 36860
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.
Upvotes: 4
Reputation: 7288
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.
Upvotes: 1
Reputation: 564
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
Upvotes: 1
Reputation: 1890
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)
Upvotes: 1