Use dedicated sequence record instead of deal with issues with ids and uuids when using the previous approach

This commit is contained in:
Jorge Manrubia
2025-11-27 11:55:47 +01:00
parent 07b4c55185
commit 2061ab4ab2
8 changed files with 91 additions and 28 deletions
+1 -1
View File
@@ -39,6 +39,6 @@ class Account < ApplicationRecord
private
def assign_external_account_id
self.external_account_id ||= ExternalAccount.create!.id
self.external_account_id ||= ExternalIdSequence.next
end
end
@@ -0,0 +1,26 @@
# Provides sequential IDs for +external_account_id+ when creating accounts without one.
class Account::ExternalIdSequence < ApplicationRecord
class << self
def next
with_lock do |sequence|
sequence.increment!(:value).value
end
end
def value
first&.value
end
private
def with_lock
transaction do
sequence = lock.first_or_create!(value: initial_value)
yield sequence
end
end
def initial_value
Account.maximum(:external_account_id) || 0
end
end
end
-5
View File
@@ -1,5 +0,0 @@
# This is meant to provide a sequential ID for +external_account_id+ when creating
# accounts without one. The SaaS version of the app uses that column, but most apps
# can just go with the default handling here.
class ExternalAccount < ApplicationRecord
end
@@ -0,0 +1,9 @@
class CreateAccountExternalIdSequences < ActiveRecord::Migration[8.0]
def change
create_table :account_external_id_sequences do |t|
t.bigint :value, null: false, default: 0
t.index :value, unique: true
end
end
end
@@ -1,15 +0,0 @@
class CreateExternalAccounts < ActiveRecord::Migration[8.0]
def up
create_table :external_accounts do |t|
end
max_id = Account.maximum(:external_account_id) || 0
if max_id > 0
execute "INSERT INTO external_accounts (id) VALUES (#{max_id})"
end
end
def down
drop_table :external_accounts
end
end
+6 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do
ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do
create_table "accesses", id: :uuid, force: :cascade do |t|
t.datetime "accessed_at"
t.uuid "account_id", null: false
@@ -25,6 +25,11 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_25_130010) do
t.index ["user_id"], name: "index_accesses_on_user_id"
end
create_table "account_external_id_sequences", force: :cascade do |t|
t.bigint "value", default: 0, null: false
t.index ["value"], name: "index_account_external_id_sequences_on_value", unique: true
end
create_table "account_join_codes", id: :uuid, force: :cascade do |t|
t.uuid "account_id", null: false
t.string "code", limit: 255, null: false
@@ -0,0 +1,42 @@
require "test_helper"
class Account::ExternalIdSequenceTest < ActiveSupport::TestCase
setup do
Account::ExternalIdSequence.delete_all
end
test ".next returns sequential values" do
first_value = Account::ExternalIdSequence.next
second_value = Account::ExternalIdSequence.next
third_value = Account::ExternalIdSequence.next
assert_equal first_value + 1, second_value
assert_equal second_value + 1, third_value
end
test ".next initializes from maximum external_account_id" do
max_id = Account.maximum(:external_account_id) || 0
first_value = Account::ExternalIdSequence.next
assert_equal max_id + 1, first_value
end
test ".next creates single sequence record" do
3.times { Account::ExternalIdSequence.next }
assert_equal 1, Account::ExternalIdSequence.count
end
test ".next is concurrency-safe" do
values = 20.times.map do
Thread.new do
Account::ExternalIdSequence.next
end
end.map(&:value)
assert_equal 20, values.uniq.size, "All values should be unique"
assert_equal values.min..values.max, values.sort.first..values.sort.last
assert_equal 20, values.max - values.min + 1, "Values should be sequential with no gaps"
end
end
+7 -6
View File
@@ -69,12 +69,13 @@ class AccountTest < ActiveSupport::TestCase
assert_equal account1.external_account_id + 1, account2.external_account_id
end
test "external_account_id can be overridden without creating sequence entry" do
custom_id = Account.last.id + 99999
test "external_account_id can be overridden" do
custom_id = 999999
sequence_value_before = Account::ExternalIdSequence.first_or_create!(value: 0).value
assert_no_difference -> { ExternalAccount.count } do
account = Account.create!(name: "Custom ID Account", external_account_id: custom_id)
assert_equal custom_id, account.external_account_id
end
account = Account.create!(name: "Custom ID Account", external_account_id: custom_id)
assert_equal custom_id, account.external_account_id
assert_equal sequence_value_before, Account::ExternalIdSequence.value
end
end