Skip to content

Instantly share code, notes, and snippets.

@somazx
Last active May 3, 2022 14:25
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save somazx/17a6333858a143c38eb79952fd4957c1 to your computer and use it in GitHub Desktop.
Save somazx/17a6333858a143c38eb79952fd4957c1 to your computer and use it in GitHub Desktop.
Working with nested, serialized dataobjects in Rails

Rails and serialized nested DataObjects

I wanted to store arbitrary nested data structures in a jsonb column and be able to retrieve and work with that data seemlessly within a Rails application. There are several tutorials covering different aspects of this, but none felt complete to me.

For this example we'll be working with a User model that can define surveys. Each survey consists of n multiple choices questions, and each question can have between two and ten answer options.

Requirements:

In this case we use Postgresql's JSONB support to store the data, but you could also use other forms of serializing such as a text column and Rails' default YAML serializing. JSONB is chosen in this case because it leaves open the possiblity of querying the data with SQL. Also, apparently serializing to JSON is much faster.[1]

I use the virtus gem to easily define the data attributes on each data object.

The walkthrough assumes you have a User model.

User model

We'll assume you already have a user model; begin by creating a rails migration that adds a jsonb column to the users table:

add_column :users, :survey_data, :jsonb, default: []

The next step is to add a line to our User model that will serialize and unserialize the survey data to a SurveyCollection instance:

app/models/user.rb:

class User < ActiveRecord::Base
  serialize :survey_data, SurveyCollection
  delegate :surveys, to: :survey_data
end

Serializing/Unserializing

We define a SurveyCollection model to handle the serializing and unserializing of the data. It is also likely you would add additional methods or validations here, for example you might limit the number of surveys a user can have.

SurveyCollection model will look like this: app/models/survey_collection.rb:

class SurveyCollection
  include Virtus.model
  include ActiveModel::Model

  attribute :surveys, Array[Survey]

  class << self
    def load(data)
      self.new(data)
    end

    def dump(data)
      data.to_json
    end
  end
end

Lets review this class definition. The first two lines includes the Virtus.model and ActiveModel. Virtus allows us to easily define attrtibutes of specific types with coercion. For example an attribute :age, Integer will ensure age="13" is converted to Integer value 13.

Including ActiveModel::Model helps our class to be more compaible within the Rails framework, and adds support for ActiveRecord validations; methods such as: #valid? and #errors.

Line 30: attribute :questions, Array[SurveyQuestion] is similar to the serialize line in the user model. It maps our json questions object to an array of SurveyQuestion instances. Virtus provides this functionality.

Finally, we define the load and dump class methods which are called when serializing and unserializing the data.

DataObjects

We still need to define our, Survey, SurveyQuestion and SurveyAnswerOption classes:

app/models/survey.rb

class Survey
  include Virtus.model
  include ActiveModel::Model

  attribute :name
  attribute :questions, Array[SurveyQuestion]

  validates_presence_of :name
end

app/models/survey_question.rb:

class SurveyQuestion
  include Virtus.model
  include ActiveModel::Model

  attribute :text, String # "What is 2 + 2?"
  attribute :answer_options, Array[SurveyAnswerOption]

  validates_presence_of :text
  validates_length_of :answer_options, within: 2..10,
        too_long: 'maximum is 10', too_short: 'minimum is 2'
end

app/models/survey_answer_option.rb:

class SurveyAnswerOption
  include Virtus.model
  include ActiveModel::Model

  attribute :text, String
  validates_presence_of :text
end

The code is very similar to the SurveyCollection class. We added a couple validations to ensure the question text is present, and that there are a sane number of answer options.

Working with the DataObjects

Now working with this data is really easy and straight forward and much like if we had used typical rails models backed by indiviudal surveys, questions, answer_options tables.

Print out all survey names: user.surveys.each { |s| puts s.name }

Get a list of the answer option text in a question: user.surveys[0].questions[0].answer_options.collect(&:text)

Forms, Validations & Strong Parameters

Coming soon!

References

  1. http://pjkh.com/articles/postgresql-json-vs-rails-serialize/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment