chr0nikler
chr0nikler

Reputation: 488

Rails nested model conversion to and from database without its own table

Background: I'm trying to refactor my code after reading Practical Object Oriented Design in Ruby (it's awesome), and in doing so, I want to introduce some more models that encapsulate responsibility, rather than have a single large file with logic (and case statements, for that).

Problem: To simplify the problem statement, I have a model Rule that "has many" RuleConditions. However, there is only one table in the database for rules. In it, I have a column for conditions that's of type jsonb (based on the complications of a RuleCondition). But I can't seem to accomplish this. Specifically I can't figure out how to instantiate a model with a nested model, and expect ActiveRecord to know how to convert the model to jsonb, and perhaps from the table back to a nested model. I also don't know if I can define a has_many relationship without a table backing it using ActiveRecord.

What I expect:

I expect that that there should be some flow (defined by a mix of ActiveRecord and ActiveModel) that would make this flow possible

  1. Get params for a Rule.
  2. Create a new array of RuleConditions from a subset of the params for a rule.
  3. Do Rule.new(rule) where rule contains :conditions => RuleCondition
  4. Do rule.save!
  5. At some point later, fetch the rule from the table, and expect it to rebuild a Rule with the nested RuleConditions model from the conditions attribute.

What I've tried:

What I thought would get me halfway there was the serialize, :conditions, JSON, but it struggles to serialize my object. After that, I really don't know. I've played around with ActiveModel::Conversion as well. So I just need some guidance.

And, to be perfectly clear, calling as_json on my RuleCondition works like I expect it to (prints out the same JSON that used to be stored in the Rule model and the database before attempting a refactor). So it's maybe that I don't understand serialize (since it's supposed to YAML unless otherwise, I think the encoding is different than just "match my column type")

Edit:

Currently I have something like (barebones, 0 validations / associations)

class Rule < ActiveRecord::Base
end

class RuleController < ApplicationController

    def create
        rule = Rule.new(rule_params[:rule]) # conditions are just an attribute in the params
        rule.save
    end
end

Now, with the new model that's defined as

class RuleCondition
    include ActiveModel::Model # (what I'm currently doing to get some of the behavior of a model without the persistence / table backing it, I think) 
    attr_accessor :noun, :subnoun # etc
end

I'm thinking I need do this

def create
    rule = rule_params[:rule]
    rule["conditions"] = rule["conditions"].map do |c|
        RuleCondition.new(c)
    end
    true_rule = Rule.new(rule)
    true_rule.save!
end

But this doesn't work, for (exactly) this reason:

18:13:52 web.1 | SQL (10.7ms) INSERT INTO "rules" ("name", "conditions", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "wefw"], ["conditions", "{#}"], ["created_at", "2018-12-16 02:13:52.938849"], ["updated_at", "2018-12-16 02:13:52.938849"]] 18:13:52 web.1 | PG::InvalidTextRepresentation: ERROR: invalid input syntax for type json 18:13:52 web.1 | DETAIL: Token "#" is invalid. 18:13:52 web.1 | CONTEXT: JSON data, line 1: #... 18:13:52 web.1 | : INSERT INTO "rules" ("name", "conditions", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" 18:13:52 web.1 | (0.5ms) ROLLBACK

Upvotes: 1

Views: 418

Answers (1)

max
max

Reputation: 101891

Keep in mind that database adapters handle certain serialization tasks for you. For instance: json and jsonb types in PostgreSQL will be converted between JSON object/array syntax and Ruby Hash or Array objects transparently. There is no need to use serialize in this case.
- api.rubyonrails.org

Don't use serialize with native JSON/JSONB columns. Its meant to be used with string columns as a poor-mans alternative.

What you are trying to do is really outside the scope of what ActiveRecord does - AR is built around a relational model where models correspond to tables. And you cannot expect that AR will have any provisions to unmarshal a JSONB column into anything but basic scalar types. And I would consider if what you are doing is really worth the effort vs actually creating a separate table for the relation.

You are on the right track with ActiveModel::Model which will give your model the same behaviour as a regular model, but you should take a look at how ActiveRecord handles nested attributes:

class Rule < ApplicationRecord
  def conditions_attributes=(attributes)
    attributes.each do |a|
      # you would need to implement this method
      unless RuleCondition.reject_attributes?(a)
        self.conditions << RuleCondition.new(c)
      end
    end
  end
end

You can possibly mimic the other aspects of an association by creating setters/getters.

But then again you could just create a rule_conditions table with JSONB column and a one to many or m2m association and spend your time actually being productive instead.

Upvotes: 2

Related Questions