David Gay
David Gay

Reputation: 1154

In Rails, what are the disadvantages of using model methods for constants, instead of Active Record columns?

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

Answers (2)

Les Nightingill
Les Nightingill

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

Kache
Kache

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.

Just Put the Data in the Relational Database

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.

I Really Don't Want to Put Data in the Database

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

Related Questions