Search site

Add to Google Subscribe in NewsGator Online

Featured Articles
ActiveRecord class association for the same object back
Previous article: Mixin for inheritance and a use case with ActiveRecord view
Next article: Real world usage of ActionMailer for account activation view

Rails_logo

The ActiveRecord component of a Ruby on Rails application is responsible for wrapping the relational layer into business objects. Why is this necessary ? two reasons:

  1. information is stored more efficiently when they are flattened down into database tables with defined relationships. This helps subsequent indexing and retrieving.
  2. humans have problems interpreting relational information from the database. It is better to represent them in hierarchical forms when they are used in the development process. Most modern development tools have the ability of providing auto-complete for these 'business objects', making their attributes available dynamically  and on demand. Programmers therefore do not have to perform mental translations between the database layer and the object layer, thus improve their productivity (theoretically !).

ActiveRecord uses naming conventions to derive implicit relationships between objects from the database tables, making additional mapping configurations redundant. This is a positive step above comparable technologies such as Castor or Hibernate. The naming convention is pretty straight forward. For example, the object Article by convention wraps a database table called articles, Attachment wraps attachments etc., and if the Article object contains many Attachment objects, the cross reference is found in a table called articles_attachments (and not attachment_articles, because articles is alphabetically higher in precedence over attachments). If you add an additional column to the table articles called articles.subject, you access it in the object world using Article.subject. That is foundation of the "convention over configuration" principle in the Ruby on Rails framework.

The problem I am specifically describing here is when you are straying away from the convention above. Let's say you have a new article which is a follow up from an existing article, or maybe you are developing an e-commerce website which has promotions of certain products in relation to other products. These scenarios require that the object be linked(or associated) back to itself. Can the Rails framework cope with such a scenario ? The answer short is, pretty well. The long answer is, with a bit of improvisation, it can be done.

Let's first describe the one to one relationship. Let's say we want to create a chain of linked articles, where the follow up article has a link to the previous one, so visitors to the site, be it right in the middle of the serialisation, or be it at either end, they will have continuity to the thread. We do this by adding a column to the table articles called articles.chained_article_id of the type int to represent the chained article. Next, we add this declaration to the Article object in the article.rb file:

class Article < ActiveRecord::Base
    :has_one ChainedArticle
end

and by magic, the follow up article can be accessed via the attribute Article.chained_article.

Next, we create a new model object called ChainedArticle in the file chained_article.rb by running the script 'script/generate scaffold chained_article' command. This will create the standard model object extending from ActiveRecord::Base as above, together with a controller ArticleController and the scaffolding templates for creating, updating, retrieving and destroying (CRUD) the model object too. We need to modify the file chained_article.rb to:

class ChainedArticle < Article
    :belongs_to :article
end

How does Rails know this new class ChainedArticle uses the articles table instead of a table called chained_articles ? the magic lies in the inheritance model. If you look at the Article object, it extends from ActiveRecord::Base, whereas ChainedArticle extends Article, indicating to Rails that the underlying table is one and the same.

That is essentially it. The rest will just automatically follow and the attributes for ChainedArticle will be identical to Article, with the exception of ChainedArticle.chained_article attribute will return nil instead of another article, unless you create another follow up article subsequently. This means that by traversing an article using the chained_article attribute, you can build up a whole chain of URLs to display against a particular article. If you want to traverse backwards to, then you will need to turn the ChainedArticle class into two classes, PreviousArticle and NextArticle, and build up the forward and backward links each time a new article is created.

You will need to manage the association between the article and its follow up yourself, as the code generated by the CRUD scaffold  above will not be able to cope with association attributes. The ArticleController object (article_controller.rb, created as part of the scaffold generator command above) will need to be modified to accommodate this :

class ArticleController < ApplicationController
    :belongs_to :article

    def create
        @article = Article.new(params[:article])
        @article.chained_article = ChainedArticle.find(params[:chained_article].to_i)
        if @article.save
            flash[:notice] = 'Article was successfully created.'
            redirect_to :action => 'show', :id => @article.id
        else
            render :action => 'new'
        end
end

The line in red above assumes you have made changes to the app/views/article/_form.rhtml file, which contains the partial template for the creation and update of an article, to incorporate a dropdown list of existing articles as:

<% Article.find(:all).each do |a| %>
    <select name='chained_article' id='chained_article'>
        <option value='<%= a.id %>'><%= a.name %></option>
    </select>
<% end %>

Also, note the call to ChainedArticle.find(), which has to be called instead of Article.find() to ensure the object returned from the query is of appropriate type. ActiveRecord also provides another rather intrusive mechanism to achieve this typecasting transparently. In order to do this, you need to ensure your articles table contains a column called 'type', or create a column reserved for the name of the object class, and override the ActiveRecord::Base.inheritance_column attribute to return this column name. If you use this implementation, then you can simply call Article.find() and ActiveRecord will return you an object which is typecast automatically to ChainedArticle.

Things are equally straight forward if you need to have more than one forward/backward links. This time a one-to-many relationship is required. Instead of the articles.chained_article_id column, you will need to create a new table to store the many-to-many relationship for both forward and backward links:

CREATE TABLE `articles_chained_articles` (
`chained_article_id` int(11) NOT NULL default '0',
`article_id` int(11) NOT NULL default '0'
);

The definitions for the Article and ChainedArticle class will need to change to:

class Article < ActiveRecord::Base
    :has_and_belongs_to_many :chained_articles
end

class ChainedArticle < Article
    :has_and_belongs_to_many :articles
end

Equally simple, in the ArticleController class, you need to modify app/views/article/_form.rhtml to present a list of all articles with checkboxes next to each:

<ul>
    <% Article.find(:all).each do |a| %>
        <li><input type='checkbox' id='chained_articles[<%= a.id %>]'><%= a.name %></li>
    <% end %>
</ul>

and then finally, change the ArticleController to process the information returned in the form:

class ArticleController < ApplicationController
    :belongs_to :article

    def create
        @article = Article.new(params[:article])
        params[:chained_articles].keys.each do |chained|
            @article.chained_articles << ChainedArticle.find(chained.to_i)
        end

        if @article.save
            flash[:notice] = 'Article was successfully created.'
            redirect_to :action => 'show', :id => @article.id
        else
            render :action => 'new'
        end
end

This approach is more flexible than the forward and backward linking method as it gives you the ability to create multiple follow up articles from the same article. I have simplified the code above to not include backward and forward linking for the sake of clarity. In a practice, each linkage should generate two entries in the articles_chained_articles table, one forward and the other backward.

back
Previous article: Mixin for inheritance and a use case with ActiveRecord view
Next article: Real world usage of ActionMailer for account activation view

discuss
 by by David at 18 Mar 2006 22:15:03
Copyrights © Transcraft Trading Limited 2006.All rights reserved. Bots Rss-rss