Rails 7.1 Expands ActiveRecord API with Async Query Support

Traditionally, ActiveRecord queries have been synchronous, causing the application to pause and wait for the database to respond before continuing execution.

While this synchronous behavior works well in many cases, it can become a bottleneck when dealing with lengthy or resource-intensive queries, impacting the overall responsiveness of the application.

Asynchronous querying, however, is executed in a background thread. This enables our application main thread to remain responsive and handle incoming requests concurrently while the queries are executed in the background.

This asynchronous approach drastically enhances performance, especially in scenarios where waiting for the database response might hinder the application’s responsiveness.

Before

Rails 7 introduced ActiveRecord load_async method that allows multiple Active Record queries to be executed in parallel using the background thread and while our foreground thread continues on with the request.

This is useful for queries that can be performed long enough before their result will be needed, or for controllers which need to perform several independent queries.

@users = User.all.load_async
@posts = Post.order(published_at: :desc).load_async

It misses methods to load aggregate queries such as count, sum, etc.

After

Building upon the foundation laid in previous versions, Rails 7.1 introduced a suite of asynchronous methods in Active Record, significantly expanding its capabilities beyond traditional synchronous querying.

These new methods cater to various scenarios, including aggregates, single record retrieval, and custom SQL operations. They offer developers the flexibility to execute queries asynchronously, resulting in improved performance for specific types of database operations.

The following are the asynchronous methods in ActiveRecord:

  • async_count
  • async_sum
  • async_minimum
  • async_maximum
  • async_average
  • async_pluck
  • async_pick
  • async_ids
  • async_find_by_sql
  • async_count_by_sql

Asynchronous queries return promise objects. To retrieve the actual result of the asynchronous operation, we need to call the value method on the returned promise. This method fetches the eventual value generated by the asynchronous query when it’s resolved.

Below is an example of using async_count

# Synchronous count
irb(main):001> User.count
  User Count (58.7ms)  SELECT COUNT(*) FROM "users"
=> 53

# Asynchronous count
irb(main):002> promise = User.async_count
  User Count (3.6ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Promise status=complete>
irb(main):003> promise.value
=> 53

async_count operates similarly to count but executes asynchronously, allowing the application to perform other tasks while the count operation runs in the background. It provides the flexibility to maintain responsiveness in the application, especially beneficial when dealing with resource-intensive queries.

In the above example, User.count fetches total (53) records in 58.7ms. Using User.async_count, it fetches in 3.6ms as asynchronous operation schedules in the background. Calling value on promise retrieves the records count (53), akin to synchronous count, affirming identical results asynchronously.

  • Here is an example of async_sum
# Synchronous sum
> Invoice.sum(:amount)
=> 0.32633333e6

# Asynchronous sum
> sum_amount = Invoice.async_sum(:amount)
> sum_amount.value
=> 0.32633333e6

async queries aims to optimize the handling of slower queries, particularly targeting aggregate functions (like count, sum) and methods that fetch single records or any result beyond a Relation, aiming for greater efficiency in their execution.

Conclusion

Implementing asynchronous queries in a Rails application offers diverse advantages. They optimize performance by executing multiple tasks concurrently, particularly benefiting complex or slow queries and applications handling concurrent requests.

Additionally, they improve application responsiveness by seamlessly managing requests while queries operate in the background. Asynchronous queries also aid in preserving memory by executing operations in separate threads, particularly advantageous for applications handling extensive datasets or high request volumes.

However, adopting best practices is crucial. Reserve asynchronous queries for slower or intricate tasks, avoid unnecessary overhead for simple queries, and conduct thorough testing to ensure compatibility and expected functionality, considering potential limitations with ActiveRecord features like transactions and callbacks.

Ultimately, embracing asynchronous queries presents a robust approach to enhancing performance, responsiveness, and memory management in our Rails application.

Need help on your Ruby on Rails or React project?

Join Our Newsletter