Resolving Rails Runtime Errors: A Guide to Handling Unpersisted Changes with with_lock

Search for a command to run...

No comments yet. Be the first to comment.
PostgreSQL Upgrade and Database Restoration Guide To support the pgvector gem, I upgraded my local Postgres.app from v13 to v17 on macOS. Below is the full upgrade and restoration process for my project. Environment OS: macOS PostgreSQL: Upgraded f...

Environment MacOS Sonoma 14.3 Original Ruby version: 2.7.8 Ruby Version Manager: RVM 1.29.12 Goal Upgrade Ruby from 2.7.8 to 3.0.6. Issues Description After successfully installing Ruby 3.0.6, I encountered difficulties running bundle install in...

It is common to see require 'something' in a Rails application. But what exactly does require do? What is require used for in Ruby? require is used to load external libraries or modules into your program. For example, Before you require the JSON modu...

In my daily job, I mainly use Rails framework. To make code readable and maintainable, we seldom write raw SQL in the codebase. Instead, we use Rails's ActiveRecord::QueryMethods module which helps developers write beautiful queries quickly. For exam...

In Ruby on Rails applications, handling database transactions efficiently is crucial to maintaining data integrity. One common issue developers face is dealing with runtime errors caused by unpersisted changes when using the with_lock method. This guide will help you understand and resolve these errors effectively.
Ruby 3.0.7
Rails 6.1.7.7
RunTimeError: Locking a record with unpersisted changes is not supported.
Use save to persist the changes, or reload to discard them explicitly.
RunTimeError: Locking a record with unpersisted changes is not supported. Use save to persist the changes, or reload to discard them explicitly.
class Invoice < ApplicationRecord
before_save :sync_monetary_columns
# Existing code...
private
def sync_monetary_columns
return unless amount_changed?
with_lock do
self.amount_decimal = amount.to_f
end
end
end
invoice = Invoice.last
invoice.amount # 1000
invoice.amount = 500
invoice.save!
# RuntimeError: Locking a record with unpersisted changes is not supported.
# Use `save` to persist the changes, or `reload` to discard them explicitly.
with_lock Workswith_lock is a Rails method that provides pessimistic locking, which is a database-level lock that prevents multiple processes from modifying the same record simultaneously. Here's how it works:
When with_lock is called, Rails:
Starts a database transaction
Reloads the record with SELECT ... FOR UPDATE
Executes your block of code
Commits the transaction
The FOR UPDATE lock means:
Other transactions must wait to read or modify this record
Prevents race conditions where multiple processes might try to update the same data
Lock is held until the transaction commits or rolls back
def with_lock
transaction do
# Reloads the object and obtains lock:
self.reload(lock: true) # SELECT * FROM table WHERE id = 123 FOR UPDATE
# Now yields to your block:
yield
# Transaction commits after your block completes
end
end
To fix the runtime error caused by unpersisted changes, here’s my key implementation details:
Cache Values Before with_lock:
with_lock internally calls reload, which would clear any unpersisted changes. (Source code from Rails)
Caching preserves the values we need to modify.
Reload Only Persisted Records:
For persisted records: with_lock executes reload(lock: true) using the record's ID. When a record has unpersisted changes, this causes the RuntimeError.
For new records: No ID exists yet, so reload cannot trigger the error.
This solution ensures proper handling of both new and persisted records while maintaining data integrity during the locking process.
class Invoice < ApplicationRecord
before_save :sync_monetary_columns
# Existing code...
private
def sync_monetary_columns
return unless amount_changed?
# Cache values
new_amount = amount.to_of
reload if persisted?
with_lock do
self.amount_decimal = new_amount
end
end
end
By understanding the mechanics of with_lock and implementing the suggested solutions, you can effectively manage runtime errors related to unpersisted changes in your Rails applications. This approach not only resolves the issue but also enhances the robustness of your data handling processes.