I can’t claim any credit for this piece of lovely insight, it all belongs to JAKUB GŁUSZECKI who’s basically written exactly what I’m about to replicate here. However it’s a sweet solution and I wanted to share it.
The problem
You’ve got a bunch of model attributes that you need to store in the database but don’t really deserve their own column. For instance, there are a number of client-specific strings associated with the Q&A panels on Pingpanel. At the time of writing you can see these at diy-advice.ratedpeople.com.
The descriptions of what the panel is about, the types of questions you can ask there etc. all belong in the database but don’t really merit messing up the schema by occupying a complete column.
What I want to do is to store all of these model attributes (which will never need searching or indexing) in lump which I can pull or update when I need to.
The solution part 1: Serialize
Rails offers a nice serialize method which will roll up a structured object (e.g. a hash), put it into a database column and then unroll it back into the original object when you need it.
class Org
serialize :descriptions
end
This now allows me to dump an arbitrary array of attributes into descriptions which can be added to or altered at future dates.
Solution part 2: OpenStruct
That’s all well and good but I’d prefer to use object notation for accessing the attributes so instead of it being:
@org.descriptions[‘strapline’]
I would prefer:
@org.descriptions.strapline
Happily, serialize allows us to enforce that only a particular class can be passed and OpenStruct is a class that basically does exactly what I need. It wraps a hash up into object notation:
class Org
…
serialize :descriptions, OpenStruct
end
which lets us do
@org.descriptions.strapline
Solution part 3: Struct
Now although I don’t want to mess the database up with arbitrary columns I still want some accountability. I’m going to have a problematic time debugging if I can read and write whatever I chose.
Let’s say I pump
@org.descriptions.strapline
into the view but save
@org.descriptions.tagline
back into the database. I’ve got a setup which looks sensible in both places but is evidently going to lead to problems very quickly. OpenStruct will do this without blinking which is a little dangerous.
The solution is Struct which is the same thing as OpenStruct but with a limited set of defined attributes.
We can create a new class derived from Struct with the attributes we want and then pass it to serialize as the enforced object:
class Org
…
class Descriptions < Struct.new(:strapline, :panel_intro)
end
serialize :descriptions, Descriptions
end
The new Descriptions object will allow @descriptions.strapline to be set and read but will throw an error if you try to write @descriptions.tagline.
Edit: Migrations. Beware.
On further thinking, while this approach has its strengths, it also has one serious weakness which is migrations and specifically what happens if you change the name of the attributes on the Struct.
The struct is stored as a string in the database:
”#<struct Org::Guidance strapline=nil, panel_intro=nil>”
When you change the name of ‘strapline’ to ‘tagline’ none of the existing, serialized strings in the database get changed (i.e. the data’s still there) but we’ll find that when we load our object the new @org.strapline attribute is now set to nil.
As soon as we save @org the previous string will be overwritten and our old data is lost.
What to do about this? TBH I don’t know. You could always write some sort of migration script but with that decoupled from the migration itself (i.e. just changing the model) we’re in dangerous territory. Perhaps it’s just best to stick with to good tools ActiveRecord has given us. Thoughts appreciated.