Reputation: 488
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
Rule
.RuleConditions
from a
subset of the params for a rule.Rule.new(rule)
where rule contains :conditions => RuleCondition
rule.save!
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
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