This is the fourth and last part of my guide on how to build a simple Rails clone from scratch, using Rack. In this post we will focus on the last missing component of our framework: models and pesistence. You can check part one for an intro to Rack, part two for the implementation of request routing and controllers, and part three for dynamic template rendering.

The usual disclaimer applies here: because this is a naive Rails clone, the code doesn’t aim to be performant or secure, but rather explicit and easy to understand. Think twice before using it directly in production.

Where we left off and what to do next

In the previous part we added view templates that are rendering values extracted from @instance variables from the controllers. Even though at that point we didn’t yet have model or record objects to pass around, we used OpenStruct instances to build objects that could then be rendered in the templates.

We basically used the same outside in approach already described in part 2. Each time we’ve worked on an element of the application, we’ve stubbed the desired interface of its collaborators. Implementing the template rendering logic of our MVC framework required a way to pass structured data from the Controller to the View, and we did so by defining what the API of our future Model should look like. Now that the rest of the moving parts are there and working fine, all we have to do is provide a real Model implementation for that OpenStruct-backed interface.

Models, persistence and interfaces

Before diving into the code, let’s stop for a moment and think about what we need.

We already have pages to display a single dog object and a list of dogs. The interface of a Dog is already defined, and we don’t want to modify those templates. The template for the HTML form to create a new dog is still a placeholder and we will implement it later.
We know that dog objects should respond to an id method returning an integer and a name method returning a string, and we know that we need a way to collect them in an Enumerable. We also need a way to create new dog objects populated with user input (just the name, to keep things simple) and we need to persist them somewhere and load them later.

Rails deals with these requirements by using a relational database (e.g. PostgreSQL, MySQL or SQLite) and the Active Record pattern, so that model classes provide a repository-like interface to load and search for records, and each model instance privides an API to persist (create or update) and delete its data.

Let’s try to implement the same interface, but without using a relational database.
Using a full-blown DB like PostgreSQL or MySQL would be too complex for our simple exercise, and while SQLite would be a nice fit, I’d still prefer something simpler.
With any of the mentioned SQL stores, we would have to hand over the persistence logic to the adapter library; even though we would write the SQL statements to support our modeling, I would rather use a simpler (and more limited) solution without “black box” parts.
Also, I would like to take the opportunity to show that it’s possible to implement an Active Record interface and simple CRUD features without an underlying SQL storage. This is important because it shows how clear abstractions (the active record interface) allow us to later replace the implementation.

With this in mind, and to use something available in the Standard Library, we’re going to build the persistence functionality on the PStore class. PStore allows us to store arbitrary Ruby objects in a file and retrieve them by key, as long as they can be marshaled.

The Model base class

Even though in software design the concept of “model” is not associated to “persistence”, in a vanilla Rails application the models directory is reserved for ActiveRecord::Base subclasses.
Since so far we have mimicked the directory structure and the naming conventions of a Rails application, we will do the same for the models.

We are thus adding an app/models/ directory to our application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rack_demo/
├─ app/
│  ├─ controllers/
│  │  ├─ base_controller.rb
│  │  └─ dogs_controller.rb
│  ├─ models/
│  │  ├─ base.rb
│  │  └─ dog.rb
│  ├─ views/
│  │  ├─ base/
│  │  │  ├─ _head.html.erb
│  │  │  └─ index.html.erb
│  │  └─ dogs/
│  │     ├─ index.html.erb
│  │     ├─ new.html.erb
│  │     └─ show.html.rb
│  └─ router.rb
├─ application.rb
└─ rack_demo.ru

There, base.rb will be an abstract parent class that contains the common logic that all our models should have, and dog.rb will be a concrete implementation we will used in the DogsController.

Let’s have a look at the implementation for base.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
require 'pstore'

class Base
  DB_FILE = File.expand_path("../../../db.pstore", __FILE__)

  module ClassMethods

    # Find a record by ID
    #
    def find(id)
      db.transaction(true) do
        db[derive_db_id(self.name, id)]
      end
    end

    # Returns all records for this model
    #
    def all
      db.transaction(true) do
        ids = extract_model_ids(db)
        ids.map { |key| db[key] }
      end
    end

    # Store an instance in the DB
    #
    def save(object)
      db_id = derive_db_id(object.class.name, object.id)
      db.transaction do
        db[db_id] = object
      end
    end

    # Scoped by class, to auto-increment the IDs
    #
    def next_available_id
      last_id = all_ids.map do |key|
        key.sub("#{self.name}_", "").to_i
      end.max.to_i

      last_id + 1
    end

    private

    # Access to the PStore binary file
    #
    def db
      @db ||= PStore.new(DB_FILE)
    end

    # Scoped by class, so that different model classes
    # can have the same numerical IDs
    #
    def derive_db_id(model_name, obj_id)
      "#{model_name}_#{obj_id}"
    end

    # All the IDs for this model
    #
    def all_ids
      db.transaction(true) do |db|
        extract_model_ids(db)
      end
    end

    # Get all the PStore root keys (the DB IDs)
    # scoped for the current class
    #
    def extract_model_ids(store)
      store.roots.select do |key|
        key.start_with?(self.name)
      end
    end
  end
  extend ClassMethods


  def save
    ensure_presence_of_id
    self.class.save(self)
  end

  private

  def ensure_presence_of_id
    self.id ||= self.class.next_available_id
  end
end

There is a lot going on there, so let’s look at it bit by bit, starting with the class methods.

Of course, we begin with requiring the pstore library, which is used in Base::db to instantiate the PStore object. The db.pstore file, which will be created in the root directory of the application, is a binary file: its contents are the marshaled representations of the stored objects.

As you might have read in the documentation for PStore (linked above), all read and write operations must happen inside transactions, and thus all our data access will be wrapped in calls to the PStore#transaction method.

The public interface of Base is simple and only consists of four methods. We have Base::find to retireve an object by ID, and Base::all that returns all stored objects. Both methods are scoped by the class name, so that if we have multiple concrete subclasses of Base, let’s say Dog and Rabbit, we can invoke Dog.all() and obtain only dog objects. Simirarly, the IDs of the objects are only unique per class, and we can persist both a dog and a rabbit with the same ID, e.g. 42, and calling Rabbit.find(42) will correctly return the matching rabbit object, not the dog.
The IDs are namespaced with the private class method Base::derive_db_id.

Next, we have Base::save, that accepts an instance and stores it in the DB, and Base::next_available_id, that we use to auto-increment the IDs of new objects as we persist them.

Among the private class methods, Base::extract_model_ids is the counterpart of the already mentioned Base::derive_db_id. It iterates through the root-level keys in the store and selects the ones that match the current class name (again, to scope the IDs by class).

The only public instance method is Base#save, that assigns an auto-incremented value for the ID and then passes the object to the Base::save class method.

That’s it. With less than 90 lines of code, comments included, we have enough functionality to support simple CRUD operations on our models. Or, well, not entirely. We have not implemented a way to delete resources, but that’s something we deliberately left out when we defined the supported routes for the application. Implementing a method to delete records is left as an exercise. :-)

A Model concrete class

Will all that base logic available, adding models to the application is just a matter of subclassing Base. The class identity and instance variables of the objects will be serialized and stored.

Here is our Dog class.

1
2
3
4
5
6
7
8
9
10
require_relative './base.rb'

class Dog < Base
  attr_accessor :id, :name

  def initialize(id: nil, name: nil)
    @id = id
    @name = name
  end
end

Quite simple, isn’t it? We can check that everyting is working correctly in a REPL like pry or irb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ pry

> require "./app/models/dog"
=> true

> Dog.all
=> []

> dog = Dog.new(name: "Droolmachine")
=> #<Dog:0x007fa6d1428c98 @id=nil, @name="Droolmachine">

> dog.save
=> #<Dog:0x007fa6d1428c98 @id=1, @name="Droolmachine">

> other_dog = Dog.new(name: "Muncher")
=> #<Dog:0x007fa6d1332410 @id=nil, @name="Muncher">

> other_dog.save
=> #<Dog:0x007fa6d1332410 @id=2, @name="Muncher">

> Dog.all
=> [#<Dog:0x007fa6d2b5f880 @id=1, @name="Droolmachine">,
 #<Dog:0x007fa6d2b5f3f8 @id=2, @name="Muncher">]

> "I have #{Dog.all.count} dogs: #{Dog.all.map(&:name).join(' and ')}."
=> "I have 2 dogs: Droolmachine and Muncher."

It looks like everything is working fine. You’ll notice that a db.pstore file has been created in the root directory of the application (delete it to reset the data), and if you exit and restart the Pry session, your dog objects will still be available. Neat.

Using the models in the application

Creating and inspecting records in the REPL is not that useful, so the next step is to update our controller code to replace the OpenStruct objects with proper Dog objects.

It’s now a good idea to have a quick look at how we left the controller when we added the stubs for the view templates, and to compare it with the new implementation, below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require_relative './base_controller.rb'

class DogsController < BaseController
  # GET /dogs
  #
  def index
    @title = "So many dogs"
    @dogs = Dog.all
    build_response render_template
  end

  # GET /dogs/:id
  #
  def show
    @dog = Dog.find(params[:id])
    @title = "#{@dog.name}'s page"
    build_response render_template
  end

  # GET /dogs/new
  #
  def new
    @title = "More dogs please"
    build_response render_template
  end

  # POST /dogs
  #
  def create
    dog = Dog.new(name: params['dog']['name'])
    dog.save
    redirect_to "dogs/#{dog.id}"
  end
end

There are a few small changes.

In index and show we are no longer building the dog objects on the fly, and we are instead loading them from the PStore DB. As long the values of the instance variables have the same interface, the templates will not require any change. In show we are also using the retrieved object’s name to set the page title.
There is nothing new in the new action (no pun intended), but the create action becomes interesting. There we are instantiating and persisting a new dog, using a name value coming from the request parameters. Once the dog has been persisted we redirect to that dog’s show page. More on that in a second.

Let’s visit http://localhost:9292/dogs and, if everything is working correctly, we should see a list with the two dogs we created in the REPL. Click them, and you’ll see their details page. Try to go to http://localhost:9292/dogs/new, however, and you’ll be presented with an empty page. Let’s fix that next.

Creating new dogs

The create action in the controller is already fine, and all that’s missing are the inputs in the form. We don’t have access to the fancy form helpers provided by Rails, but for such a simple case we can get our hands dirty and manually write the required HTML.

Our new views/dogs/new.html.erb file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
  <%= render_partial "base/_head.html.erb" %>
  <body>
    <div>
      <h1><%= @title %></h1>
      <form action="/dogs" method="POST">
        <div>
          <label for="dog_name">The dog's name</label>
          <input type="text" name="dog[name]" id="dog_name">
        </div>
        <input type="submit" name="commit" value="create">
      </form>
    </div>
  </body>
</html>

Now reload the page and add some more dogs to the pack. :-)

With this last piece of functionality our simple Rack application is finally complete.

Other persistence options

Earlier in this post I briefly mentioned the possibility of changing the underlying storage without modifying the interface of the Model layer. It would be a good exercise, so let’s do it now by replacing PStore with something similar.

We are lucky, as the Standard Library provides the excellent YAML::Store class, a drop-in replacement for PStore that will save the objects as YAML instead of marshaled binary data.

Since the interface of YAML::Store is the same of PStore, the required changes are minimal:

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'yaml/store'

class Base
  DB_FILE = File.expand_path("../../../db.yml", __FILE__)

  module ClassMethods
    def db
      @db ||= YAML::Store.new(DB_FILE)
    end
    # ...
  end
  # ..
end

We’ve just replaced:

  • require 'pstore' with require 'yaml/store',
  • "../../../db.pstore" with "../../../db.yml" and
  • PStore.new(DB_FILE) with YAML::Store.new(DB_FILE)

Restart the Rack server, reload the dog index page, and you’ll see that all the dogs have disappeared because we are now looking for a different DB file. The form will still work and, if you recreate the dogs we had previously added, you’ll see that everhing is still working as before.

Something is a bit better, though. YAML files are just plain text, and we can now check the contents of the db.yml file in the root directory of the project:

1
2
3
4
5
6
7
---
Dog_1: !ruby/object:Dog
  id: 1
  name: Droolmachine
Dog_2: !ruby/object:Dog
  id: 2
  name: Muncher

Wrap up

We started this series of posts with an introduction to Rack and we concluded with a working Rack application built from scratch. Even more than that, we adopted a set of patterns that would make it easy to expand the application with new functionality, and this will hopefully help understand how frameworks like Ruby on Rails work.

For reference, the full sourcecode for the application is available on github.

That’s all Folks!