Skip to content

Instantly share code, notes, and snippets.

@hakagura
Created November 23, 2011 17:43
Show Gist options
  • Save hakagura/1389337 to your computer and use it in GitHub Desktop.
Save hakagura/1389337 to your computer and use it in GitHub Desktop.
Explicações de conceitos do Rails e outras infos úteis.

Active Record

É um design pattern que o Rails implementa a partir da gem ActiveRecord.

Serve para conectar a camada Model da aplicação com tabelas do database, para assim criar um modelo de domínio persistível, onde a lógica (Model) e dados (BD) são apresentados em uma única solução.

Já persiste no BD:

obj.create

Salva mas não persiste (não da commit):

obj.save

Com build_costumer tenho que chamar save. Com create_customer não.

Referência aqui: http://ar.rubyonrails.org/

Database relationships

FK sempre fica no belongs_to.

belongs_to de um lado, e has_one de outro.

AREL (Active Relation)

Uma maneira mais robusta de se trabalhar com dados que o Rails oferece.

Pequeno exemplo da abstração que a API promove:

user = User.new #Model User
query = user.where("first_name = ? AND last_name = ?","Fulano","de Tal")
query.to_sql #retorna qual o SQL gerado pela abstração

OBS o ? mostrado acima é muito mais recomendado do que se fizesse interpolação de strings. Através do ?, o Rails faz tratamentos para evitar SQL Injection.

Scope/scoped

Retorna um objeto para poder concatenar no futuro outras queries.

Scope é mais vantajoso do que criar um método, pois caso fosse, necessitaria da menção da classe ao qual pertence.

Uso de scoped (Controller):

@posts = Post.scoped
@posts = @posts.search(params[:search]) if params[:search]
@posts = @posts.published

Uso de scope (Model):

scope :published, lambda {
  where("draft = ? AND published_at < ?", false, Time.current)
}

scope :search, lambda { |terms|
  where("title LIKE :t OR body LIKE :t", :t => "%#{terms}%")
}

Routes

O Rails usa REST, ou seja, usa o protocolo HTTP para mostrar diferentes estados. Dessa maneira, apps conversam entre si, sem a necessidade de webservices SOAP, nem nada disso.

Ao acessar uma URL, o framework acessa seu arquivo de routes, redireciona para o controller responsável por ela, o qual renderiza a action conforme configurado (XML, JSON, JS, HTML).

resources :customers

Gera uma série de rotas pré-definidas para o model Customer.

São 7 as actions/rotas, e se utilizam dos 4 verbos do protocolo HTTP (POST, GET, PUT, DELETE):

Via campo hidden o Rails passa um atributo _method no formulário, para que ele possa emular os verbos PUT e DELETE, que os browsers não suportam.

GET

index -> exibe todos os registros.
show  -> exibe UM registro (recebe parâmetro ID).
new   -> formulário para criação de um novo registro.
edit  -> formulário editação de UM registro (recebe parâmetro ID)

POST

create -> efetua a criação de um registro.

PUT

update -> efetua a alteração de um registro.

DELETE

destroy -> efetua a remoção de um/vários registros.

Juntamente com a criação das rotas, são criados aliases para elas, que facilitam na programação de controllers/views.

Ex:

(index)		GET			/customers				=> customers_path, customers_url 				
(new)     GET			/customers/new		=> new_customer_path, new_customer_url 
(create)	POST		/customers 				=> customers_path, customers_url
(edit)		GET 		/customers/1/edit	=> edit_customer_path(@customer), edit_customer_url(@customer)
(show)		GET 		/customers/1			=> customer_path(@customer), customer_url(@customer)
(update)	PUT 		/customers/1			=> customer_path(@customer), customer_url(@customer)
(destroy)	DELETE  /customers/1			=> customer_path(@customer), customer_url(@customer)

Aliases terminados em path apresentam caminho relativo, enquanto que terminados em url são absolutos.

No caso de existir aninhamento ou um namespace nas rotas, é essencial especificar isso no código:

#neste caso, comment depende de post na criação das rotas. ex: /post/3/comments
form_for [post, comment] do
...
end

Nested Routes

resources :posts do
	member do 
		post :confirm
		get :notify		
	end

	get :search, :on => :collection
end

Named Routes

match "/prices" => "pages#prices", :as => :prices

Parametro opcional:

match "/about(.:format)" => "pages#about", :as => :about

Constraints

match "/mobile" => "pages#mobile", :constraints => {:user_agent => /iPhone/}

match "/:year(/:month)" => "posts#archive", :constraints => {:year => /\d{4}/}

constraints :host => /localhost/ do
	match "/secret" => "info#about"
end

Helpers

Quando a minha view fica muito atrolhada (mesmo com _partials), eu extraio uma funcionalidade dela e transformo em helper, para re-utilização.

Ver /app/helper. Helpers visíveis ao aplicativo ficam em application_helpers.rb, e assim por diante.

Ex:

def my_helper(resource)
	render :partial => "shared/partial", :locals => { :resource => resource }
end

Conforme pode ser visto acima, não chamo partials com o underscore (_) da sua nomenclatura.

Testes

Unit => são testes de Model.

Functional => são testes de Controller.

Integration => são testes com múltiplos controllers, isto é, servem para simular um usuário acessando o sistema. São testes de workflow.

Convenções e afins

  • Models são no singular.
  • Tables são no plural (tenho vários itens).
  • Controllers são no plural (via de regra, exceção Session por exemplo)
  • Cada ambiente tem a sua configuração (ex: cache, DBMS, etc)
  • Qualquer alteração na pasta config, tenho que reiniciar o server

Boas práticas

Pensar bastante antes de criar uma nova action em um controller (famosos "fat controllers" - cheios de actions e actions grandes) ou mesmo sair criando rotas.

Em alguns casos, faz muito mais sentido criar um novo controller. Por ex: uma rotina de manipulação de arquivos. Onde vamos poder editar, criar um novo arquivo, deletar, etc. Em outros casos, é melhor pensar e ver se não podemos aproveitar alguma das actions que o Rails já define.

Exemplo:

Para fazer uma busca nos meus posts, posso simplesmente aproveitar a action "index", criando um método scope (search):

def index
	@posts = Post.scoped
	@posts = @posts.search(params[:search]) if params[:search]
end

I18n e L10n

  • i18n (internationalization) -> preparar sistema para outros idiomas/regiões.
  • L10N (globalization) -> implementar as diferenças.

Traduções default já estão em um .yml no fonte do Rails. Ex (no Console):

I18n.translate "error.messages.blank"
=> "can't be blank"

Para traduzir para pt-BR:

  1. Criar pt-BR.yml em config/locales/ (pode pegar lá no Git do Rails tradução pronta e colar aqui)

  2. config/application.rb => alterar linha config.i18n.default_locale = "pt-BR"

  3. Reiniciar o server

-> Posso definir tanto pelo application.rb o idioma corrente, como também posso criar um before_filter. Ex:

Dentro de ApplicationController:

before_filter :change_locale

...

protected
	def change_locale
		#session[:locale] = :en #poderia deixar na SESSION!
		I18n.locale = (params[:locale] || :en)
	end

Paginação

Will_paginate (gem para paginação)

  1. Modificar gem file adicionando a gem.

  2. No controller desejado, acrescentar:

     def index
     	@posts = Post.all.paginate(:page => params[:page], :per_page => 4)
     end
    
  3. Na view dessa action:

     <%= will_paginate @posts %>
    

Pronto :)

Delegation no Rails (tip)

Ao invés de criar um método que chama um método de outra classe. Ex:

class Post < ActiveRecord::Base

	...

	def full_name
		self.author.full_name
	end
end

Posso usar o método delegate do Rails:

class Post < ActiveRecord::Base

	...

	#gera author_full_name
	delegate :full_name, :to => :author, :prefix => true

end

AJAX

  1. No caso de ser um formulário, basta acrescentar :remote => true

  2. No controller, devemos deixar responder ao format.js - ex:

     respond_to do |format| 
     	format.html
     	format.js { render "create.js.erb" }
     end
    
  3. Criar a respectiva view, que tem o <.js> <.erb> Essa view, por ser um template ERB, aceita código Ruby/Rails e JS. Ela que trás JS que será executado no retorno do AJAX. Pode ser usado um append(), html(), val()... O que for.

OU

OBS: Neste caso a rota dashboard_path já existia. will paginate para a paginação.

CONTROLLER

dashboard_controller.rb

def index
  @notes = Note.all.paginate(:page => params[:page], :per_page => 5)
end

VIEWS

index.html.erb

<h2>Últimas atividades</h2>

<div id="latest_notes">
	<%= render :partial => "note", :collection => @notes %>
</div>

<%= image_tag "loader.gif", :id => "ajax_loading" %>

<%= link_to "Mostrar mais notas...", 
		dashboard_path(:page => @notes.next_page),
		:remote => true,
		:id => "more_notes",
		:class => "more"
%>

<% content_for :bottom do %>
	<script type="text/javascript" charset="utf-8">
		var toggleLoading = function() { $("#ajax_loading").toggle(); }

		$(document).ready(function(){
			$("#more_notes")
			.bind("ajaxStart", toggleLoading)
			.bind("ajaxComplete", toggleLoading);
		});
	</script>
<% end %>

_notes.html.erb

	<div class="notes" id="<%= dom_id(note) %>">
		<div class="note_avatar">
			<%= avatar_for note %>
		</div>

		<div class="note_info">
			<h4><%= link_to note.notable.name, note.notable %></h4>
			<ul>
				<li class="date">criado <%= time_ago_in_words note.created_at %> atrás</li>
				<li class="creator">por <%= note.creator.name %></li>
				<li class="content"><%= note.content %></li>
			</ul>
		</div>

		<div class="clear"></div>
	</div>

index.js.erb

$("#latest_notes").append("<%= escape_javascript(render :partial => "note", :collection => @notes) %>");

<% unless @notes.total_pages == @notes.current_page %>
	$("#more_notes").attr("href", "<%= dashboard_path(:page => @notes.next_page) %>");
<% else %>
	$("#more_notes").hide();
<% end %>

ActionMailer without ActiveRecord models (no database)

Mesmo procedimento do ActionMailer normal (gerar um mailer, configurá-lo...)

1 - Nosso Mailer. Criar método no nosso Mailer para que o e-mail seja enviado (método esse que é invocado do nosso model, no método save):

class UserMailer < ActionMailer::Base

  default :from => "[email protected]"

  def site_contact(contact)
    @contact = contact
        mail(:to => "[email protected]",
             :from => contact.email,
             :subject => "[Contato via site] #{contact.subject}")
  end

end

2 - Gerar um Model que não herde de ActiveRecord, e incluir o módulo de validações nele. Criar attr_accessor para cada elemento, e mais alguns métodos:

class Contact
  include ActiveModel::Validations

  attr_accessor :name, :email, :subject, :message

  validates_presence_of :name, :email, :subject, :message

  def initialize(attributes={})
    attributes.each do |key, value|
      self.send("#{key}=", value)
    end
  end

  def to_key
  end

  def to_param
    (id = self.id) ? id.to_s : nil 
  end

  def save
    if self.valid?
      UserMailer.site_contact(self).deliver #UserMailer é o nome que dei neste exemplo para classe do ActionMailer. site_contact o meu método.
      return true
    end

    return false
  end

end

OBS: no código acima foi necessário definir o método to_param, pois ao instanciar o formulário, ficava aparecendo o endereço do objeto contact.

3 - Criar um controller para gerenciar as actions new e create:

# encoding=UTF-8
class ContactsController < ApplicationController

  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new(params[:contact])
    if @contact.save
      redirect_to contact_path, :notice => "Contato enviado com sucesso!"
    else
      flash[:alert] = "Todos os campos são obrigatórios."
      render 'new'
    end
  end

end

4 - Criar as rotas:

get '/contato' => "contacts#new", :as => "contact"
post '/contato' => "contacts#create", :as => "contact"

5 - Criar views para ActionMailer e para as actions do nosso controller:

#app/views/user_mailer/site_contact.html.erb

Enviado por: <%= @contact.name %> - <%= @contact.email %> <br/>

Mensagem: <br/>
<%= @contact.message %>

#app/views/contacts/new.html.erb

<h2>Entre em contato conosco</h2>

<%= render 'form' %>

#app/views/contacts/_form.html.erb

<%= form_for @contact, contact_path do |f| %>
  <p>
    <%= f.label "Nome" %>
  </p>
  <p>
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label "Email" %>
  </p>
  <p>
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label "Assunto" %>
  </p>
  <p>
    <%= f.text_field :subject %>
  </p>
  <p>
    <%= f.label "Mensagem" %>
  </p>
  <p>
    <%= f.text_area :message %>
  </p>
  <p>
    <%= f.submit "Submit" %>
  </p>
<% end %>

Dica: Caso seja necessário de fato enviar o e-mail no ambiente de desenvolvimento, basta editar o arquivo em config/environments/development.yml. Acrescentar/modificar as seguintes linhas:

# We want to display errors in development environment, because we are trying to send it
config.action_mailer.raise_delivery_errors = true

# Set true to actually send the email 
config.action_mailer.perform_deliveries = true

E, claro, deve ser configurada a conta no config/initializers/mail_settings.rb:

ActionMailer::Base.smtp_settings = {
  :address              => "smtp.gmail.com",
  :port                 => 587,
  :domain               => "dominio.com.br",
  :user_name            => "[email protected]",
  :password             => "secret",
  :authentication       => "plain",
  :enable_starttls_auto => true
}

Links úteis:

Intializers/config

Tudo posto dentro de config/initializers executa junto com o Rails.

Tudo que é modificado dentro de /config, necessita que o server seja reiniciado!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment