Login

Leveraging Ruby on Rails

This article is part of a series. The prior article "Leveraging Ruby on Rails and ClojureScript." detailed the project business and data requirements.
In this article, I'll detail the server implementation using Ruby on Rails, Version 4 (ROR) and SQLite (database server).

Note! Full source on my GitHub repository.

Customizing and Optimizing the Database Schema

ROR provides a database abstraction layer. One of the purposes of a database abstraction layer is provide a single interface for a diverse set of database engines. However, database engines tend to vary. Each database server and version may have unique features. Thus, ROR's database abstraction layer has limits as to what if can do. ROR's database abstraction layer provides a great foundation. The developer has the option to customize and extend that foundation. I'll demonstrate how to customize the schema instructions generated by ROR.

One of the features of ROR is boiler plate code generation. For the application database, ROR has an option to define database schemas and validations. ROR also has an option to store changes to the database schema. Either defining or changing a database schema is referred to as "Migration" in ROR terminology.

For this application's requirements, we'll use the "Migration" files generated by ROR. However, we'll customize the contents of the "Migration" files to meet or detailed requirements. In other words, we will edit the "Migration" files generated by ROR.

Unique Records

One of our detailed requirements is, we want the database to prevent duplicate records. For example, we don't want to have two authors with the name, "Mark Twain". We could set the application to enforce that rule. However, it's both better performance and better architecture, to set those rules in the database schema itself.

Note! We are using ROR's default database engine SQLite. You must verify your SQLite installation is built with foreign key operation support.

For reference as to why we setting the unique rules in the database schema, note the recommendation in the ROR Guide. Section 1.1 Why Use Validations:

" it may be a good idea to use some constraints at the database level. Additionally, database-level validations can safely handle some things (such as uniqueness in heavily-used tables) that can be difficult to implement otherwise."

Generating Models

ROR can generate both "models" and the database schema. Models are simply the Ruby data and database access objects.

Let's start with the database schema. We use a terminal command line. We navigate to root of our ROR application and execute the following command line:

$> rails generate model book title:string

Next, we need to edit the "Migration" file. This is a Ruby source file which executes the application's database engine Data Definition Language (DDL). In other word, it exectures the same commands you would use in SQLite's comand line. The same commands to create tables, indexes, etc.

So navigate to the application "db/migrate" directory. Locate a file with a name pattern of [DATETIME]_create_books.rb.

Open the file with an editor. ROR has added a method called "change". Let's replace the body of the change method with full DDL statement. We delimit start of each statement with "execute << SQL". We delimit the end of each statement with "SQL".

Here is our source code:

  def change
    execute <<-SQL
                DROP TABLE IF EXISTS books
            SQL

    execute <<-SQL
                CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                title varchar(50) NOT NULL,
                created_at datetime, updated_at datetime, author_id integer NOT NULL,
                FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE)
            SQL
  end

Note, I added 2 statements. The first statement deletes the "books" table if it already exists.

Next, we instruct ROR to execute our "Migrate" file. We can instruct ROR to execute the "migration" on a specific database. The following command executed the migration on the "test" database.

$> rake db:migrate RAILS_ENV=test

ROR will display a success or failure message. If want to verify a success, you can open the "test" database with the SQLite command line interpreter. In a terminal navigate to the "db" directory. Issue the following command:

$>sqlite3 test.sqlite3
sqlite>

Enter the ".table" command to view a list of tables. Enter the ".schema" command to view either the whole datbase schema or a specific object. Here is the command to view the "books" table schema:

sqlite>.schema books

Let's generate the model and "migration" (database schema) for Author. Our Author entiity does contain foreign keys, so we can define most of the schema in ROR's "migration" format. First issue the ROR command line:

$> rails generate model author first_name:string, last_name:string

Next, we locate the "migratation" file in the "db/migration" directory. We'll add the "NOT NULL" and length constraints. The following is our source code:

  def change
    create_table :authors do |t|
      t.string :first_name, :limit => 25, :null => false
      t.string :last_name, :limit => 25, :null => false
      t.timestamps
    end
  end

":limit=>25" sets of lengh constraint. ":null=> false" sets our "can not be null" constraint.

We need to add our "unique" constraint. For Author, that means the database can not store 2 records the same combination of first and last name. We implement the constraint by adding a index to the "authors" table. We start by instucting ROR to generate a "migrate" file.

$> rails destroy  migration AddPrimaryKeyIndexes

Again, locate the "migrate" file and edit the contents:

def change
    execute <<-SQL
           DROP INDEX IF EXISTS unique_author_name
      SQL
      execute <<-SQL
           CREATE UNIQUE INDEX unique_author_name ON authors (first_name, last_name)
      SQL
  end
end

Again, instruct ROR to execute the migration:

$> rake db:migrate RAILS_ENV=test

We now have our database integrity set for Author.

Customize our Model

Now, we need to now customize the ROR Author model. We define the relationships and validations. Locate the app/models directory, then the "author.rb" file. Lets edit the contents, here is our edited source code:

class Author < ActiveRecord::Base
    has_many :books, :dependent => :destroy
    validates :first_name, presence: true
    validates :last_name, presence: true
end

"has_many :books, :dependent => :destroy" states that each Author may have 0 to many related books. If the Author model is deleted, ROR should delete any books related to the Author.

"validates:first_name, presence:true" sets an application level validation. For example, before an insert statement is issued, the application with signal the first name must have a value.

Note! The above are nuanced, yet important optimizations. The validate prevents a trip to the database. If the database is on a remote server, then that is a very valuable optimization. Our destroy is being done in 2 different tiers. The database (see our books DDL above) will cascade the delete for the database records. The application will cascade the delete for the database access objects (Models).

Testing the Model and the Database Schema

ROR also generates test boiler plates. We can test our database schema, but we need to edit the bolierplate test code first.

Currently we have a "test" database schema, but we don't have any test data. We need to populate the test database with some records. Navigate to the application root then "test/fixtures" sub-directory. Locate the "authors.yml" file. The author.yml file defines our test data. Here is our edited source file:

one:
    id: 1
    first_name: Mark
    last_name: Twain

two:
    id: 2
    first_name: Brad
    last_name: Meltzer

Run the following command to verify we have test data in the test database.

$> rake db:test:load

Now, let's edit to test file for our Author model. Navigate to the "test/models" directory, locate the "author_test.rb" file. Here is our edited source code:

class AuthorTest < ActiveSupport::TestCase
    fixtures :authors

    test "is Mark Twain" do
        @author = authors(:one)
        assert_equal("Mark", @author.first_name)
        assert_equal("Twain", @author.last_name)
    end

    test "is Brad Meltzer" do
        @author = authors(:two)
        assert_equal("Brad", @author.first_name)
        assert_equal("Meltzer", @author.last_name)
    end

    test "rejects no name" do
        author = Author.new
        assert !author.save
    end

    test "accepts  name" do
        author = Author.new
        author.first_name = "Joe"
        author.last_name = "Smoe"
        assert author.save
    end

    test "rejects rejects duplicate name" do
        author = Author.new
        author.first_name = "Mark"
        author.last_name = "Twain"
        assert_raise ActiveRecord::RecordNotUnique do
            assert author.save
        end
    end
end

The last case tests our unique rule. Note that we are testing to see if the database through an exception when an attempt to enter a second "Mark Twain" was made. We run the tests with the following command line:

$>  rake test test/models/author_test.rb

Notifying the User

Currently, if a user tries to enter a duplicate Author, the application will attempt the insert, but the database will reject it. That is exactly how we want to divide the responsibility. We don't want the application to search through the data and determine uniqueness. We want the database to determine uniqueness.

Therefore, our ROR model is not going to validate the unique Author name. Instead our Author controller is going to catch the exception. Specifically the controller will catch a ActiveRecord::RecordNotUnique exception.

Once the controller catches the exception, we need a mechanism for displaying a user friendly error message. In ROR terminology, notifications are referred to as "Flash" messages. Flash messages are a set of key/values.

Once the router dispenses a response, the "View" portion of the application takes over. Thus, we'll need to customize our "Author" form view so that it can render when an attempt to add a duplicate Author was encountered.

Adding a custom "Flash" message.

We can register a new "Flash" key, by editing the application controller. The application controller is the parent to the rest of our application's controllers. That means the author controller will inherit our new message key. So will the books, categories and the rest of the controllers.

Navigate to the "app/controllers" directory, located the "application_controller.rb" file. Here is our edit source:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  add_flash_types :duplicate_record_exception
end

We added the last statement "add_flash_types :duplicate_record_exception". "add_flash_types" is a ROR directive. ":duplicate_record_exception" is our custom key (note the ":" prefix designates a key).

Next, we need to generate and customize a Author controller. Again, ROR generates the boilerplate code. We then customize the boilerplate. From the application root directory issue the following command:

$> rails g scaffold Author --migration=false --skip

That just means, our database schema is already set, don't try and generate it. "scaffold" generates the controller and the basic create, report, update and delete views. The report view is "index.html.erb", the create is "new.html.erb", etc.

Let's edit the Author controller. Navigate to the "app/controllers/" directory, locate the "authors_controller.eb" file.

We want to add an exception handler and then generate a custom Flash value (for our Flash key :duplicate_record_exception), from within the the exception handler.

At top of the controller source code, add the following statement:

rescue_from ActiveRecord::RecordNotUnique, :with => :unique_record_handler

"unique_record_handler" is the method name. We need to implement that method. Let's make that method private. So at the bottom of the source code, let's add the following:

private
    def unique_record_handler(exception)
         redirect_to new_author_url, :duplicate_record_exception => "Author already exists! Can not add a duplicate."
     end

"new_author_url" evaluates to a value set in the application's route facility. You can view the current routes by invoking the following command line:

$> rake routes

I found an entry for "new_author". That is the route for the "Add New Author" view. You have to suffix "new_author" with "_url" for the ROR to evaluate the expresssion. So the whole "unique_record_handler()" method catches the database exception. It then assgns a custom value to our Flash key, and issues a web browser redirect back to the "Add New Author Form".

Next, we need to customize the "Add New Author Form" so that it displays our custom "Flash" message. Navigate to the "app/views/authors" directory, locate the "_form.html.erb" file. Add the following to the source code:

<% if flash[:duplicate_record_exception] %>
<div id="error_explanation">
    <%= flash[:duplicate_record_exception] %>
</div>
<% end %>

OK, we repeat the above steps for the rest of our application's entities. That is authors, books, categories and reviews. And finally apply the same to our relationship book_category.

Customizing our Application UI/UX

As mentioned in the introductory article. "Leveraging Ruby on Rails and ClojureScript" [URL], I want use a light weight, et very powerful CSS framework "Pure". I've built a copy of the pure framework locally. I now need to add the Pure CSS files to our ROR application.

Navigate to the application's root directory and then "public" sub-directory. If a "stylesheets" sub-directory does not exist, create it. In the "public/stylesheets" directory add your Pure CSS files. Note the file names, we will the file names in the next step.

We now need to edit the "layout" view. The layout adds the html header. The html header is what we need to edit. Navigate to "app/views/layout". Locate the "application.html.erb" file. Edit the file. Add the following for a CSS file called "normalize.css" (it's an example name):

<%= stylesheet_link_tag "normalize" %>

When you render any of the applications pages (views), view the html source and you will see the following in the header section:

<link href="/stylesheets/normalize.css" media="screen" rel="stylesheet">

Repeat the above for each custom CSS file you added.

Providing a Custom Mobile Layout and View

So currently, our application is generating html, css and javascript dedicated to our full-size browser layout (the layout defined in "application.html.erb". However, our ClojureScript client does not need the boilerplate JavaScript code. The ClojureScript client also need some customized html markup.

Let's stay in the same place on the file system. That is stay in "app/views/layout". Let's make a copy of the "application.html.erb" file and name it "mob-application.html.erb". We just created a "layout" for our mobile client.

Our next step is provide a route (path for our mobile client to call) and a custom output format for our ClojureScript client. For brevity's sake, let's concentrate on our book route, controller and view.

Routes are defined in application file called "routes.rb". The routes file is located in "config" directory. You can specify which type of HTTP request a route services (E.G. GET, POST, PUT, DELETE). You can also specify which controller and controller method responds to the request. ROR provides several ways to express routes. Here is my implementation for our ClojureScript client's entry point:

get "books/mob/index", to: "books#mob_index", as: "books_mob_index"

That specifies a HTTP GET, with a url of http://YOUR_SITE/books/mob/index. "books#mob_index" specifies the books controller and mob_index method. "books_mob_index" defines an application variable we can use if we want to specify this route in our application code (E.G. books_mob_index_url).

Let's edit our book controller and add our new "mob_index" method. Here is the source code for our new controller method:

    def mob_index
        @books = Book.all.includes(:author, :reviews)
         render action: "index", layout: "mob-application"
    end

Note! We are instructing the "View" application layer to use a custom layout when rendering our books/index view ("index.html.erb").

Providing a JSON response.

Our ClojureScript client is going to need the base html document structure once. After the initial response, the ClojureScript client will request all subsequent information in JavaScript Notation Object Notation (JSON). Thus, we need to customize our "view" to make sure it can deliver the custom JSON messages.

Navigate to the "app/views/books" directory. You should notice that ROR generated a "json.jbuilder" file for each of the boilerplate views. The "json.jbuilder" files define, serialize and respond with JSON messages. Lets edit "index.json.jbuilder" file. Here is my edited source for "mob_index.json.jbuilder":

json.set! :books do
    json.array!(@books) do |book|
        json.extract! book,  :id, :title
        json.review_count book.reviews.count
        json.categories_count book.book_categories.count
        json.author_last_name book.author.last_name
        json.author_first_name book.author.first_name
        json.author_id book.author.id
    end
end

The statement "json.array!(@books)" directs ROR to add a "books" prefix to our json array. The respone will look like

"{books:[{id:1, title: "Book of Fate"}, {id: 2, title: "Huckleberry Finn"}, ....

We are responding with a JSON object. The JSON object has one property named "books". The value of "books" is a JSON array. Each element in the JSON array describes one our books.

Note! I am responding with both a count of categories related to the book and a count of reviews related to the book. Those values are not required.. They are custom values I set for my own use case.

Adding the JSON consumer

Finally, we need to add our ClojureScript compiler's custom JavaScript. We need to add "script" tag which references the custom JavaScript. Again, ROR provides several strategies for adding custom JavaScript. I decided to add the custom JavaScript file to the "assets" directory. The file name is "book_cljs.js".

We now must add a reference to our "book_cljs.js" file. We edit the "mob-application.hrml.erb" layout file. The layout file is located in "app/views/layouts". We add the following statement in the HTML head section:

<%= javascript_include_tag "books_cljs" %>

Adding Offline Support with HTML5 Cache Manifest

We need our mobile client to cache all of the application resources. When we don't have a route to our LAN (I.E. on the road, at a book store), we turn our mobile phone's network setting to "Airplane Mode". "Airplane Mode" sets the mobile device to "off line". More importantly. the mobile web browser will use the cached data rather and will not attempt to ready our ROR server.

Register the Cache Manifest MIME type

HTML5 Cache Manifests are delivered with a custom MIME type ("text/cache-manifest"). Thus, we need to register the Cache Manifest MIME type with the ROR server. We edit the file "mime_types.rb", located in the "config/intializers"directory. Add the following statement:

Mime::Type.register_alias "text/cache-manifest", :appcache

Create the cache.appcache file
We create a new file in the "public" directory called "cache.appcache". We want to cache our JavaScript, CSS, HTML skeleton, and any graphic files (like a fav icon). Here is my source for the "cache.appcache" file:

CACHE MANIFEST

#ver 223

CACHE:
books/mob/index
stylesheets/normalize.css
stylesheets/responsive.css
stylesheets/pure-min.css
stylesheets/app-custom.css
assets/books_cljs.js

NETWORK:
*

FALLBACK:

The "#ver 233" is a comment. However you can modify the line to force the browser to retrieve a new version of the cache manifest. The are many great resources that detail the HTML 5 cache manifest facility. Here is a link to the Mozilla organization's guide.

Add the manifest processing instruction
We need to make one final edit of our "mob-application.html.erb" file. We need to edit the &lthtml> tag and add a "manifest" attribute. Here is my edited &lthtml> tag:

<html manifest="/cache.appcache">

You can view the full source on my GitHub repository. The "mob_index.html.erb" file contains a few dependencies that are specific to our ClojureScript development process.

The article Leveraging ClojureScript details my mobile client implementation using ClojureScript.

About the Author:
Lorin M Klugman - I'm an experienced developer. My main interest is in new technology. Please use our contact box here if you are interested in hiring me. Please no recruiters :)