Skip to content

DmitryTsepelev/store_model

master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
December 28, 2022 11:39
bin
February 20, 2019 15:45
June 13, 2022 18:51
lib
January 21, 2023 00:36
February 20, 2019 15:45
March 28, 2021 12:28
January 21, 2023 00:36
February 20, 2019 15:45
December 1, 2022 13:37
February 20, 2019 15:45

StoreModel Gem Version Coverage Status

StoreModel gem allows you to wrap JSON-backed DB columns with ActiveModel-like classes.

  • 💪 Powered with Attributes API. You can use a number of familiar types or write your own
  • 🔧 Works like ActiveModel. Validations, enums and nested attributes work very similar to APIs provided by Rails
  • 1️⃣ Follows single responsibility principle. Keep the logic around the data stored in a JSON column separated from the model
  • 👷‍♂️ Born in production.
class Configuration
  include StoreModel::Model

  attribute :model, :string
  enum :status, %i[active archived], default: :active

  validates :model, :status, presence: true
end

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type
end

Why should I wrap my JSON columns?

Imagine that you have a model Product with a jsonb column called configuration. This is how you likely gonna work with this column:

product = Product.find(params[:id])
if product.configuration["model"] == "spaceship"
  product.configuration["color"] = "red"
end
product.save

This approach works fine when you don't have a lot of keys with logic around them and just read the data. However, when you start working with that data more intensively–you may find the code a bit verbose and error-prone.

For instance, try to find a way to validate :model value to be required. Despite of the fact, that you'll have to write this validation by hand, it violates the single-responsibility principle: why parent model (Product) should know about the logic related to a child (Configuration)?

📖 Read more about the motivation in the Wrapping JSON-based ActiveRecord attributes with classes post

Getting started

Start with creating a class for representing the hash as an object:

class Configuration
  include StoreModel::Model

  attribute :model, :string
  attribute :color, :string
end

Attributes should be defined using Rails Attributes API. There is a number of types available out of the box, and you can always extend the type system.

Register the field in the ActiveRecord model class:

class Product < ApplicationRecord
  attribute :configuration, Configuration.to_type
end

When you're done, the initial snippet could be rewritten in the following way:

product = Product.find(params[:id])
if product.configuration.model == "spaceship"
  product.configuration.color = "red"
end
product.save

Usage note: Rails and assigning Arrays/Hashes to records

  • Assigned attributes must be a String, Hash, Array of Hashes, or StoreModel. For example, if the attributes are coming from a controller, be sure to convert any ActionController::Parameters as needed.
  • Any changes made to a StoreModel instance requires the attribute be re-assigned; Rails doesn't track mutations of objects. For example: self.my_stored_models = my_stored_models.map(&:as_json)

Documentation

  1. Installation
  2. StoreModel::Model API:
  1. Array of stored models
  2. One of
  3. Alternatives
  4. Defining custom types

Credits

Initially sponsored by Evil Martians.

License

The gem is available as open source under the terms of the MIT License.