Microsoft's Internet Explorer browser has no built-in vector graphics machinery required for "loss-free" gradient background themes.

Please upgrade to a better browser such as Firefox, Opera, Safari or others with built-in vector graphics machinery and much more. (Learn more or post questions or comments at the Slide Show (S9) project site. Thanks!)

RubyOnRails

ActiveRecord

Master "Tecnologie OpenSource"

ActiveRecord: il pattern

ActiveRecord è prima di tutto il nome di un design pattern identificato e battezzato da Martin Fowler nel libro Patterns of Enterprise Application Architecture

Questo design pattern consente di collegare alcuni dei costrutti base della OOP (le Classi e gli Oggetti) alle tabelle e ai dati contenuti in un database relazionale.

Le librerie/framework che implementano tale pattern si occuperanno di generare ed eseguire il codice SQL corrispondente alle operazioni da effettuare.

Tra i vantaggi, oltre alla migliore integrazione con il resto di un’applicazione sviluppata in OOP, vi è in genere l’indipendenza dal particolare DBMS (MySQL, PostgreSQL, SQLite, Oracle, SQLServer etc.)

http://en.wikipedia.org/wiki/Active_record_pattern

ActiveRecord: l’implementazione Ruby

Nel framework Ruby on Rails ActiveRecord è il nome dell’ORM che implementa (ovviamente in Ruby) il pattern di Martin Fowler.

Oltre a quanto detto riguardo al design pattern (che è stato implementato in molti altri framework e in molti altri linguaggi) l’implementazione Ruby aggiunge le seguenti particolarità:

Creare un modello con i generator

Possiamo far uso dei generator per generare i file necessari ad implementare e testare i nostri modelli:

$ ruby script/generate model News  
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/news.rb
      create  test/unit/news_test.rb
      create  test/fixtures/news.yml
      create  db/migrate
      create  db/migrate/20090416195202_create_news.rb

Convenzioni sui modelli

Anche i modelli seguono delle convenzioni in RoR:

NOTA: nel caso in cui si stiano utilizzando database già esistenti, che non rispettano quindi le convenzioni di RoR, sarà possibile specificare il nome della tabella in maniera esplicita nel modello:

class MyModel
set_table_name "my_table_name"
end

Inflector e String.tableize

A questo punto ci sarebbe da chiedersi… come facciamo a sapere come si chiamerà la tabella relativa al modello che stiamo creando?

$ ruby script/console 
Loading development environment (Rails 2.3.2)
>> puts "News".tableize
news
=> nil
>> puts "Person".tableize
people
=> nil
>> puts "MyPerson".tableize
my_people
=> nil

La classe Inflector è quella che si occupa di definire le regole di traduzione/pluralizzazione ed è possibile configurarne il comportamento modificando il file config/initializers/inflections.rb

NOTA: è sconsigliato abusarne… non cercate di localizzare in italiano i modelli e le regole di pluralizzazione se non volete impazzire.

Migration DSL

I file nella directory db/migrate descrivono i cambiamenti incrementali da eseguire sul database:

class CreateNews < ActiveRecord::Migration
  def self.up
    create_table :news do |t|
      t.string :title
      t.string :author
      t.text   :body

      t.timestamps
    end
  end

  def self.down
    drop_table :news
  end
end

Avviare il processo di migrazione del database

$ rake db:migrate
(in /home/rpl/Projects/ALCA/MasterOpenSource/Rails/demo)
==  CreateNews: migrating =====================================================
-- create_table(:news)
   -> 0.0981s
==  CreateNews: migrated (0.0985s) ============================================

In questo modo verranno eseguiti i metodi self.up di tutte le migration nell’ordine in cui sono state create.

Navigare il database con dbconsole

$ ruby script/dbconsole
SQLite version 3.5.9
Enter ".help" for instructions
sqlite> .schema
CREATE TABLE "news" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255), "author" varchar(255), "body" text, "created_at" datetime, "updated_at" datetime);
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
sqlite> select * from schema_migrations;
20090414195202
sqlite> 

NOTA: la console eseguita da script/dbconsole cambia a seconda della configurazione contenuta nel file config/database.yml.

Modificare una migration esistente

Modificando una migration esistente è necessario eseguire una migrazione inversa (che esegue i metodi self.down delle migration) e rieseguire la migrazione:

$ rake db:rollback
(in /home/rpl/Projects/ALCA/MasterOpenSource/Rails/demo_tmp)
==  CreateNews: reverting =====================================================
-- drop_table(:news)
   -> 0.0516s
==  CreateNews: reverted (0.0519s) ============================================

$ rake db:migrate
...

NOTA: E’ buona educazione modificare solo migration di cui non si sia ancora effettuato un commit/rilascio ufficiale.

Uso della console interattiva

$ ruby script/console 
Loading development environment (Rails 2.3.2)
>> News
=> News(id: integer, title: string, author: string, body: text, created_at: datetime, updated_at: datetime)
>> News.find :all
=> []
>> news1 = News.new
=> #<News id: nil, title: nil, author: nil, body: nil, created_at: nil, updated_at: nil>
>> news1.title = "prima news con rails"
=> "prima news con rails"
>> news1.author = "Luca Greco"
=> "Luca Greco"
>> news1.body = "blah blah blah blah"
=> "blah blah blah blah"
>> news1.save
=> true

Uso della console interattiva

>> news_ary = News.all
=> [#<News id: 1, title: "prima news con rails", author: "Luca Greco", 
   body: "blah blah blah blah", created_at: "2009-04-16 20:42:04", 
   updated_at: "2009-04-16 20:42:04">]
>> news_ary[0].author
=> "Luca Greco"
>> News.all.each do |news|
?> puts "#{news.title} (#{news.author}): #{news.body}"
>> end
prima news con rails (Luca Greco): blah blah blah blah
=> [#<News id: 1, title: "prima news con rails", author: "Luca Greco", 
   body: "blah blah blah blah", created_at: "2009-04-16 20:42:04", 
   updated_at: "2009-04-16 20:42:04">]

Per una panoramica delle opzioni di ActiveRecord::Base.find:

http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002208

Uso dei modelli nei controller

JUST USE IT

class SayController < ApplicationController

  def xml
    @news = News.find :all
    render :xml => @news
  end

  def json
    @news = News.find :all
    render :json => @news
  end

end

Model Validation DSL

class News < ActiveRecord::Base
  validates_presence_of :title, :author, :body
end
$ ruby script/console 
Loading development environment (Rails 2.3.2)
>> a_news = News.new
=> #<News id: nil, title: nil, author: nil, body: nil, created_at: nil, updated_at: nil>
>> a_news.save
=> false
>> a_news.errors
=> #<ActiveRecord::Errors:0xb6d990c0 @errors={"body"=>["can't be blank"], "author"=>["can't be blank"], "title"=>["can't be blank"]}, @base=#<News id: nil, title: nil, author: nil, body: nil, created_at: nil, updated_at: nil>>
>> a_news.errors.full_messages
=> ["Body can't be blank", "Author can't be blank", "Title can't be blank"]
>> 

Per avere un quadro delle validazioni di alto livello già disponibili ci si può riferire all’apidoc:

Model Validation DSL

Quando diventa necessario personalizzare il processo di validazione di un modello è possibile passare all’approccio più completo descritto al seguente link:

http://api.rubyonrails.org/classes/ActiveRecord/Validations.html

class Person < ActiveRecord::Base
  protected
    def validate
      errors.add_on_empty %w( first_name last_name )
      errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
    end

    def validate_on_create # is only run the first time a new object is saved
      unless valid_discount?(membership_discount)
        errors.add("membership_discount", "has expired")
      end
    end

    def validate_on_update
      errors.add_to_base("No changes have occurred") if unchanged_attributes?
    end
  end

Model Relationship DSL – Creazione di un nuovo modello

Creare un nuovo modello da mettere in relazione con il precedente:

$ ruby script/generate model Author
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/author.rb
      create  test/unit/author_test.rb
      create  test/fixtures/authors.yml
      exists  db/migrate
      create  db/migrate/20090414214924_create_authors.rb

Model Relationship DSL – Creazione della migration

class CreateAuthors < ActiveRecord::Migration
  def self.up
    create_table :authors do |t|
      t.string :name
      t.string :surname

      t.timestamps
    end
    change_table :news do |t|
      t.remove :author
      t.integer :author_id
    end
  end

  def self.down
    drop_table :authors
    change_table :news do |t|
      t.remove :author_id
      t.string :author
    end
  end
end

Model Relationship DSL – Esecuzione della migration

$ rake db:migrate
(in /home/rpl/Projects/ALCA/MasterOpenSource/Rails/demo)

==  CreateAuthors: migrating ==================================================
-- create_table(:authors)
   -> 0.0078s
-- change_table(:news)
   -> 0.1084s
==  CreateAuthors: migrated (0.1166s) =========================================

Model Relationship DSL – Aggiunta delle relazioni ai Modelli

class News < ActiveRecord::Base
  validates_presence_of :title, :author, :body

  belongs_to :author
end
class Author < ActiveRecord::Base
  has_many :news
end

Model Relationship DSL – uso nella console

$ ruby script/console 
Loading development environment (Rails 2.3.2)
>> auth1 = Author.new :name => "Luca", :surname => "Greco"
=> #<Author id: nil, name: "Luca", surname: "Greco", created_at: nil, updated_at: nil>
>> auth1.save
=> true
>> news1 = News.new :title => "test news", :author => auth1, :body => "news content"
=> #<News id: nil, title: "test news", body: "news content", created_at: nil, updated_at: nil, author_id: 1>
>> news1.save
=> true
>> auth1.news
=> [#<News id: 1, title: "test news", body: "news content", created_at: "2009-04-16 22:18:32", updated_at: "2009-04-16 22:18:32", author_id: 1>]
>> news1.author
=> #<Author id: 1, name: "Luca", surname: "Greco", created_at: "2009-04-16 22:17:58", updated_at: "2009-04-16 22:17:58">
>> news1.author.name
=> "Luca"
>> 

Testing dei modelli

require 'test_helper'

class NewsTest < ActiveSupport::TestCase
  test "should not save news without title, author and body" do
    news = News.new
    assert !news.save
  end

  test "news without title should not be valid" do
    assert !news(:news_without_title).valid?
  end
end
require 'test_helper'

class AuthorTest < ActiveSupport::TestCase
  test "should not save author without name and surname" do
    author = Author.new
    assert !author.save
  end
end

Testing dei modelli – Fixtures

news_without_title:
  title: nil
  author: 1
  body: "test content"

Le fixture non sono altro che dati (in formato YAML o CSV) pronti per l’uso nei test.

NOTA:

Testing dei modelli – eseguire i test unit

$ rake test:units
(in /home/rpl/Projects/ALCA/MasterOpenSource/Rails/demo_tmp)
/usr/bin/ruby1.8 -Ilib:test "/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/helpers/say_helper_test.rb" "test/unit/author_test.rb" "test/unit/news_test.rb" 
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
Started
F..
Finished in 0.355051 seconds.

  1) Failure:
test_should_not_save_author_without_name_and_surname(AuthorTest) [/test/unit/author_test.rb:6]:
<false> is not true.

3 tests, 3 assertions, 1 failures, 0 errors
rake aborted!
Command failed with status (1): [/usr/bin/ruby1.8 -Ilib:test "/usr/lib/ruby...]

(See full trace by running task with --trace)

Testing dei modelli – eseguire i test unit

Mentre si sviluppa un particolare testcase è utile poter eseguire un solo test file anzichè tutti i testunit:

$ cd test
$ ruby unit/news_test.rb 
Loaded suite unit/news_test
Started
..
Finished in 0.326485 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

Link

Rails Guides (http://guides.rubyonrails.org)

Copyright (C) 2008 - Alca Societa' Cooperativa

http://alca.le.it - info@alca.le.it

released under CreativeCommons 2.5 by-nc-sa

NOTA: le immagini dei software e dei device contenuti
nella presentazione sono proprieta' dei relativi detentori
del copyright e sono state riprodotte a scopo esclusivamente didattico.