Rails workshop for Java people (September 2015)

Preview:

Citation preview

RUBY ON RAILS

Ruby vs Java

Ruby = Slower

Java = Verbose

Ruby == JavaWhat's the same?

Garbage collected

Strongly typed objects

public, private, and protected

Ruby != JavaWhat'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? endend

> andre = Person.new("André")> andre.say("Hello!")=> André: Hello!

Inheritance

class RudePerson < Person def shout(message="default message") say(message.upcase) endend

Mixins

module RudeBehaviour def shout(message="default message") say(message.upcase) endend

class Person include RudeBehaviour # ...end

Readability nazi'sUsually 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 endend

def repeat(message="default message", count=1) while(count > 0) say(message) count -= 1 endend

def repeat(message="default message", count=1) until(count == 0) say(message) count -= 1 endend

def repeat(message="default message", count=1) (0...count).each do say(message) endend

def repeat(message="default message", count=1) for index in (0...count) say(message) endend

def repeat(message="default message", count=1) count.times do say(message) endend

Meta-programmingChanging 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 endend

> 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 endend

Your turn

Animal kingdomBuild 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) endend

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? endend

class Animal attr_accessor :health

def initialize self.health = 100 end

def make_noise noise puts noise endend

module Beaked def peck(animal) animal.health -= 1 endend

class Egg ; end

module EggLayer def lay_egg Egg.new endend

class Chicken < Animal include Beaked, EggLayer def make_noise super("tock") endend

RailsA web framework

DRY & MVCConvention over configuration

[EXPLAIN MVC GRAPH]

GET http://localhost:3000/people

Routing# config/routes.rbMyApp::Application.routes.draw do get "/people", to: "people#index"end

The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController def index endend

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.rbclass PeopleController < ApplicationController def index @people = ["André", "Pieter", "Matthijs"] endend

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.slimul - @people.each do |person| li= person

What about a database?The default is SQLLite

The model# app/models/person.rbclass Person < ActiveRecord::Baseend

Migrations# db/migrations/00000000_create_people.rbclass CreatePeople < ActiveRecord::Migration def change create_table :people do |t| t.string :name end endend

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.rbclass PeopleController < ApplicationController def index @people = Person.all endend

The view (SLIM)# app/views/people/index.html.slimul - @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.rbclass Person < ActiveRecord::Base validates :name, presence: trueend

Validations> p = Person.new

> p.save=> false

> p.errors.messages=> {:name=>["can't be blank"]}

Let's add a formThis will introduce two new 'actions'

NEW & CREATEthe form, and the creation

Routing# config/routes.rbMyApp::Application.routes.draw do get "/people", to: "people#index" get "/people/new", to: "people#new" post "/people", to: "people#create"end

Routing# config/routes.rbMyApp::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.rbclass PeopleController < ApplicationController def index @people = Person.all end

def new @person = Person.new endend

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.rbclass 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) endend

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.rbMyApp::Application.routes.draw do resources :people, only: [:index, :new, :create, :destroy]end

The view (SLIM)# app/views/people/index.html.slimul - @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 routesPrefix Verb URI Pattern Controller#Action-------------------------------------------------------------people GET /people people#indexperson GET /people/:id people#shownew_person GET /people/new people#newedit_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.rbclass PeopleController < ApplicationController # def index ... # def new ... # def create ...

def destroy Person.find(params[:id]).destroy redirect_to action: :index endend

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# Gemfilegem '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: relationshas_many, belongs_to, has_one, has_many_through

The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: true has_many :skillsend

# app/models/skill.rbclass Skill < ActiveRecord::Base belongs_to :personend

Migrations# db/migrations/00000000_create_skills.rbclass CreateSkills < ActiveRecord::Migration def change create_table :skills do |t| t.string :name t.references :person end endend

But this binds the skill to a single person...

We need a link tableRails forces you to name it properly!

# app/models/proficiency.rbclass Proficiency < ActiveRecord::Base belongs_to :person belongs_to :skillend

Migrations# db/migrations/00000000_create_proficiencies.rbclass CreateProficiencies < ActiveRecord::Migration def change create_table :proficiencies do |t| t.references :person t.references :skill end endend

Migrations# db/migrations/00000000_remove_person_reference_from_skills.rbclass RemovePersonReferenceFromSkills < ActiveRecord::Migration def change remove_reference :skills, :person endend

The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: true

has_many :proficiencies has_many :skills, through: :proficienciesend

# app/models/skill.rbclass Skill < ActiveRecord::Base has_many :proficiencies has_many :people, through: :proficienciesend

# app/models/proficiency.rbclass Proficiency < ActiveRecord::Base belongs_to :person belongs_to :skillend

> 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 formsThough 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.rbclass PeopleController < ApplicationController # def index ... # def new ... # def create ...

private

def person_attributes params.require(:person).permit(:name, proficiencies_attributes: [:skill_id]) endend

The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: true

has_many :proficiencies has_many :skills, through: :proficiencies

accepts_nested_attributes_for :proficienciesend

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

TestingPick your poison: rspec, test-unit,

# test/models/person_test.rbrequire '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? endend

Run your tests

> rake test

# test/integration/people_get_test.rbrequire '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" endend

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'sMaking a JSON API for your models

Routing# config/routes.rbMyApp::Application.routes.draw do resources :people namespace :api do resources :people endend

The controller# app/controllers/api/people_controller.rbclass Api::PeopleController < ApplicationController def index render json: Person.all endend

The response[ { id: 1, name: "André" }, { id: 2, name: "Pieter" }, { id: 3, name: "Matthijs" } ]

Adapting the JSONMultiple ways to achieve the same thing.

The model# app/models/person.rbclass Person < ActiveRecord::Base def as_json options={} { name: name } endend

But this changes it everywhere

JSON is also a view

The controller# app/controllers/api/people_controller.rbclass Api::PeopleController < ApplicationController def index @people = Person.all endend

The view# app/views/api/people/index.json.jbuilderjson.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 & ASpecific 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.

Recommended