window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster already exists') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() Optimizing code with Query Objects - The Codest
The Codest
  • About us
  • Services
    • Software Development
      • Frontend Development
      • Backend Development
    • Staff Augmentation
      • Frontend Developers
      • Backend Developers
      • Data Engineers
      • Cloud Engineers
      • QA Engineers
      • Other
    • It Advisory
      • Audit & Consulting
  • Industries
    • Fintech & Banking
    • E-commerce
    • Adtech
    • Healthtech
    • Manufacturing
    • Logistics
    • Automotive
    • IOT
  • Value for
    • CEO
    • CTO
    • Delivery Manager
  • Our team
  • Case Studies
  • Know How
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About us
  • Services
    • Software Development
      • Frontend Development
      • Backend Development
    • Staff Augmentation
      • Frontend Developers
      • Backend Developers
      • Data Engineers
      • Cloud Engineers
      • QA Engineers
      • Other
    • It Advisory
      • Audit & Consulting
  • Value for
    • CEO
    • CTO
    • Delivery Manager
  • Our team
  • Case Studies
  • Know How
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
Back arrow GO BACK
2019-03-08
Software Development

Optimizing code with Query Objects

The Codest

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

Build Future-Proof Web Apps: Insights from The Codest’s Expert Team

Discover how The Codest excels in creating scalable, interactive web applications with cutting-edge technologies, delivering seamless user experiences across all platforms. Learn how our expertise drives digital transformation and business...

THECODEST
Software Development

Top 10 Latvia-Based Software Development Companies

Learn about Latvia's top software development companies and their innovative solutions in our latest article. Discover how these tech leaders can help elevate your business.

thecodest
Enterprise & Scaleups Solutions

Java Software Development Essentials: A Guide to Outsourcing Successfully

Explore this essential guide on successfully outsourcing Java software development to enhance efficiency, access expertise, and drive project success with The Codest.

thecodest
Software Development

The Ultimate Guide to Outsourcing in Poland

The surge in outsourcing in Poland is driven by economic, educational, and technological advancements, fostering IT growth and a business-friendly climate.

TheCodest
Enterprise & Scaleups Solutions

The Complete Guide to IT Audit Tools and Techniques

IT audits ensure secure, efficient, and compliant systems. Learn more about their importance by reading the full article.

The Codest
Jakub Jakubowicz CTO & Co-Founder

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

    About us

    The Codest – International software development 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
    • Dictionary

      Services

    • It Advisory
    • Software Development
    • Backend Development
    • Frontend Development
    • Staff Augmentation
    • Backend Developers
    • Cloud Engineers
    • Data Engineers
    • Other
    • QA Engineers

      Resources

    • Facts and Myths about Cooperating with External Software Development Partner
    • From the USA to Europe: Why do American startups decide to relocate to Europe
    • Tech Offshore Development Hubs Comparison: Tech Offshore Europe (Poland), ASEAN (Philippines), Eurasia (Turkey)
    • What are the top CTOs and CIOs Challenges?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Website terms of use

    Copyright © 2025 by The Codest. All rights reserved.

    en_USEnglish
    de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek en_USEnglish