Go to content
The Codest
  • About Us
    • Staff Augmentation
    • Project Development
    • Cloud Engineering
    • Quality Assurance
    • Web Development
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About Us
    • Staff Augmentation
    • Project Development
    • Cloud Engineering
    • Quality Assurance
    • Web Development
  • Our Team
  • Case studies
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
Content
2019-03-08
Software Development

Optimizing code with Query Objects

Tomasz Szkaradek

Development Architect

It’s quite probable that at work you have encountered with overloaded models and a huge number of calls in the controllers for quite a lot of times. Basing on the knowledge in the Rails environment, in this article, I am going to propose a simple solution to this problem.

A very important aspect of the rails application is to minimize the number of redundant dependencies, which is why the entire Rails environment has recently been promoting the service object approach and the use of the PORO (Pure Old Ruby Object) method. A description of how to use such a solution you will find here. In this article, we will solve the concept step by step and adapt it to the problem.

Problem

In a hypothetical application, we are dealing with a complicated transaction system. Our model, representing each transaction, has a set of scopes, that help you to get data. It is a great job facilitation as it can be found in one place. However, this doesn’t last for long. With the development of the application, the project is becoming more and more complicated. Scopes no longer have simple 'where' references, we lack data and start to load relationships. After a while, it reminds a complicated system of mirrors. And, what's worse, we do not know how to do a multi-line lambda!

Below, you will find an already expanded application model. The payment system transactions are stored in. As you can see in the example below:

class Transaction < ActiveRecord::Base
  belongs_to :account
  has_one :withdrawal_item

  scope(:for_publishers, lambda do
    select("transactions.*")
      .joins(:account).where("accounts.owner_type = 'Publisher'")
      .joins("JOIN publishers ON owner_id = publishers.id")
  end)

  scope :visible, -> { where(visible: true) }

  scope(:active, lambda do
    joins(<<-SQL
      LEFT OUTER JOIN source ON transactions.source_id = source.id
      AND source.accepted_at IS NOT NULL
    SQL
  end)
end

The model is one thing, but as the scale of our project increases, the controllers also start to swell. Let's look at the example below:

class TransactionsController < ApplicationController
  def index
    @transactions = Transaction.for_publishers
                                   .active
                                   .visible
                                   .joins("LEFT JOIN withdrawal_items ON withdrawal_items.transaction_id = transactions.id")
                                   .joins("LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR \
 (withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')")
                                   .order(:created_at)
                                   .page(params[:page])
                                   .per(params[:page])
    @transactions = apply_filters(@transactions)
  end
end

Here we can see many lines of chained methods alongside with additional joins that we do not want to perform in many places, only in this particular one. The attached data is later used by the apply_filters method, which adds the appropriate data filtering, based on the GET parameters. Of course, we can transfer some of these references to scope, but isn’t this the problem that we are actually trying to resolve?

Solution

Since we already know about a problem we have, we must solve this. Based on the reference in the introduction, we will use the PORO approach here. In this exact case, this approach is called the query object, which is a development of the service objects concept.

Let's create a new directory named “services”, located in the apps directory of our project. There we will create a class named TransactionsQuery.

class TransactionsQuery
end

As a next step, we need to create an initializer where a default call path for our object will be created

class TransactionsQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end
end

Thanks to this, we will be able to transfer the relationship from the active record to our facility. Now we can transfer all our scopes to the class, which are needed only in the presented controller.

class TransactionsQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end

  private

  def active(scope)
    scope.joins(<<-SQL
      LEFT OUTER JOIN source ON transactions.source_id = source.id
      AND source.accepted_at IS NOT NULL
    SQL
  end

  def visible(scope)
    scope.where(visible: true)
  end

  def for_publishers(scope)
    scope.select("transactions.*")
         .joins(:account)
         .where("accounts.owner_type = 'Publisher'")
         .joins("JOIN publishers ON owner_id = publishers.id")
  end
end

We still miss the most important part, ie collecting data into one string and making the interface public. The method where we will stick everything together will be named a “call”.

What is really important is that we will use the @scope instance variable there, where the scope of our call is located.

class TransactionsQuery
  ...
  def call
    visible(@scope)
        .then(&method(:active))
        .then(&method(:for_publishers))
        .order(:created_at)
  end

  private
  ...
end

The entire class presents itself as the following:

class TransactionsQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end

  def call
    visible(@scope)
        .then(&method(:active))
        .then(&method(:for_publishers))
        .order(:created_at)
  end

  private

  def active(scope)
    scope.joins(<<-SQL
      LEFT OUTER JOIN source ON transactions.source_id = source.id
      AND source.accepted_at IS NOT NULL
    SQL
  end

  def visible(scope)
    scope.where(visible: true)
  end

  def for_publishers(scope)
    scope.select("transactions.*")
         .joins(:account)
         .where("accounts.owner_type = 'Publisher'")
         .joins("JOIN publishers ON owner_id = publishers.id")
  end
end

After our cleaning-up, the model looks definitely lighter. There we focus only on the data validation and relationships between other models.

class Transaction < ActiveRecord::Base
  belongs_to :account
  has_one :withdrawal_item
end

The controller has already implemented our solution; we have moved all additional queries to a separate class. However, the calls, we did not have in the model, remain an unresolved issue. After some changes, our index action looks like this:

class TransactionsController < ApplicationController
  def index
    @transactions = TransactionsQuery.new
                                     .call
                                     .joins("LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id")
                                     .joins("LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR \
 (withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')")
                                     .order(:created_at)
                                     .page(params[:page])
                                     .per(params[:page])
    @transactions = apply_filters(@transactions)
  end
end

Solution

In the case of implementing good practices and conventions, a good idea may be to replace all similar occurrences of a given problem. Therefore, we will move the SQL query from the index action to the separate query object. We will call this a TransactionsFilterableQuery class. The style, which we prepare the class in, will be similar to that one presented in TransactionsQuery. As part of the code changes, a more intuitive record of large SQL queries will be smuggled, using multiline character strings called heredoc. The solution available you will find below:

class TransactionsFilterableQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end

  def call
    withdrawal(@scope).then(&method(:withdrawal_items))
  end

  private

  def withdrawal(scope)
    scope.joins(<<-SQL
      LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR
      (withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')
    SQL
  end

  def withdrawal_items(scope)
    scope.joins(<<-SQL
      LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id
    SQL
  end
end

In case of changes in the controller, we reduce the mass of lines by adding the query object. It is important that we separate everything except the part responsible for pagination.

class TransactionsController < ApplicationController
  def index
    @transactions = TransactionsQuery.new.call.then do |scope|
      TransactionsFilterableQuery.new(scope).call
    end.page(params[:page]).per(params[:page])

    @transactions = apply_filters(@transactions)
  end
end

Summary

Query object changes a lot in the approach to writing SQL queries. In ActiveRecord, it is very easy to place all business and database logic in the model since everything is in one place. This will work quite well for smaller applications. As the complexity of the project increases, we set the logic to other places. The same query object allows you to group member query queries into a specific problem.

Thanks to this, we have an easy possibility of the code’s later inheritance and because of duck typing, you can also use these solutions in other models. The disadvantage of this solution is a larger amount of code and fragmentation of responsibility. However, whether we want to take up such a challenge or not, depends on us and how badly we are disturbed by fat models.

Related articles

Software Development

3 Useful HTML Tags You Might Not Know Even Existed

Nowadays, accessibility (A11y) is crucial on all stages of building custom software products. Starting from the UX/UI design part, it trespasses into advanced levels of building features in code. It provides tons of benefits for...

Jacek Ludzik
Software Development

5 examples of Ruby’s best usage

Have you ever wondered what we can do with Ruby? Well, the sky is probably the limit, but we are happy to talk about some more or less known cases where we can use this powerful language. Let me give you some examples.

Pawel Muszynski
Software Development

Maintaining a Project in PHP: 5 Mistakes to Avoid

More than one article has been written about the mistakes made during the process of running a project, but rarely does one look at the project requirements and manage the risks given the technology chosen.

Sebastian Luczak
Software Development

Why you will find qualified Ruby developers in Poland?

Real Ruby professionals are rare birds on the market. Ruby is not the most popular technology, so companies often struggle with the problem of finding developers who have both high-level skills and deep experience; oh, and by the...

Jakub
Software Development

9 Mistakes to Avoid While Programming in Java

What mistakes should be avoided while programming in Java? In the following piece we answers this question.

Rafal Sawicki
Software Development

A quick dive into Ruby 2.6. What is new?

Released quite recently, Ruby 2.6 brings a bunch of conveniences that may be worth taking a glimpse of.  What is new? Let’s give it a shot!

Patrycja Slabosz

Subscribe to our knowledge base and stay up to date on the expertise from industry.

About us

The Codest – International Tech Software Company with tech hubs in Poland.

    United Kingdom - Headquarters

  • Office 303B, 182-184 High Street North E6 2JA London, England

    Poland - Local Tech Hubs

  • Fabryczna Office Park, Aleja Pokoju 18, 31-564 Kraków
  • Brain Embassy, Konstruktorska 11, 02-673 Warsaw, Poland

    The Codest

  • Home
  • About us
  • Services
  • Case studies
  • Know how
  • Careers

    Services

  • PHP development
  • Java development
  • Python development
  • Ruby on Rails development
  • React Developers
  • Vue Developers
  • TypeScript Developers
  • DevOps
  • QA Engineers

    Resources

  • What are top CTOs and CIOs Challenges? [2022 updated]
  • Facts and Myths about Cooperating with External Software Development Partner
  • From the USA to Europe: Why do American startups decide to relocate to Europe
  • Privacy policy
  • Website terms of use

Copyright © 2023 by The Codest. All rights reserved.

We use cookies on the site for marketing, analytical and statistical purposes. By continuing to use, without changing your privacy settings, our site, you consent to the storage of cookies in your browser. You can always change the cookie settings in your browser. You can find more information in our Privacy Policy.