1

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

chr0nikler
  • 468
  • 4
  • 13
  • can you summarise what you want, like specific questions? – kasperite Dec 16 '18 at 03:22
  • The question isn't very clear so if you could narrow down what you're looking for that would help. Can you tell us what database you're using, and confirm that you ran your migration with something like `add_column :rules, :conditions, :json` so it created the proper migration in the database? That might be why you're struggling serializing your data – Jay Dorsey Dec 16 '18 at 04:45
  • Clarifying question: you currently have one model called `Rule`. That model contains an attribute called `rules_conditions` that is currently serialized into a `jsonb` field. This is all working correctly, today. Correct? – aridlehoover Dec 16 '18 at 05:16
  • @aridlehoover, that's exactly right. I'll add a little code example to demonstrate. – chr0nikler Dec 16 '18 at 08:31
  • You don't want to use serialize with native JSON column types. Its a hack from back in days before we had native JSON/HStore/Array types that uses a string column to store JSON/Yaml strings and marshals/unmarshals the data in Ruby. https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html – max Dec 16 '18 at 11:32

1 Answers1

2

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.

max
  • 96,212
  • 14
  • 104
  • 165