Forum OpenACS Development: Ruby on Rails Review Part I
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/]pagewheras in Rails a requested URL is decoded by this:
http://host/[module/]controller/action/valueThis 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
- 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>
- Create a table called
users
with all the required columns - 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<h2>Callbacks</h2># 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, ...
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
.
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.
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.
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.
cheers
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)).
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....