How to use a custom primary_key attribute when attribute has custom type registered

Viewed 46

I'm building a Rails 6 app on top of a database whose schema cannot be altered, as the database is shared across multiple legacy apps.

Our Product class has a string code attribute which is composed of various parts. Using a custom object and Rails' attribute API, we've been deserialising the code into its constituent parts:

# config/initializers/active_record_types.rb
ActiveRecord::Type.register(:product_code, ProductCodeType)

# app/types/product_code_type.rb
class ProductCodeType < ActiveModel::Type::String
  def cast(value)
    return if value.nil?

    ProductCode.new(value)
  end

  def serialize(value)
    value.to_s
  end
end

# app/types/product_code.rb
class ProductCode
  PATTERN = /<<REGEX HERE>>/.freeze

  attr_reader :family_code, :version_code, :flags

  def initialize(source)
    matches = source.match(PATTERN)

    @family_code = matches[:family_code]
    @version_code = matches[:version_code]
    @flags = matches[:flags]
  end

  def to_s
    [family_code, version_code, flags].join
  end
end

This all works great, and allows us to greatly simplify a lot of code throughout the app.

The problem comes in another part of the codebase, where we have a table that joins to products via the code column:

# in another app (Rails 4) using the same DB

# product.rb
class Product < ActiveRecord::Base
  has_one :interest_rate, foreign_key: :code, primary_key: :code
end

But if we replicate that in the new app where code is deserialized:

# product.rb
class Product < ApplicationRecord
  has_one :interest_rate, foreign_key: :code, primary_key: :code
end

% p = Product.find 1357
# => #<Product:0x00007ff0941c2aa0 ...>
% p.interest_rate
# => TypeError: can't quote ProductCode
from /bundle/ruby/2.7.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/quoting.rb:231:in `_quote'

Is there a way to be able to deserialize code for most uses, while also being able to use its serialized string form as a key for this join?

1 Answers

This feels like a copout to a certain extent, but we ended up not automatically deserializing code in this way.

We left code as a String, which meant it could be used as part of the join, and added a product_code method which deserialized the code when needed:

def product_code
  @product_code ||= ProductCode.new(code)
end

Most of our use for the product code is to identify attributes of the product that aren't expressible in other ways; so we might call product.product_code.flexible?, for example.

By delegating these methods (delegate :flexible?, to: :product_code) we end up not referencing the product code directly anyway – we can call product.flexible? when needed.

This means that having both code and product_code as attributes of a product doesn't cause confusion.

Related