RUBY ON RAILS
Ruby vs Java
Ruby = Slower
Java = Verbose
Ruby == Java
What's the same?
Garbage collected
Strongly typed objects
public, private, and protected
Ruby != Java
What's different?
No compilation
begin...end vs {}
require instead of import
Parentheses optional
in method calls
Everything is an object
No types, no casting, no static type
checking
Foo.new vs new Foo()
nil == null
under_score vs CamelCase
No method overloading
Mixins vs interfaces
Example time
Classes
class Person
attr_accessor :name
def initialize(name)
@name = name
end
def say(message="default message")
puts "#{name}: #{message}" unless message.blank?
end
end
> andre = Person.new("André")
> andre.say("Hello!")
=> André: Hello!
Inheritance
class RudePerson < Person
def shout(message="default message")
say(message.upcase)
end
end
Mixins
module RudeBehaviour
def shout(message="default message")
say(message.upcase)
end
end
class Person
include RudeBehaviour
# ...
end
Readability nazi's
Usually 5+ ways to do the same thing, so it looks nice.
def repeat(message="default message", count=1)
# say message count times.
end
def repeat(message="default message", count=1)
loop do
say(message)
count -= 1
break if count == 0
end
end
def repeat(message="default message", count=1)
while(count > 0)
say(message)
count -= 1
end
end
def repeat(message="default message", count=1)
until(count == 0)
say(message)
count -= 1
end
end
def repeat(message="default message", count=1)
(0...count).each do
say(message)
end
end
def repeat(message="default message", count=1)
for index in (0...count)
say(message)
end
end
def repeat(message="default message", count=1)
count.times do
say(message)
end
end
Meta-programming
Changing the code on the fly
> Person.new.respond_to?(:shout)
=> false
> Person.include(RudeBehaviour)
=> Person
> Person.new.respond_to?(:shout)
=> true
# Add the 'shout' method to each object that has a 'say' method.
> Object.subclasses.each{ |o| o.include(RudeBehaviour) if o.respond_to?(:say) }
We can manipulate code
class Person
["hello", "goodbye", "howdie"].each do |word|
define_method("say_#{word}") do
say("#{name}: #{word}")
end
end
end
> Person.new("André").say_hello
=> André: hello
Who even needs methods?
The power of method_missing
class Person
def method_missing(method_name, *arguments)
if method_name.starts_with?("say_")
# Say everything after 'say_'
say(method_name[4..-1])
else
super
end
end
end
Your turn
Animal kingdom
Build a Fish, Chicken, and Platypus
4 Each animal makes a noise (blub, tock, gnarl)
4 All animals have health
4 Certain animals have a beak (chicken, platypus, NOT
fish)
4 Animals with a beak can peck other animals
(health--)
4 Certain animals can lay eggs (fish, chicken, NOT
platypus)
module RudeBehaviour
def shout(message="default message")
say(message.upcase)
end
end
class Person
include RudeBehaviour
attr_accessor :name
def initialize(name)
self.name = name
end
def say(message="default message")
puts "#{name}: #{message}" unless message.blank?
end
end
class Animal
attr_accessor :health
def initialize
self.health = 100
end
def make_noise noise
puts noise
end
end
module Beaked
def peck(animal)
animal.health -= 1
end
end
class Egg ; end
module EggLayer
def lay_egg
Egg.new
end
end
class Chicken < Animal
include Beaked, EggLayer
def make_noise
super("tock")
end
end
Rails
A web framework
DRY & MVC
Convention over configuration
[EXPLAIN MVC GRAPH]
GET http://localhost:3000/people
Routing
# config/routes.rb
MyApp::Application.routes.draw do
get "/people", to: "people#index"
end
The Controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
def index
end
end
The view
# app/views/people/index.html
<p>Hello world!</p>
This is a bit plain...
Let's add some dynamic data
The Controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
def index
@people = ["André", "Pieter", "Matthijs"]
end
end
The view (ERB)
# app/views/people/index.html.erb
<ul>
<% @people.each do |person| %>
<li><%= person %></li>
<% end %>
</ul>
The view (SLIM)
# app/views/people/index.html.slim
ul
- @people.each do |person|
li= person
What about a database?
The default is SQLLite
The model
# app/models/person.rb
class Person < ActiveRecord::Base
end
Migrations
# db/migrations/00000000_create_people.rb
class CreatePeople < ActiveRecord::Migration
def change
create_table :people do |t|
t.string :name
end
end
end
rails g migration create_people name:string
Wait!!!?
How does it know people belongs to the person model?
Convention over
configuration
Tables are plural, models are singular
> rake db:create db:migrate
The Controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
def index
@people = Person.all
end
end
The view (SLIM)
# app/views/people/index.html.slim
ul
- @people.each do |person|
li= person.name
Convention over
configuration
All columns are mapped to methods
Okay, lets add some people.
> Person.new(name: "André").save
> Person.new(name: "Pieter").save
> Person.new(name: "Matthijs").save
> Person.count
=> 3
Validations
> Person.new.save
=> true
The model
# app/models/person.rb
class Person < ActiveRecord::Base
validates :name, presence: true
end
Validations
> p = Person.new
> p.save
=> false
> p.errors.messages
=> {:name=>["can't be blank"]}
Let's add a form
This will introduce two new 'actions'
NEW & CREATE
the form, and the creation
Routing
# config/routes.rb
MyApp::Application.routes.draw do
get "/people", to: "people#index"
get "/people/new", to: "people#new"
post "/people", to: "people#create"
end
Routing
# config/routes.rb
MyApp::Application.routes.draw do
resources :people, only: [:index, :new, :create]
end
Convention over
configuration
index, show, new, edit, create, update, destroy
The Controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
def index
@people = Person.all
end
def new
@person = Person.new
end
end
The view (ERB)
# app/views/people/new.html.erb
<%= form_for @person do |f| %>
<div>
<%= f.text_field :name %>
</div>
<div>
<%= f.submit "Save" %>
</div>
<% end %>
The view (SLIM)
# app/views/people/new.html.slim
= form_for @person do |f|
div= f.text_field :name
div= f.submit "Save"
From now on, we will continue in SLIM, but ERB is just as
good.
The Controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
# def index ...
# def new ...
def create
@person = Person.new(person_attributes)
if @person.save
redirect_to action: :index
else
render :new
end
end
private
def person_attributes
params.require(:person).permit(:name)
end
end
The view (SLIM)
# app/views/people/new.html.slim
- @person.errors.full_messages.each do |error|
div.red= error
= form_for @person do |f|
div= f.text_field :name
div= f.submit "Save"
Finally, destroying stuff.
Routing
# config/routes.rb
MyApp::Application.routes.draw do
resources :people, only: [:index, :new, :create, :destroy]
end
The view (SLIM)
# app/views/people/index.html.slim
ul
- @people.each do |person|
li= link_to(person.name, person_path(person), method: :delete)
NOTE: the method is the HTTP method, not the controller method.
Path helpers
> rake routes
Prefix Verb URI Pattern Controller#Action
-------------------------------------------------------------
people GET /people people#index
person GET /people/:id people#show
new_person GET /people/new people#new
edit_person GET /people/:id/edit people#edit
POST /people people#create
PATCH /people/:id people#update
DELETE /people/:id people#destroy
The Controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
# def index ...
# def new ...
# def create ...
def destroy
Person.find(params[:id]).destroy
redirect_to action: :index
end
end
Example time
Installing ruby (OSX)
> brew install rbenv ruby-build
Then add eval "$(rbenv init -)" to your .profile
> rbenv install 2.2.3
Linux: https://github.com/sstephenson/rbenv
Installing Rails
> gem install rails
Making your app
> rails new [my_app]
Structure
- app
- models
- views
- controllers
- config
- db
- Gemfile / Gemfile.lock
Dependencies
# Gemfile
gem 'slim-rails'
Add this line to your Gemfile to use slim, then install them:
> bundle install
Running your app
> rails s
Build a small app (45 mins)
Use the pdf for reference
Your app should:
4 be able to add items
4 be able to edit items
4 be able to destroy items
4 be able to show a single item
4 be able to show a list of items
Next up: relations
has_many, belongs_to, has_one, has_many_through
The model
# app/models/person.rb
class Person < ActiveRecord::Base
validates :name, presence: true
has_many :skills
end
# app/models/skill.rb
class Skill < ActiveRecord::Base
belongs_to :person
end
Migrations
# db/migrations/00000000_create_skills.rb
class CreateSkills < ActiveRecord::Migration
def change
create_table :skills do |t|
t.string :name
t.references :person
end
end
end
But this binds the skill to a
single person...
We need a link table
Rails forces you to name it properly!
# app/models/proficiency.rb
class Proficiency < ActiveRecord::Base
belongs_to :person
belongs_to :skill
end
Migrations
# db/migrations/00000000_create_proficiencies.rb
class CreateProficiencies < ActiveRecord::Migration
def change
create_table :proficiencies do |t|
t.references :person
t.references :skill
end
end
end
Migrations
# db/migrations/00000000_remove_person_reference_from_skills.rb
class RemovePersonReferenceFromSkills < ActiveRecord::Migration
def change
remove_reference :skills, :person
end
end
The model
# app/models/person.rb
class Person < ActiveRecord::Base
validates :name, presence: true
has_many :proficiencies
has_many :skills, through: :proficiencies
end
# app/models/skill.rb
class Skill < ActiveRecord::Base
has_many :proficiencies
has_many :people, through: :proficiencies
end
# app/models/proficiency.rb
class Proficiency < ActiveRecord::Base
belongs_to :person
belongs_to :skill
end
> andre = Person.new(name: "André")
> andre.skills << Skill.create(name: "Knitting")
> andre.save
Update your app (30 mins)
Use the pdf for reference
Your app should:
4 have a relationship through a link table
4 have the forms to create/update/destroy the
related items (in our example: Skills)
4 should NOT be able to build the relationship using a
form (yet).
Building nested forms
Though usually it can be prevented by making the link table a first-class citizen.
The view
# app/views/people/new.html.slim
= form_for @person do |f|
div= f.text_field :name
= f.fields_for :proficiencies, @person.proficiencies.build do |g|
div= g.collection_select :skill_id, Skill.all, :id, :name
div= f.submit "Save"
The controller
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
# def index ...
# def new ...
# def create ...
private
def person_attributes
params.require(:person).permit(:name, proficiencies_attributes: [:skill_id])
end
end
The model
# app/models/person.rb
class Person < ActiveRecord::Base
validates :name, presence: true
has_many :proficiencies
has_many :skills, through: :proficiencies
accepts_nested_attributes_for :proficiencies
end
Update your app (30 mins)
Use the pdf for reference
Your app should:
4 should be able to build the relationship using a form.
4 you can pick: nested or first-class
Testing
Pick your poison: rspec, test-unit,
# test/models/person_test.rb
require 'test_helper'
class PersonTest < ActiveSupport::TestCase
test "Person has a name, that is required" do
assert !Person.new.valid?
assert Person.new(name: "André").valid?
end
end
Run your tests
> rake test
# test/integration/people_get_test.rb
require 'test_helper'
class PeopleGetTest < ActionDispatch::IntegrationTest
test "that the index shows a list of people" do
# Build three people
names = ["André", "Matthijs", "Pieter"]
names.each{ |name| Person.create(name: name) }
get people_path
assert_response :success
assert_select "li", "André"
assert_select "li", "Matthijs"
assert_select "li", "Pieter"
end
end
Update your app (15 mins)
Use the pdf for reference
Your app should:
4 Test your validations, and relationships.
4 Test a few basic forms
Building API's
Making a JSON API for your models
Routing
# config/routes.rb
MyApp::Application.routes.draw do
resources :people
namespace :api do
resources :people
end
end
The controller
# app/controllers/api/people_controller.rb
class Api::PeopleController < ApplicationController
def index
render json: Person.all
end
end
The response
[ { id: 1, name: "André" },
{ id: 2, name: "Pieter" },
{ id: 3, name: "Matthijs" } ]
Adapting the JSON
Multiple ways to achieve the same thing.
The model
# app/models/person.rb
class Person < ActiveRecord::Base
def as_json options={}
{ name: name }
end
end
But this changes it
everywhere
JSON is also a view
The controller
# app/controllers/api/people_controller.rb
class Api::PeopleController < ApplicationController
def index
@people = Person.all
end
end
The view
# app/views/api/people/index.json.jbuilder
json.array! @people, :name
This means we can even
merge both controllers
just drop the jbuilder view in the original views
the view will be selected using the request format
Update your app (45 mins)
Use the pdf for reference
Your app should:
4 Have a view JSON api
4 Test the API
Q & A
Specific questions go here
EXTRA: Update your app
(60 mins)
Use the pdf for reference
Your app should:
4 Do something you want it too
4 We will help.

Rails workshop for Java people (September 2015)