Saurav Prakash
Saurav Prakash

Reputation: 1247

Ruby on rails active record queries which one is efficient

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: enter image description here

Looks like both run the same queries.

My questions are

  1. 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?
  2. Which one is more performant in case of bigger data size?
  3. Doesn't calling author.last_name from Book class violates design principles ?

Upvotes: 2

Views: 223

Answers (5)

dcporter7
dcporter7

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

SteveTurczyn
SteveTurczyn

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

Naren Sisodiya
Naren Sisodiya

Reputation: 7288

  1. 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.

  1. 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.

  2. Doesn't calling author.last_name from Book class violates design principles?

    No, you can even use delegate like @Steve Suggested.

Upvotes: 1

a131
a131

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

nattfodd
nattfodd

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

Related Questions