Ruby on Rails
Regis Pires Magalhã[email protected]
http://regispiresmag.googlepages.comÚltima atualização em 24/01/2007
Instalação do Ruby
1 – No Linux - http://rubyforge.org/frs/download.php/7858/ruby-1.8.4.tar.gz
tar xvzf ruby-<versão>.tgz./configuremakemake install
2 – No Windows - One-Click Installer – já vem com RubyGems:http://rubyforge.org/frs/download.php/11488/ruby184-20.exe
Instalação do RubyGems
• Desnecessário no Windows, pois já vem no One-Click Installer• RubyGems - gerenciador de pacotes do Ruby
1 – Descompactar: http://rubyforge.org/frs/download.php/11290/rubygems-0.9.0.zip
2 – Executar: ruby setup.rb
Instalação do Rails
• Via Internet:gem install rails --include-dependencies
• Local:– Ir para diretório onde estão os arquivos gem e executar: gem install activesupport actionpack actionmailer activerecord actionwebservice rails –-local
– Arquivos necessários:• rails-1.1.6.gem• actionmailer-1.2.5.gem• actionpack-1.12.5.gem• actionwebservice-1.1.6.gem• activerecord-1.14.4.gem• activesupport-1.3.1.gem
Instalação do PostgreSQL
• No Windows:– Descompactar:
• postgresql-8.1.4-1.zip– Executar:
• postgresql-8.1.msi
Instalação da biblioteca do PostgreSQL para o Ruby
• Via internet:gem install postgres-pr
• Local:gem install postgres-pr –-local
Criação do Banco de Dados
create database <projeto>_<ambiente>
create database livraria_development
Ambientes:• Development - desenvolvimento• Test - testes• Production - produção
Criação de tabelas
• usuarios– id serial (PK)– nome varchar(75)– email varchar(75)– senha varchar(10)– admin int4 – img bytea
• tipos– id serial (PK)– descricao varchar(50)
• categorias– id serial (PK)– descricao varchar(50)
• produtos– id serial (PK)– descricao varchar(100)– tipo_id int4– categoria_id int4
• telefones– id serial (PK)– usuario_id int4– numero varchar(10)
Criação do Projeto
rails <projeto>
>rails livrariacreatecreate app/controllerscreate app/helperscreate app/modelscreate app/views/layoutscreate config/environmentscreate componentscreate dbcreate doccreate libcreate lib/taskscreate log...
Configuração do acessoa banco de dados
• Abrir arquivo config/database.yml:development: adapter: postgresqldatabase: livraria_developmentusername: postgrespassword: 1234host: localhost
...
Testando o Servidor WEB
ruby script/server
ruby script/server –e [development|test|production]
• Development é o default.
Geração de um Scaffold
• Scaffold - tradução: andaime, palanque, armação , esqueleto• Scaffold é um meio de criar código para um determinado modelo
através de um determinado controlador.• É um meio de começarmos rapidamente a ver os resultados no
navegador Web e um método muito rápido de implementar o CRUD (Create, Retrieve, Update, Delete) na sua aplicação.
ruby script/generate scaffold <Model> <Controller>
ruby script/generate scaffold Usuario Admin::Usuario
• O controlador Admin::Usuario gerenciará as ações recebidas. O rails irá criar a seguinte estrutura de diretórios: <aplicação>/controllers/admin
Outras gerações comuns
• Modelo:ruby script/generate model <Model>
• Controlador:ruby script/generate controller <controller>
Diretórios da aplicação• controllers
– Classes de controle.– Uma classe controller trata uma requisição Web do usuário.– A URL da requisição Web é mapeada para uma classe de controle e
para um método dentro da classe.• views
– Armazena as templates de visualização para preenchimento com dados da aplicação, conversão para HTML e retorno para o browser.
• models– Armazena as classes que modelam e encapsulam dos dados
armazenados nos bancos de dados da aplicação.• helpers
– Armazena classes de auxílio usadas para ajudar as classes model, view e controller. Ajudam a manter os códigos de model, view e controller bastante enxutos.
Modelo<aplicação>/app/models/usuario.rb:
class Usuario < ActiveRecord::Baseend
• A classe Usuario já é capaz de gerenciar os dados da tabela no banco de dados.
• Não há necessidade de explicitar o mapeamento das colunas do banco com atributos da classe.
• Rails não proíbe nada: se for necessário existe como mapear uma coluna para outro atributo de nome diferente.
• Nome de tabela diferente da convenção:
class EncomendaCliente < ActiveRecord::Baseset_table_name "encomendas_clientes"
end
Modelo• Toda entidade é criada no diretório padrão:
/app/models/<controller>/<model>.rb• Toda entidade herda diretamente da classe ActiveRecord::Base.
• Não há necessidade de mapear manualmente cada coluna da tabela.
• Convenção: a classe tem o nome no singular (Usuario), a tabela tem o nome do plural (usuarios).
• Convenção: Toda tabela tem uma chave primária chamada id que é de tipo auto-incremento.
Active Record Interativo
ruby script/console• Toda entidade criada pode ser manipulada
pelo console.• Facilita testes antes de criar as actions.
Acrescentando validaçãoclass Usuario < ActiveRecord::Base validates_presence_of :nome validates_length_of :nome, :maximum => 75, :message => "Máximo de %d caracteres."end
Outros exemplos: validates_uniqueness_of :cpf validates_length_of :cpf, :is => 11 validates_length_of :user_name, :within => 6..20, :too_long => "deve ser menor", :too_short => "deve ser maior" validates_length_of :first_name, :maximum=>30
Controller
• Todo Controller fica no diretório:/app/controllers/<nome>_controller.rb
• Todo Controller herda a classe ApplicationController• Todo aplicativo Rails é criado com uma classe chamada ApplicationController, que herda de ActionController::Base, e é base de todos os outros controllers
• Todo método de um controller é chamado de Action• Uma classe Controller pode ter quantas Actions quanto
necessárias.class Admin::UsuarioController < ApllicationControllerdef index
render :text => “Hello World!”end
end
Acessando uma Action
• Roteamento Customizável (routes.rb)http://localhost:3000/:controller/:action/:id
• Exemplo:http://localhost:3000/blog/index
• blog = app/controller/blog_controller.rb• index = método index em BlogController
View
index.rhtml:<h3>Hello da View!</h3>
blog_controller.rb:
Mais convenções• Ao final de toda Action, Rails chamará uma view
com o mesmo nome da Action, no seguinte diretório:/app/views/<controller>/<action>.<ext>
• A extensão do arquivo pode ser:– .rhtml - Embedded Ruby (HTML+Ruby)– .rxml - XML Builder (Ruby puro)– .rjs - Javascript Generator (Ruby puro)
• Este fluxo pode ser interrompido com uma chamada explícita ao método render ou redirect_to.
Modificando a listagem
/app/views/admin/usuario/list.rhtml
• Podemos alterar o layout ou qualquer código como quisermos.
Layouts
• Layouts permitem o compartilhamento de conteúdo.• Todo novo controller automaticamente ganha um
layout no diretório:/app/views/layouts/<controller>.rhtml
• As views desse controller preenchem o espaço:<%= @content_for_layout %>
Layouts# test_controller.rbclass TestController < ApplicationController
layout "default"def indexend
end<!-- default.rhtml --><html><head>
<title>Teste</title></head><body>
<h1>Layout Padrao</h1><%= @content_for_layout %>
</body></html>
Scriptlets
<% @usuarios.each do |usuario| %>Faz alguma coisa com usuario<% end %>...<% if number == "7" %>Está correto!<% end %>
Flash• flash[:notice] está disponível somente
naquela requisição. Usado para comunicação entre ações: passagem de string contendo informação ou erro.
• Similar a variáveis de sessão, mas somente existe de uma página para outra. Uma vez que a requisição para a qual foram propagadas acaba, elas são automaticamente removidas do contexto de execução.def three flash[:notice] => "Hello" flash[:warning] => "Mewl" flash[:error] => "Erro!" renderend
Logger• Nível de log configurado em:
config/environment.rb:config.log_level = :debug
• Mensagens escritas em um arquivo no diretório log. O arquivo de log usado depende do ambiente (environment) usado pela aplicação. Ex.: log/development.loglogger.warn("I don't think that's a good idea" )logger.info("Dave's trying to do something bad" )logger.error("Now he's gone and broken it" )logger.fatal("I give up" )
class PersonController < ApplicationController model :person paginate :people, :order => 'last_name, first_name', :per_page => 20 # ...end
Paginação
• Para todas as ações do Controller:
Paginação
• Para uma única ação do Controller: def list @person_pages, @people = paginate :people, :order => 'last_name, first_name' end
Paginação
• Condicional@upcoming_event_pages, @upcoming_events = paginate :upcoming_event, :conditions => "event LIKE '%"params['upcoming_event']['search'].to_s"%'", :order_by => "date DESC", :per_page => 10
Paginação
• Genérica def list @person_pages = Paginator.new self, Person.count, 10,
params[:page] @people = Person.find :all, :order => 'last_name, first_name', :limit => @person_pages.items_per_page, :offset => @person_pages.current.offset end
Paginação• paginate(collection_id, options={}) – Retorna um paginator e
uma coleção de instâncias de modelo Active Record para a página atual.
• Opções: – :singular_name: the singular name to use, if it can‘t be inferred by singularizing the
collection name – :class_name: the class name to use, if it can‘t be inferred by camelizing the singular
name – :per_page: the maximum number of items to include in a single page. Defaults to 10 – :conditions: optional conditions passed to Model.find(:all, *params) and Model.count – :order: optional order parameter passed to Model.find(:all, *params) – :order_by: (deprecated, used :order) optional order parameter passed to
Model.find(:all, *params) – :joins: optional joins parameter passed to Model.find(:all, *params) and Model.count – :join: (deprecated, used :joins or :include) optional join parameter passed to
Model.find(:all, *params) and Model.count – :include: optional eager loading parameter passed to Model.find(:all, *params) and
Model.count – :select: :select parameter passed to Model.find(:all, *params) – :count: parameter passed as :select option to Model.count(*params)
Paginação• Em um array / collection:
def paginate_collection(collection, options = {}) default_options = {:per_page => 10, :page => 1} options = default_options.merge options pages = Paginator.new self, collection.size, options[:per_page], options[:page] first = pages.current.offset last = [first + options[:per_page], collection.size].min slice = collection[first...last] return [pages, slice] end
@pages, @users = paginate_collection User.find_custom_query, :page => @params[:page]
Convenção de Nomes
Fluxo do MVC
Parâmetros de Forms
Associações• Todo usuario pode ter vários telefones.• Telefone pertence a Usuario através da coluna usuario_id.• Convenção de Chave Estrangeira: <classe>_idclass Usuario < ActiveRecord::Base has_many :telefones # um usuário tem muitos telefones validates_presence_of :nome validates_length_of :nome, :maximum => 75, :message => "Máximo de %d caracteres."end
class Telefone < ActiveRecord::Base belongs_to :usuario # um telefone pertence a um usuárioend
Associações
• Um para Um
Associações
• Um para Muitos
Associações
• Muitos para Muitos
Mapeamento de Tipos
Rake
• Programa em Ruby para realizar um conjunto de tarefas (tasks).
• Cada tarefa tem um nome, uma lista de tarefas das quais ela depende e uma lista de ações a realizar.
• Para ver as tarefas existentes em um arquivo rake (Rakefile):rake --tasks
Testes Unitários• Toda nova entidade ganha um arquivo para teste unitário em:
/app/test/unit/<entidade>_test.rb• Devemos seguir os preceitos de Test-Driven Development:
“Se não vale a pena testar, para que estamos codificando?”
• Os testes acontecem em banco de dados separado do desenvolvimento<projeto>_test
• Cada teste roda de maneira isolada: os dados modificados em um teste não afetam outro teste
• Cada teste unitário tem um arquivo de “fixture”, carga de dados para testes:/app/test/fixture/<tabela>.yml
• Executando todos os testes unitáriosrake test:units
• Executando apenas um teste unitário:ruby test/unit/<entidade>_test.rb
Testes Unitáriosrequire File.dirname(__FILE__) + '/../test_helper'class PostTest < Test::Unit::TestCase fixtures :posts # Replace this with your real tests. def test_add_comment # adiciona um post @post = Post.create(:title => 'New Post', :body => 'New Post', :author => 'Anonymous') # adiciona um comment @comment = @post.comments.create(:body => 'New Comment', :author => 'Anonymous') assert_not_equal nil, @comment # recarrega a entidade @post.reload # checa se existe apenas um post assert_equal 3, Post.count # já existem 2 posts na fixture # checa se esse post tem apenas um comment assert_equal 1, @post.comments.count endend
Fixture YAML
• “YAML Ain’t a Markup Language”• Maneira de serializar objetos Ruby em
forma de texto• Formato humanamente legível• Mais leve e simples que XML
Fixture YAML# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.htmlronaldo: id: 1 name: Ronaldo login: ronaldo admin: true email: [email protected] data: 2004-07-01 00:00:00 hashed_password: <%= Digest::SHA1.hexdigest('abracadabra') %>marcus: id: 2 name: Marcus login: marcus admin: false email: [email protected] data: <%= 1.day.from_now.strftime("%Y-%m-%d") %> hashed_password: <%= Digest::SHA1.hexdigest('teste') %>
Fixture YAML
Fixture YAML
Testes Unitários
Testes Unitários
Testes Unitários
Testes Funcionais
• Todo novo controller ganha uma classe de teste em:/app/test/functional/<classe>_controller_test.rb
• Devemos testar cada action do controller• Métodos como get e post simulam navegação com um
browser• Executando todos os testes funcionais:
rake test:functionals• Executando apenas um testes funcional:
ruby test/functional/<classe>_controller_test.rb
Testes Funcionaisrequire File.dirname(__FILE__) + '/../test_helper'require 'blog_controller'
# Re-raise errors caught by the controller.class BlogController; def rescue_action(e) raise e end; end
class BlogControllerTest < Test::Unit::TestCase fixtures :posts
def setup @controller = BlogController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end
def test_create num_posts = Post.count # checa total de posts atual # simula submit do formulário de criar novo post post :create, :post => {:title => 'New Title', :body => 'New Post', :author => 'Anonymous'} # checa se depois da action fomos redirecionados assert_response :redirect # checa se realmente foi acrescentado um novo post assert_equal num_posts + 1, Post.count endend
Testes Funcionaisrequire File.dirname(__FILE__) + '/../test_helper'require 'users_controller'
# Re-raise errors caught by the controller.class UsersController; def rescue_action(e) raise e end; end
class UsersControllerTest < Test::Unit::TestCase fixtures :users def setup @controller = UsersController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end
def test_index get :index assert_redirected_to :controller => "login", :action => "login" end def test_index_with_login @request.session[:user] = users(:ronaldo).id get :index assert_response :success assert_tag :tag => "table", :children => { :count => User.count + 1, :only => { :tag => "tr" } } assert_tag :tag => "td", :content => users(:ronaldo).name endend
Testes Funcionais
Testes Funcionais
Testes de Integração
• Para gerar o teste:script/generate integration_test login
• Para executar o teste:rake test:integration
Testes de Integraçãorequire "#{File.dirname(__FILE__)}/../test_helper"class LoginTest < ActionController::IntegrationTest fixtures :users def test_login user = users(:ronaldo) get "/home" assert_redirected_to "/login/login" follow_redirect! assert_response :success post "/login/login" assert_response :success assert_equal "Invalid login and/or password.", flash[:notice] post "/login/login", :login => user.login, :password => user.password assert_redirected_to "/home" follow_redirect! assert_tag :tag => "div", :attributes => { :id => "edit_form" } endend
Teste de Performance
Mais Testes
• Testes Unitários devem testar todos os aspectos da entidade como associações, validações, callbacks, etc
• Testes Funcionais devem testar todas as actions de um mesmo controller, todos os fluxos, redirecionamentos, filtros, etc
• Testes Integrados servem para avaliar a navegação e fluxos entre actions de diferentes controllers. Funcionam de maneira semelhante a um teste funcional
Helpers
# Global helper para views.module ApplicationHelper # Formata um float com duas casa decimais def fmt_decimal(valor, decimais) sprintf("%0.#{decimais}f", valor) endend
O que NÃO fizemos• Não precisamos recompilar e reinstalar o aplicativo a cada
mudança.
• Não precisamos reiniciar o servidor a cada mudança.
• Não precisamos mapear cada uma das colunas das tabelas para as entidades.
• Não precisamos configurar dezenas de arquivos XML. Basicamente colocamos a senha do banco de dados, apenas.
• Não precisamos usar Javascript para fazer Ajax: a maior parte pode ser feita com Ruby puro.
• Não sentimos falta de taglibs: expressões Ruby, partials foram simples o suficiente.
• Não precisamos codificar código-cola, o framework possui “padrões espertos” afinal, todo aplicativo Web tem a mesma infraestrutura.
Referências• Agile Web Development with Rails. 2ª Edição.
– Thomas e David Heinemeier Hansson. • Entendendo Rails
– Fábio Akita (http://www.esnips.com/web/BalanceOnRails)• Creating a weblog in 15 minutes
– http://media.rubyonrails.org/video/rails_take2_with_sound.mov• Tutorial de Rails do TaQ
– http://www.eustaquiorangel.com/downloads/tutorialrails.pdf• Tutorial de Ruby on Rails do Ronaldo Ferraz
– http://kb.reflectivesurface.com/br/tutoriais/rubyOnRails• Rolling with Ruby on Rails by Curt Hibbs
– http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html• Tutorial de Ruby do TaQ
– http://www.eustaquiorangel.com/downloads/tutorialruby.pdf
Top Related