Skip to content

Instantly share code, notes, and snippets.

@choncou
Last active June 15, 2021 12:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save choncou/ed77efabdea92e38833839c974b35eee to your computer and use it in GitHub Desktop.
Save choncou/ed77efabdea92e38833839c974b35eee to your computer and use it in GitHub Desktop.
Optimistic Locking in Rails: Preventing outdated updates
# Run with `ruby rails_optimistic_locking_example.rb`
#
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# gem "rails", github: "rails/rails", branch: "main"
gem 'rails', '~> 6.1', '>= 6.1.3.2'
gem "sqlite3"
end
require "active_record"
require "minitest/autorun"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
# ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :products, force: true do |t|
t.string :name
t.string :description
t.integer :weight_grams
t.integer :price_cents
end
create_table :product_with_lockings, force: true do |t|
t.string :name
t.string :description
t.integer :weight_grams
t.integer :price_cents
t.integer :lock_version
end
end
class Product < ActiveRecord::Base
end
class ProductWithLocking < ActiveRecord::Base
end
class ProductUpdateTest < Minitest::Test
PERMITTED_PARAM_NAMES = ['name', 'description', 'weight_grams', 'price_cents']
def setup
#1 We create a new product
@new_product = Product.create(name: "Book")
# Data loaded in the form of edit pages after creation
edit_page_form = @new_product.attributes.slice(*PERMITTED_PARAM_NAMES)
#2 Someone updates that product
Product
.find(@new_product.id)
.update!(edit_page_form.merge( # Like handling submitted form data
'description' => 'The greatest story ever told',
'price_cents' => 70_00
))
#3 We also update the product on our old tab
Product
.find(@new_product.id)
.update!(edit_page_form.merge('weight_grams' => 314))
@actual_product = Product.find(@new_product.id)
end
def test_product_name_saved
refute_nil @actual_product.name # => ✅
end
def test_product_weight_saved
refute_nil @actual_product.weight_grams # => ✅
end
def test_product_description_saved
refute_nil @actual_product.description # => ❌
end
def test_product_price_saved
refute_nil @actual_product.price_cents # => ❌
end
end
class ProductWithLockingUpdateTest < Minitest::Test
# IMPORTANT: `lock_version` should be included in the form data
PERMITTED_PARAM_NAMES = ['name', 'description', 'weight_grams', 'price_cents', 'lock_version']
def setup
#1 We create a new product
@new_product = ProductWithLocking.create(name: "Book")
# Data loaded in the form of edit pages after creation
@edit_page_form = @new_product.attributes.slice(*PERMITTED_PARAM_NAMES)
#2 Someone updates that product
ProductWithLocking
.find(@new_product.id)
.update!(@edit_page_form.merge( # Like handling submitted form data
'description' => 'The greatest story ever told',
'price_cents' => 70_00
))
end
# Now you aren't allowed to update if the version doesn't match
def test_updating_with_old_version
assert_raises ActiveRecord::StaleObjectError do
ProductWithLocking
.find(@new_product.id)
.update(@edit_page_form.merge('weight_grams' => 314))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment