ActiveHash: A Rails Model Without a Database Table

Sometimes a Rails model doesn't need a database table behind it. That was the situation when I needed to store definitions for puzzle game levels — basically just a title and a cloud-init.yaml file. Keeping those in the database meant making updates through a web interface, which was too slow for the frequent changes I was making. It also created sync problems: the puzzles table in development and production kept drifting out of alignment.

The solution was ActiveHash, a gem that lets you define all of a model's data in a hash or file instead of a database table. With only about six puzzles, storing them in a file made them much easier to edit directly.

Defining the Puzzle Model

The resulting Puzzle class is straightforward:

class Puzzle < ActiveHash::Base
  def to_param
    "#{id}-#{slug}"
  end

  def finished?(user)
    PuzzleStatus.where(user_id: user.id).where(puzzle_id: self.id).first&.finished || false
  end

  def cloud_init
    File.read("puzzles/#{group}/#{slug}/cloud-init.yaml")
  end

  self.data = [
    {
      id: 1,
      group: "networking",
      slug: "connection-timeout",
      title: "The Case of the Connection Timeout",
      published: false,
    },
    ... more data here

One caveat: puzzle IDs are entered manually (e.g., id: 1), so it's important not to accidentally reuse or change them, since other database fields reference those IDs.

Working with ActiveRecord Methods

While belongs_to and has_many don't work with ActiveHash, standard query methods like Puzzle.find(id) do. That meant most existing code didn't need changes, and if the puzzles ever need to go back into the database, the ActiveRecord-style interface should make the switch relatively painless.

Migrating Away from the Database Table

Switching to ActiveHash required a few clean-up steps:

  • Removing all create/update/edit code from the Puzzles controller
  • Writing a migration to drop the puzzles table
  • Removing puzzles.yml fake data from tests, since it was inserting into a table that no longer existed

There were likely a few other small adjustments along the way, but the overall change was minimal and the resulting setup feels much easier to maintain.