Rails - Maintenance hash/JSON column with a customize serializer

23 Sep 2020

Sometimes, you may often meet some requirements need you to save a hash or JSON to one column. Most time, you may use Rails default serialie to serialize it with YAML or JSON and save it to the column like this:

class User < ApplicationRecord
  serialize :settings
end

user.settings = {
  a: 1,
  b: 2,
  c: 3
}
user.save

This kind of simple design has a big problem which will increase the difficulty of maintenance. The key values in the column will change in all sorts of the way over time. Maybe after several years, you can’t tell what actually save in the columns. You have to check every record in the online database.

You may ask, what caused this happens. Recently, I also met this kind of issue. And I can tell the anwser is simple. It’s because you don’t have a schema for that column. You need a way to maintenance the data structure in your codebase not database.

My way for it is one of Rails way - customize serializer

class User < ApplicationRecord
  serialize :settings, DescriptionSerializer
end

class DescriptionSerializer
  class << self
    def load(json_string)
      return unless json_string.present?

      hash = JSON.load(json_string).with_indifferent_access
      UserSettings.new(hash)
    end

    def dump(description)
      JSON.dump description.dump
    end
  end
end

class UserSettings < BaseJsonColumn
  persistent_attributes :a, :b, :c, :d
end

class BaseJsonColumn

  class << self
    def persistent_attributes(*names)
      self.class_variable_set(:@@__persistent_attributes, names.map(&:to_sym))

      names.each do |name|
        define_method name do
          attributes[name]
        end

        define_method "#{name}=" do |value|
          attributes[name] = value
        end
      end
    end

    def persistent_attribute_names
      self.class_variable_get(:@@__persistent_attributes)
    end
  end

  attr_reader :attributes

  def initialize(attributes = {})
    @attributes = attributes.with_indifferent_access
  end

  def dump
    @attributes.select do |name, _|
      persistent_attribute_names.include?(name.to_sym)
    end
  end

  def persistent_attribute_names
    self.class.persistent_attribute_names
  end

end

Now, you can keep your eyes on the JSON/hash column structure with UserSettings class. Each time you can put other data in it, you have to go back to here to deal with it and change it explicitly. Then you will never lose control on this kind of columns.

Back to top