Last Updated: September 27, 2021
·
4.62K
· aceofspades

Use UUID keys in Active Record

Identify and associate your ActiveRecord models using validated UUID's.

gem 'uuid'
gem 'activesupport'
gem 'activerecord'
gem 'sqlite3'

require 'active_support'
require 'active_record'
require 'uuid'

ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:";

ActiveRecord::Schema.define do
  self.verbose = false

  create_table :posts, id: false, primary_key: :uuid do |t|
    t.string :uuid, primary_key: true
    t.string :title
    t.text :content
    t.timestamps
  end
  add_index :posts, :uuid

  create_table :comments, id: false, primary_key: :uuid do |t|
    t.string :uuid, primary_key: true
    t.string :post_uuid
    t.text :content
    t.timestamps
  end
  add_index :comments, :uuid
  add_index :comments, :post_uuid
end

ActiveRecord::Reflection::AssociationReflection.class_eval do
  alias_method :derive_foreign_key_before_uses_uuids, :derive_foreign_key
  def derive_foreign_key
    key = derive_foreign_key_before_uses_uuids
    active_record.respond_to?(:uses_uuids?) && active_record.uses_uuids? ? key.sub(/_id$/, '_uuid') : key
  end
end

class UuidValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    Uuid.new(value).valid_uuid? or
      record.errors[attribute] << (options[:message] || "is not a valid UUID")
  end
end

class Uuid
  attr_reader :value

  def self.build(uuid)
    uuid.is_a?(Uuid) ? uuid : new(uuid)
  end

  def self.generate
    new(UUID.new.generate)
  end

  def initialize(value)
    @value = value.to_s
  end

  def valid_uuid?
    @value =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
  end

  alias_method :to_s, :value
end

module UsesUuids
  extend ActiveSupport::Concern

  module SetsAfterInitialize
    extend ActiveSupport::Concern
    included do
      after_initialize :set_uuid
    end
  end

  module ActiveRecord
    extend ActiveSupport::Concern

    included do
      validates_uniqueness_of :uuid
    end

    def write_attribute(attr_name, value)
      _value = value.is_a?(Uuid) ? value.to_s : value
      super(attr_name, _value)
    end

    def read_attribute_for_validation(key)
      value = super
      value.is_a?(Uuid) ? value.to_s : value
    end

    module ClassMethods
      def primary_key
        :uuid
      end

      def uses_uuids?
        true
      end
    end

  end

  included do
    validates :uuid, presence: true, uuid: true
  end

  def uuid
    return nil unless self[:uuid]
    @_uuid ||= Uuid.new(read_attribute(:uuid))
    fail unless @_uuid.valid_uuid?
    @_uuid
  end

  private

  def set_uuid
    self.uuid ||= Uuid.generate.to_s
  end

end

class Record < ActiveRecord::Base
  self.abstract_class = true
  include UsesUuids
  include UsesUuids::ActiveRecord
  include UsesUuids::SetsAfterInitialize
end

class Post < Record
  has_many :comments
end

class Comment < Record
  belongs_to :post
end

post = Post.new 
# => #<Post uuid: "e2cc2bd0-6d6e-0131-2bc8-38f6b1147145", title: nil, content: nil, created_at: nil, updated_at: nil>

Post.joins(:comments).to_sql
# => "SELECT \"posts\".* FROM \"posts\" INNER JOIN \"comments\" ON \"comments\".\"post_uuid\" = \"posts\".\"uuid\""

Another noteworthy implementation: https://github.com/jashmenn/activeuuid

2 Responses
Add your response

Activerecord 4 support uuid by default.

over 1 year ago ·
over 1 year ago ·