Forum OpenACS Development: Ruby on Rails Review Part I

Collapse
Posted by Nima Mazloumi on
<h2>Ruby on Rails Review Part I</h2>

This post gives a short introduction on Ruby on Rails. Further post hopefully will follow. I started reviewing it as part of my work on web application frameworks. For everyone interested I recommend the book "Agile Web Development with Rails", ISBN 0-9766940-0-X.

<h2>Routing Requests</h2>

In OpenACS the requested URL is decoded by this

http://host/node/.../package/[folder/]page
wheras in Rails a requested URL is decoded by this:
http://host/[module/]controller/action/value
This behavior can be changed by the developer at any time.
As you can see no packages are available but controllers can be aggregated to modules. I will explain a controller later on. It's just important to note that there is no package management and no site map.

<h2>Model-View-Controller</h2>
For a single page in OpenACS

  • the model is represented by the acs object used and the corresponding SQL calls in the xql file
  • the view by the adp file
  • the controller by the tcl file
Rails differ from that by this
  • the model is represented by the model class defined as an active record. I will explain that later. Almost no sql is required here. All validation and model life-cycle is encapsulated in the class.
  • the view (action view) is represented by an action view which corresponds to the action mentioned above and represents a template.
  • the controller (action controller) is a class that is responsible for processing the request. Thus given the above request its action (which is a public class method) is called and the corresponding response is either returned directly or forwarded to the action view template for rendering. Controllers can have helpers to reduce repeating. The helpers could correspond to procs defined in our tcl folder.

<h2>Object Relational Mapping</h2>

Rails makes heavy use of this concept through introduction of conventions, schema introspection, SQL abstraction, callbacks and observers and validators

The best way to explain this is maybe by giving you an example:

<h2>Model Class Definition</h2>

  1. Create a table called users with all the required columns
  2. Now define the model class
    class User < ActiveRecord::Base
    end

The class attributes correspond to the columns defined in the table. By convention Rails assumes that when you define a class User a table called users must exist. By inheriting from ActiveRecord::Base the class benefits from all the underlying features of which some I will explain next.

In oder to create an object of that class you do this:

new_user = User.new
new_user.first_name = "Tom"

Or you use a constructor and pass the values for the attributes:

new_user = User.create(
    :last_name => "Hanks"
    :first_name => "Tom"
    :email => "mailto:tom@hanks.star";
)
new_user.save

The method save stores your data into the database. Updates are done by this:

new_user.update_attribute(:last_name, "Cruise")
# other methods are: update_attributes, update

In order to delete a user you do this:

new_user.delete
# other methods are delete(:id), delete(:list), delete_all(:condition)
<h2>Finders</h2>

Several methods are provided to find objects. Here some examples:

#first lets count them
size = User.count

# find a single object admin = User.find(:all, :conditions => "first_names = 'Tom'")

# pagination all_user = User.find( :all, :conditions => "first_names = 'Tom'" :order => "last_name" :limit => :page_size :offset => page_num*page_size )

# dynamic finders that are generated automatically dynamic finders some_users User.find_by_last_name_and_age(:last_name, 22)

# other finders are: find_by_sql, joins, fist, last, ...

<h2>Callbacks</h2>
The whole life-cycle of a class model is managed by active records. Here some examples:
after/before_validation
after/before_save/create/update
before/after_destroy

You can use these callbacks to provide extra functionality like this:

class User < ActiveRecord::Base
    before_destroy :dont_destroy_admin
    after_save :log_history
    before_update :modify_timestamp
end

Thus before a user is removed from the database your method dont_destroy_admin is called and after any changes your method log_history and so on.

<h2>Validators</h2>

The ActiveRecord class provides several validators that you can use to ensure data consistency right in your model class. Of course you can extend this by writing your own validator:

class User < ActiveRecord::Base
    validates_presence_of :first_names, :last_name, :photo_url
    validates_numericality_of :age
    validates_uniqueness_of :user_name

    validates_format_of :photo_url,
                                   :with => %r{^http:.+\.(gif|jpg|png)$}i,
                                   :message => "must be a URL for a GIF, JPG, or PNG image"

    # some more: validates_acceptance_of, validates_associated, validates_confirmation_of, validates_each, validates_exclusion_of, validates_inclusion_of

    #your own validation
    protected
    def validate
        # the error message for a given class attribute
        errors.add(:age, "should be over eighteen") unless age.nil? || price >= 18

        # the error message for the whole class
        errors.add_to_base("You must be an adult to trade with investment fonds...")
    end
end

The above example showns how Rails automatically knows from introspection which attributes exists. The validators are self explaining I guess. One important thing to note. As you can see you can provide messages on attribute level like for the attribute photo_url or or the age. These messages are like in ad_form automatically displayed if the validation fails. As you can see there is an object called errors which provides methods for messages on attribute and on class level.

<h2>Observers</h2>
Some aspects are found everywhere in the code. Thus Rails provides a class called ActiveRecord::Observer. You can use the above mentioned callbacks to ensure some required behavior:

class UserObserver < ActiveRecord::Observer
    def after_save(user)
       user.logger.info("Created User #{user.first_names} #{user.last_name} with user_id #{user.id}")
    end
end

As you can see each child of ActiveRecord::Base also provides a logger object with several log levels. Also if you need an observer for several model classes you simple list their names instead of the above case where as convetion Rails assumes that for an observer called UserObserver a model class called User must exist.

<h2>Relationships</h2>

Rails has support for the relationhip one-to-one, one-to-many, many-to-many and acting as list and tree. While the first three are self explaining the last two are for model classes that define a list or tree like behavior towards their parent class. Here an example:

class Customer < User
    # has a 1:0 or 1:1 relationship
    has_one :account

    # 1:m, referenced by teh m-side
    belongs_to :group

    # 1:m, is referencing the m-side
    has_many :address

    # n:m
    has_and_belongs_to_many :category

    # act_as_list ...
    # act_as_tree ...

That's all you need to define relationships in Rails. Rails will look for the model classes Account, Group, Address and Category and their corresponding tables in database. Again these methods are provided since User is a child of ActiveRecord::Base.

<h2>Summary</h2>

This short introduction is what it is short. Please forgive if I have not mentioned other important aspects. If I can I will write other posts later.

Still there are some lessons to learn i think:

  • MVC rocks
  • object orientation
    • is more maintainable
    • improves reuse
    • is more readable
    • encapsulates model and behaviour, life-cycle and validation
  • conventions
    • enforce a standardized and common development culture
    • allows integration of generative approaches
  • object relational mapping (ORM)
    • gives support for several databases
    • abstract from the verbosity of SQL
  • aspect orientation through introduction of callbacks and observers
    • reduces code by out-sourcing model unspecific code like loggin
    • allows to extend a framework transparently

I strongly believe that OpenACS can and started benefitting from some of these principles. With callbacks and XoTCL we have taken the right step.

What is missing is maybe the introduction of an ORM layer to give support to other databases as well. And here is where I see a difference between OpenACS and Rails: packages! The Rails community is mainly focusing on the toolkit and not on packages. New techniques maybe introduced in some customer project are integrated in the toolkit but not the application itself. The OpenACS community has decided for another direction by providing the package manager we decided to not only provide a toolkit but also working applications for the two supported databases. This results to several things:

  • high maintance effort
  • many outdated packages
  • wider focus and thus slower productivity
  • limited developer base

New developers should be able to benefit from the OpenACS toolkit and write their own stuff for any database regardless of existing packages. An in that sense Rails is ahead. But OpenACS could provide both great features as a toolkit and even nice packages to use.

Focusing on the toolkit in future will make the toolkit also more attractive again and therefore attract new developers. Thus I believe from the toolkit perspective that we have to go to the next level:

  • more and clearer conventions
  • object orientation
  • object relational mapping
  • aspect orientation
  • code generation
  • XHTML
  • AJAX

Some aspects where Rails is ahead:

  • API/Class Documentation
  • Testing and Debugging (performance, profiling, benchmarking)
  • Caching
  • Objects and ORM
  • XHTML
  • AJAX

Some things OpenACS has.

  • i18n
  • Sitemap
  • Better monitoring with xotcl-request-monitor
  • content repository
  • permissions
  • workflow
  • portals

Just if you wonder: I personally see workflow and portals as core functionality for a toolkit.

Collapse
Posted by Dave Bauer on
Nima,

I would have to disagree with the idea that workflow and portals should be part of the code, since I have not used them for any projects yet. I guess it depends on what you are building.

I think the idea of seperation of Core and Packages is the way to reduce the complexity of maintenance and release, but then we have ended up with inconsistenly maintained packages. So maybe we need some better solution.

I definitely agree that OpenACS needs object orientation for core organization. Its not quite true that OpenACS has no code/data model conventions, they are documented, but maybe not consistently applied so this is definitely an area for improvement. The priciple author of Ruby on Rails has said more than once that the code generation is nice, but not really the key feature of Rails at all. Its handy, its something that's not too hard, and it is useful to get something simple going quickly, I have used this kind of feature myself. I just think code organization, api consistency, and documentation are much more important. Design of APIs so that their use is clear and probably removal of the hundreds of ancient unused tcl utility procs is probably a good start so new developers can see more easily what APIs to use.

I think you over-estimate the value of code generation.

Collapse
Posted by Nima Mazloumi on
Dave,

sorry, if you got me wrong on that:

"portals and workflow" - I never said make workflow and portals core. I just think they are important.

"code generation". I don't overestimate it. I just believe that through code generating you can simply do two things: rapid prototyping and thus understanding better your client and attract new developers by givin them something that they can start playing with.

"conventions" - of course OpenACS has conventions. We need more consistency and improvement on that, as you say.

Collapse
Posted by Malte Sussdorff on
For the code generation. Using package builder allowed us to quickly get the basics of a package done and *ENFORCE* internationlization, use of Listbuilder, ad_form and other best practices. So I would not yield the results of code generation in "speed to achieve goal", but in clearness, maintainability and same look and feel across the site and packages.
Collapse
Posted by Nima Mazloumi on
I think we all agree that code generation is a good mean for developers to quickly get aquainted with the toolkit.
Collapse
Posted by Matthew Dodwell on
Where is the package builder package? Looking in CVS I can't seem to find it.

cheers

Collapse
Posted by Andrei Popov on
I have not posted or taken part in OpenACS anything for a few years now, and I have been playing around with RoR a bit in the last few weeks -- here's my feeling: for an "easy" start it is much more like ACS 3.x with a few lessons learned.  The lessons are basically the same (or similar) to the ones implemented in OACS 4.x, but having more to learn from and using a different language as the basis of the toolkit -- result is different (and overall very nice).

One thing I find it does suffer from (not planning to start a flame war, no) is a bit of MySQL-ism here and there, yet it is not too ugly.  Examples: it is a pain to call up a function if you use Pg, references are deduced from column naming instead of explicit foreign key definition, I don't think (but I am too new to this to be sure) one can easily express a join of several (more than two) tables (although with Postgres to the rescue one can do a view with a rule that'd act on all the joins you want to put in)).

Collapse
Posted by Andrei Popov on

One more point -- rake migrations are great and ActiveRecord is pretty cool, but both work at a "lowest" denominator level. What this means is that you can have this defined in Postgres:

create table things (
  id integer not null primary key,
  some_text varchar(100)
    constraint things_some_text_un unique
);

create table gadgets ( id integer not null primary key, thing_id integer not null constraint gadgets_fk references things(id), status_code varchar(1) constraint gadgets_status_code_ck check (status_code in ('c','o')) default 'c' );

But all (well, most of) these wonderful constraints will not be re-created once you run rake migrate on another machine....