Ruby on Rails Interview Questions and Answers — Model — Part 4

Gokul
9 min readOct 7, 2023

--

  • How do you implement custom methods in Rails models to perform specific tasks?
  • What is the purpose of the default_scope in Rails models, and when should you use it?
  • How do you handle database transactions in Rails models?
  • What is the role of model factories and how do you use them in testing?
  • Can you explain the difference between optimistic and pessimistic locking in Rails models?

16. How do you implement custom methods in Rails models to perform specific tasks?

The custom methods can encapsulate business logic, data manipulation, or any other tasks that are specific to the model.

Create a Model

If you haven’t already, create a model using Rails generators. For example, to create a User model, you can run:

rails generate model User name:string email:string

Define Custom Methods

Inside the model file (e.g., app/models/user.rb), you can define custom methods. Here's an example of a full_name method that returns the full name of a user

class User < ApplicationRecord
# Other attributes and validations...

def full_name
"#{first_name} #{last_name}"
end
end

In this example, full_name is a custom method that combines the first_name and last_name attributes of the user.

Using Custom Methods

You can now use this custom method in your application. For example, in a view or controller:

@user = User.find(1)
full_name = @user.full_name

This will call the full_name method on the @user instance and return the full name.

Here are some additional tips and considerations:

  • You can pass arguments to your custom methods if needed, just like any other Ruby method.
  • Custom methods can call other methods, access instance variables, and interact with the database through ActiveRecord queries.
  • It’s a good practice to keep your models slim and put most of your business logic in separate service objects or modules to follow the Single Responsibility Principle (SRP) and make your code more maintainable.
  • Make sure to follow Rails naming conventions for methods and attributes (e.g., use snake_case for method names and lowercase_plural for table names).

By following these steps, you can easily implement custom methods in your Rails models to encapsulate and organize your application’s logic effectively.

17. What is the purpose of the default_scope in Rails models, and when should you use it?

The default_scope in Rails, models are used to specify a default set of conditions or ordering that should be applied to all queries made against that model. It allows you to define a default behavior for your model's database queries, and it's applied automatically unless overridden explicitly in other parts of your application. However, it should be used cautiously as it can have unintended consequences if not used carefully.

class Post < ApplicationRecord
default_scope { order(created_at: :desc) }
end

In this example, the default_scope is set to always order Post records by the created_at column in descending order by default. So, whenever you query the Post model without specifying an ordering, it will be ordered by created_at in descending order.

Use Cases:

Common Ordering: It can be useful for cases where you almost always want records sorted in a specific way by default.

Soft Deletion: You can use default_scope to implement soft delete functionality, where records marked as deleted are automatically excluded from queries.

Cautions

Complexity: Be cautious when using default_scope for complex conditions or joins, as it can make queries less predictable and harder to debug.

Overriding: It’s essential to be aware that default_scope can be overridden in specific queries. If you find yourself frequently overriding the default scope, it might indicate that the default scope is not the right solution.

Impact on Performance: Applying default scopes to every query can affect performance negatively if the scope includes complex conditions or joins. Consider using unscoped or other methods to exclude the default scope in specific situations.

Alternatives

If you need more flexibility, you can use named scopes or class methods to define specific query behaviors that can be applied when needed. Consider using default_scope sparingly and only for simple, consistently needed behaviors.

18. How do you handle database transactions in Rails models?

The models use the built-in support for database transactions provided by ActiveRecord, which is Rails’ Object-Relational Mapping (ORM) library. Database transactions are essential for maintaining data consistency and integrity when multiple database operations need to be executed together, and if any part of the transaction fails, the entire transaction can be rolled back.

Starting a Transaction

To start a new database transaction, you can use the transaction method on your ActiveRecord model class. You typically use a block to encapsulate the database operations that should be part of the transaction.

ActiveRecord::Base.transaction do
# Database operations here
end

Performing Database Operations

Inside the transaction block, you can perform various database operations like creating, updating, or deleting records using ActiveRecord methods.

ActiveRecord::Base.transaction do
user = User.create(name: 'John')
account = Account.create(user: user, balance: 1000)

# Additional operations...
end

Committing and Rolling Back

By default, if an exception occurs inside the transaction block, the entire transaction is rolled back, undoing any changes made during the transaction. If no exceptions occur, the transaction is automatically committed when the block exits.

You can also explicitly roll back a transaction within the block if needed:

ActiveRecord::Base.transaction do
begin
# Database operations...

if some_condition
raise ActiveRecord::Rollback # Roll back the transaction
end

# Additional operations...
rescue
# Handle exceptions...
end
end

Using ActiveRecord::Rollback is a common way to roll back a transaction when a specific condition is met.

Nested Transactions

You can nest transactions within one another, but it’s important to note that the inner transactions don’t have a separate scope. If an inner transaction is rolled back, it affects the entire outer transaction. To use nested transactions, you can simply call transaction again within an existing transaction block.

ActiveRecord::Base.transaction do
# Outer transaction operations...

ActiveRecord::Base.transaction do
# Inner transaction operations...
end

# More outer transaction operations...
end

Handling Errors

Be sure to handle exceptions gracefully within your transactions, as unhandled exceptions can cause unexpected behavior.

ActiveRecord::Base.transaction do
begin
# Database operations...
rescue StandardError => e
# Handle the exception (e.g., log it or raise a custom error)
end
end

By using these techniques, you can effectively handle database transactions in your Rails models, ensuring that your data remains consistent and reliable even in complex operations.

19. What is the role of model factories and how do you use them in testing?

Model factories play a crucial role in testing, particularly in the context of web development frameworks like Ruby on Rails. They are used to create test data for your application’s models in an automated and controlled manner.

Model factories help ensure that your tests are consistent, repeatable, and isolate the behavior you want to test. In Rails, the most commonly used library for creating model factories is FactoryBot (formerly known as FactoryGirl).

Here’s a breakdown of the role of model factories and how you can use them in testing:

Generating Test Data

Model factories are used to generate instances of your application’s models with predefined attribute values. This allows you to create test data on-the-fly, which is essential for running tests, including unit tests, integration tests, and end-to-end tests.

Maintaining Test Data Consistency

Using model factories helps maintain data consistency across tests. You can define default values for attributes and override them as needed for specific test cases. This ensures that your tests start with a known and consistent data state.

Isolating Tests

Model factories enable you to isolate individual tests from the state of the database. Each test can create its own test data, perform operations, and assert outcomes without affecting other tests. This isolation prevents side effects and test dependencies.

DRY (Don’t Repeat Yourself) Testing

Model factories promote DRY testing by centralizing the creation of test data. Instead of manually creating records in every test case, you define factories once and reuse them throughout your test suite.

Using FactoryBot in Rails

Here’s how you can use FactoryBot to define and use model factories in a Ruby on Rails application:

Install FactoryBot

First, add FactoryBot to your Gemfile and run bundle install:

# Gemfile
gem 'factory_bot_rails'

Define Factories

Create factory files for your models in the spec/factories directory. For example, if you have a User model, you can create a users.rb factory file:

# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { "John Doe" }
email { "john@example.com" }
end
end

Use Factories in Tests

In your test files, you can use the defined factories to create instances of models for testing:

# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
it "is valid with valid attributes" do
user = FactoryBot.build(:user)
expect(user).to be_valid
end
end

The FactoryBot.build method creates a new instance of the User model with the attributes defined in the factory.

Customize Factory Attributes

You can also override factory attributes for specific test cases if needed:

# Override the email attribute
user = FactoryBot.build(:user, email: "custom_email@example.com")

By using FactoryBot and model factories in your testing workflow, you can streamline the process of creating test data, maintain consistency, and write more efficient and reliable tests for your Rails application.

20. Can you explain the difference between optimistic and pessimistic locking in Rails models?

Optimistic locking and pessimistic locking are two different strategies used in database management systems, including Rails models, to handle concurrent access to data and prevent conflicts when multiple users or processes try to modify the same record simultaneously.

These two approaches have distinct characteristics:

  • Optimistic Locking
  • Pessimistic Locking

Optimistic Locking

Concurrency Handling Philosophy: Optimistic locking assumes that conflicts between users/processes are rare. It allows multiple users to read and potentially update a record concurrently without blocking each other. Conflicts are detected at the point of saving changes.

How It Works: In Rails, optimistic locking is typically implemented by adding a special column, often named lock_version, to the database table. This column is a numeric field that gets incremented with each update of the record.

Usage: When a user retrieves a record, the lock_version is also retrieved. When they try to update the record, Rails checks if the lock_version of the record in the database matches the one the user has. If they match, the update is allowed; if not, it means another user has modified the record since it was retrieved, and the update is rejected.

# Assume the 'posts' table has a 'lock_version' column.
post = Post.find(1)
post.title = "Updated Title"
post.save # Rails will check the 'lock_version' during the save.

Pros: Optimistic locking allows for better concurrency and responsiveness because it doesn’t block users unnecessarily. Conflicts are rare, and only the user who tries to save conflicting data will be affected.

Cons: It doesn’t prevent conflicts entirely; it just detects them after the fact. Conflicts may require handling, such as notifying the user or merging changes manually.

Pessimistic Locking

Concurrency Handling Philosophy: Pessimistic locking assumes that conflicts are more likely to occur. It locks a record explicitly to prevent other users/processes from accessing or modifying it until the lock is released.

How It Works: In Rails, pessimistic locking is implemented using the lock method or SQL-specific locking statements like FOR UPDATE. When a record is locked, other users trying to access it will be blocked until the lock is released.

Usage: You can use pessimistic locking when you want to ensure exclusive access to a record for a specific operation. For example, if you’re processing a payment, you might want to lock the associated account record to prevent concurrent updates.

# Pessimistic locking using the lock method.
account = Account.find(1)
account.with_lock do
account.balance -= 100
account.save
end

Pros: Pessimistic locking guarantees exclusive access to a record during a specific operation, preventing conflicts. It’s suitable for critical operations.

Cons: It can lead to decreased concurrency and responsiveness because other users/processes may be blocked while waiting for the lock to be released. It’s typically used sparingly for specific scenarios where exclusive access is necessary.

Optimistic locking is a strategy that assumes conflicts are rare and handles them at the point of saving changes, while pessimistic locking is a strategy that assumes conflicts are more likely and locks records explicitly to prevent concurrent access. The choice between them depends on the specific requirements and characteristics of your application and the operations you want to perform on the data.

I appreciate you taking the time to read this. Please follow me on Medium and subscribe to receive access to exclusive content in order to keep in touch and continue the discussion. Happy Reading..!

--

--

Gokul

Consultant | Freelancer | Ruby on Rails | ReactJS