© 2008 Alca Societa' Cooperativa -
http://alca.le.it
-
info@alca.le.it
-
released under
CC 2.5 by-nc-sa
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!)
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.)
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à:
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
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
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.
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
$ 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.
$ 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.
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.
$ 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
>> 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
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
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:
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
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
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
$ 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) =========================================
class News < ActiveRecord::Base validates_presence_of :title, :author, :body belongs_to :author end
class Author < ActiveRecord::Base has_many :news end
$ 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" >>
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
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:
$ 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)
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
Rails Guides (http://guides.rubyonrails.org)