Reputation: 1154
I'm building a Rails app that will have a very high number of models using single-table inheritance. For the model subclasses, I want to be able to set constant values for things like name
and description
. I know I can set defaults using attribute
, like this:
class SpecialWidget < Widget
attribute :name, :string, default: "Special Widget"
attribute :description, :text, default: "This is an awfully important widget."
end
The advantage here, as I understand it, is that by storing the defaults in the database, I retain the ability to do things use #order
to sort by name, and paginate. But it seems bad to store constants in the database like that. It seems better to use constant methods, like this:
class SpecialWidget < Widget
def name
"Special Widget"
end
def description
"This is an awfully important widget."
end
end
In fact, that's what I was doing originally, but then I read posts like these (one, two, three), which pointed out that then if I wanted to do nice things like sort by the methods, I'd have to load the entire Widget.all
into memory and then do a plain-old Ruby sort.
My application is built quite heavily around these STI models, and I will definitely have to sort by constants like name
. Are the concerns about sorting and pagination significant disadvantages that will cause me to come to regret using methods in the future, or will the difference be negligible? What other disadvantages/problems might I have? I'd really like to be able to use methods instead of storing constants in the database, if possible without crippling my app's performance.
Upvotes: 3
Views: 320
Reputation: 6156
There are many benefits and few downsides to storing the default values in the database. But if it troubles you, you can have similar sorting efficiency by constructing your sort like this:
class SpecialWidget < Widget
DefaultAttrs = {name: 'Special Widget', description: 'This is... etc'}
end
class Widget < ApplicationRecord
def self.sort_by_name
types = pluck(:type).uniq
case_statements = types.map{|type| "WHEN '#{type}` THEN `#{type.constantize.const_get(:'DefaultAttrs')[:name]}'"
case_sql = "CASE type #{case_statements.join(' ') END"
order(case_sql)
end
end
... not very elegant, but it does the job!
maybe better to put the constants in the database!
Upvotes: 2
Reputation: 16687
It depends entirely on the shape of your data and how you want to use it. You haven't provided enough contextual specifics to guarantee that my recommendation applies to your situation, but it's a recommendation that's specifically designed to work for 95+% of all situations.
The database is the store for all things in your domain that is dynamic and needs to be persisted, i.e. state. It should be internally consistent, meaningfully self-descriptive, and well-structured in order to fully leverage the power of a relational db to flexibly manipulate and represent complex inter-related data.
Based on what you've said, and assuming that there are a bunch of different "widget types" implemented using Rail's STI implementation with a type
column, I would model Widget
and SpecialWidget
in the database like this:
widgets
id | type
-------------------
1 | 'Widget'
2 | 'SpecialWidget'
3 | 'Widget'
4 | 'Widget'
widget_types
type | name | description
--------------------------------------------------------------
'Widget' | 'Normal Widget' | 'A normal widget.'
'SpecialWidget' | 'Special Widget' | 'This is an awfully important widget.'
You called these values a "constant", but are they really? In the purposes of your domain, will they never change like the value of Matth::PI
never changes? Or will descriptions be changed, widgets renamed, widgets added, and widgets expired? Without knowing for sure I'm going to assume they're not actually Constant
.
Having name
and description
as methods is effectively storing that widget_types
table in your application source code, moving data out of your database. If you really can't afford the extra millisecond a simple JOIN
for two small strings on each Widget
incurs, then just load the full widget_types
table into cache once on application startup, and it'll perform the same as saving it in source code.
This schema is more normalized (incurring benefits), the data itself describes all I need to know, and as you've pointed out, I can flexibly operate on that data (important since you "will definitely have to sort"). The data in this form is also extensible for future changes as they come.
Again: the database stores structured data for the purpose of on-demand flexible manipulation -- you can make up queries on the fly, and the DB can answer it.
Okay... then you'll have to pass that data into the database every time you want to operate on it. You can do it like so:
SELECT w.id, w.type, wt.name
FROM widgets w
INNER JOIN (
VALUES ('Widget', 'Normal Widget'), ('SpecialWidget', 'Special Widget')
) wt(type, name) ON wt.type = w.type
ORDER BY wt.name
The VALUES
expression creates an ad-hoc table mapping the class to the name. By passing in that mapping and joining on it (every time), you can tell the DB to ORDER BY
it.
Upvotes: 1