0% found this document useful (0 votes)
27 views92 pages

Ch04 Rails

This document provides an overview of the Rails web application framework, including its implementation of the MVC pattern using ActiveRecord as the model layer. It describes how Rails uses conventions to simplify configuration, and how ActiveRecord handles CRUD operations on database records through generated SQL. Resource-oriented routes in Rails are also covered.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
27 views92 pages

Ch04 Rails

This document provides an overview of the Rails web application framework, including its implementation of the MVC pattern using ActiveRecord as the model layer. It describes how Rails uses conventions to simplify configuration, and how ActiveRecord handles CRUD operations on database records through generated SQL. Resource-oriented routes in Rails are also covered.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 92

CS 169 Software Engineering

git commit -m 'deal with it' &&


git push --force origin master

1
Today’s Concept Outline
• Quick whirlwind tour of Rails
• ActiveRecord for persisting app data
• ActiveRecord models as resources, and
resource-oriented routes to manipulate them
• Rails tour revisited, putting concepts into
context
• Comparison of concepts & mechanisms in
Rails vs. Sinatra

2
Hello Rails :
from Zero to CRUD
(Engineering Software as a Service §4.1)
Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Rails as an MVC Framework
Model, View, Controller Relational Persistence: mysql or
tables Database sqlite3
Subclasses of
ActiveRecord::Base,
an object-relational models/*.rb
mapping layer
Logic: your code &
your app Rack appserver
views/*.html.haml controllers/*.rb
Rails rendering Rails routing
Presentation:
Subclasses of WEBrick,
Subclasses of Apache, etc.
ActionView
ApplicationController

Client: Firefox
A trip through a Rails app
1. Routes (in routes.rb) map incoming URL’s to controller
actions and extract any optional parameters
– Route’s “wildcard” parameters (eg :id), plus any stuff after “?” in URL,
are put into params[] hash accessible in controller actions
2. Controller actions set instance variables, visible to views
– Subdirs and filenames of views/ match controllers & action names
3. Controller action eventually renders a view

app/controllers/movies_controller.rb app/views/movies/show.html.haml

def show %li


id = params[:id] Rating:
@m=Movie.find(id) = @m.rating
config/routes.rb end

get '/movies/:id' => 'movies#show'


Rails Philosophy
• Convention over configuration
– If naming follows certain conventions,
no need for config files
MoviesController#show in movies_controller.rb
è views/movies/show.html.haml
• Don’t Repeat Yourself (DRY)
– mechanisms to
extract common functionality
• Both rely heavily on Ruby features:
– introspection and metaprogramming
– blocks (closures)
– modules (mix-ins)
Why must every interaction with a
Rails app eventually cause
something to be rendered?

☐Because of convention over configuration

☐ Because HTTP is a request-reply protocol

Because MVC implies that every action renders



its own View, and Rails is strongly MVC
☐ Actually, some interactions using AJAX
do contact the server but don’t render
anything
7
cut
8
Models, Databases, and
Active Record

Engineering Software as a Service §2.6/4.3


Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


9
• How should we store and retrieve
record-oriented structured data?
• What is the relationship between
data as stored and data as
manipulated in a programming
language?

10
11
In-Memory vs. In-Storage
objects
#<Movie:0x1295580>
m.name, m.rating, ... marshal/serialize
#<Movie:0x32ffe416> unmarshal/deserialize ?
m.name, m.rating, ...
• How to represent persisted object in storage
– Example: Movie with name & rating attributes
• Basic operations on object: CRUD (Create,
Read, Update, Delete)
• ActiveRecord: every model knows how to
CRUD itself, using common mechanisms

12
Rails Models Store Data in
Relational Databases (RDBMS)
• Each type of model gets its own database table
– All rows in table have identical structure
– one row in table == one instance of model’s class
– Each column stores value of an attribute of the model
– Each row has unique value for primary key (by
convention, in Rails this is an integer and is called id)
id rating title release_date
2 G Gone With the Wind 1939-12-15
11 PG Casablanca 1942-11-26
... ... ... ...
35 PG Star Wars 1977-05-25

• Schema: Collection of all tables and their structure


“Ted” Codd
CRUD in SQL
• Structured Query Language (SQL) is the
query language used by RDBMS’s
• Rails generates SQL statements at
runtime, based on your Ruby code
• 4 basic operations on a table row:
Create, Read, Update attributes, Delete
INSERT INTO users (username, email, birthdate)
VALUES ("fox", "[email protected]", "1968-05-12"),
("patterson", "[email protected]", "????")
SELECT *
FROM users
WHERE (birthdate BETWEEN "1987-01-01" AND “2000-01-01”)
UPDATE users
SET email = "[email protected]"
WHERE username="fox"
DELETE FROM users WHERE id=1
The Ruby side of a model
• Subclassing from ActiveRecord::Base
– “connects” a model to the database
– provides CRUD operations on the model
http://pastebin.com/ruu5y0D8

• Database table name derived from


model’s name: Movieèmovies
• Database table column names are getters &
setters for model attributes
• Observe: the getters and setters do not
simply modify instance variables!
Creating: new ≠ save
• Must call save or save! on an AR model
instance to actually save changes to DB
– '!' version throws exception if operation fails
– create just combines new and save
• Once created, object acquires a primary key
(id column in every AR model table)
– if x.id is nil or x.new_record? is true, x
has never been saved
– These behaviors inherited from ActiveRecord::
Base—not true of Ruby objects in general
Assume table fortune_cookies
has column fortune_text
Which of these instance methods of
FortuneCookie < ActiveRecord::Base
will NOT return a silly fortune (if any)?

☐ def silly_fortune_1
@fortune_text + 'in bed'
end
def silly_fortune_2
☐ self.fortune_text + 'in bed'
end
def silly_fortune_3
☐ fortune_text + 'in bed'
end
☐ They will all return a silly fortune

17
Alternative: DataMapper
• Data Mapper associates separate mapper with
each model
– Idea: keep mapping independent of particular data store
used => works with more types of databases
– Used by Google AppEngine
– Con: can’t exploit
RDBMS features to
simplify complex
queries & relationships
• We’ll revisit when
talking about
associations
18
cut
19
RESTful Resource Routes
in Rails

Engineering Software as a Service §2.7


(revisited)
Armando Fox
© 2013 Armando Fox & David Patterson, all rights reserved
20
21
Basics of config/routes.rb
get '/pastries/:flavor'=> 'pastries#eat',
:as => 'eat_dessert'
• Routes matching route to
PastriesController#eat
• Populates params[:flavor] from URL
• In a view:
link_to 'Eat', eat_dessert_path('cherry')
è <a href="/pastries/cherry">Eat</a>
• DRY idea: route logic runs both ways
• Quick reference:
http://guides.rubyonrails.org/routing.html 22
CRUD on a RESTful resource:
resources :movies
get '/movies' => 'movies#index', :as => 'movies' I
get '/movies/:id/new'=>'movies#new', :as=>'new_movie' C
post '/movies' => 'movies#create', :as => 'movie'
get '/movies/:id' => 'movies#show', :as => 'movie' R
get '/movies/:id/edit' => 'movies#edit', :as => 'edit_movie' U
put '/movies/:id'=>'movies#update', :as=>'movie'
delete '/movies/:id' => 'movies#destroy', :as=>'movie'
D
Route Action

GET /movies/3 Show info about movie whose ID=3

POST /movies Create new movie from attached form data

PUT /movies/5 Update movie ID 5 from attached form data

DELETE /movies/5 Delete movie whose ID=5

23
CRUD on a RESTful resource:
resources :movies
get '/movies' => 'movies#index', :as => 'movies'
get '/movies/:id/new'=>'movies#new', :as=>'new_movie'
post '/movies' => 'movies#create', :as => 'movie'
get '/movies/:id' => 'movies#show', :as => 'movie'
get '/movies/:id/edit' => 'movies#edit', :as => 'edit_movie'
put '/movies/:id'=>'movies#update', :as=>'movie'
delete '/movies/:id' => 'movies#destroy', :as=>'movie'

form_for movie_path(5)
<form action="/movies/5" method="post">
form_for movie_path(5), :method => :put
<form action="/movies/5" method="post">
<input type="hidden" name="_method" value="put"> ...
link_to 'Edit', edit_movie_path(5)
<a href="/movies/5/edit">Edit</a>
link_to 'List All', movies_path
<a href="/movies">List All</a> 24
Demo: rake routes

25
Which statement is NOT true regarding
Rails RESTful routes and the
resources to which they refer:

☐ A resource may be existing content or a request


to modify something.

☐ In an MVC app, every route must eventually


trigger a controller action.

☐ One common set of RESTful actions is the


CRUD actions on models.

☐ The route always contains one or more


"wildcard" parameters, such as :id, to identify
the particular resource instance in the operation
26
cut
27
Databases & Migrations
(Engineering Software as a Service §4.2)

Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Your customer data is golden!

• How do we avoid messing it up when


experimenting/developing new
features?
• How do we track and manage schema
changes ?
• …the answer to both is automation!

29
Multiple environments,
multiple databases
• Rails solution: development, production and
test environments each have own DB
– Different DB types appropriate for each!
• How to make changes to DB, since will have
to repeat changes on production DB?
• Rails solution: migration—script describing
changes, portable across DB types
Migration Advantages
• Can identify each migration, and know
which one(s) applied and when
– Many migrations can be created to be reversible
• Can manage with version control
• Automated == reliably repeatable

• Theme: don’t do it—automate it


– specify what to do, create tools to automate
Meet a Code Generator
rails generate migration CreateMovies
• Note, this just creates the http://pastebin.com/VYwbc5fq

migration. We haven’t applied it.


• Apply migration to development:
rake db:migrate
• Apply migration to production:
heroku rake db:migrate
• Applying migration also records in DB itself
which migrations have been applied
Rails Cookery #1
• Augmenting app functionality ==
adding models, views, controller actions
To add a new model to a Rails app:
– (or change/add attributes of an existing model)
1. Create a migration describing the changes:
rails generate migration (gives you boilerplate)
2. Apply the migration: rake db:migrate
3. If new model, create model file
app/models/model.rb
4. Update test DB schema: rake db:test:prepare
5. Eventually deploy: heroku rake db:migrate
Demo: run the app, apply a
migration, see routes

34
Based on what you’ve seen of Rails, what kind of
object is likely being yielded in the migration code:
def up
create_table 'movies' do |t|
t.datetime 'release_date' ...
end
end

☐An object representing a database

☐ An object representing an instance of a model

☐ An object representing a table

☐ Give me a break, it could be anything

35
cut
36
Models: Finding, Updating,
Deleting
(Engineering Software as a Service §4.3)
Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Read: finding things in DB
• class method where selects objects based on
attributes; returns an enumerable
Movie.where("rating='PG'")
Movie.where('release_date < :cutoff and
rating = :rating',
:rating => 'PG', :cutoff => 1.year.ago)
Movie.where("rating=#{rating}") # BAD IDEA!
Movie.find(3) #exception if not found
• Can be chained together efficiently
kiddie = Movie.where("rating='G'")

old_kids_films =
kiddie.where("release_date < ?",30.years.ago)
Update: 2 ways
• Let m=Movie.where(title: 'The Help')
• Modify attributes, then save object
m.release_date='2011-Aug-10'
m.save!
• Update attributes on existing object
m.update_attributes(
release_date: '2011-Aug-10')

• Transactional: either all attributes are


updated, or none are
Deleting is straightforward
• Note! destroy is an instance method
m = Movie.where(name: 'The Help')
m.destroy
• There’s also delete, which doesn’t trigger
lifecycle callbacks we’ll discuss later (so,
avoid it)
• Once an AR object is destroyed, you can
access but not modify in-memory object
m.title = 'Help' # FAILS
...btw, how is this implemented?
Summary: ActiveRecord intro
• Subclassing from ActiveRecord::Base
“connects” a model to database
– C (save/create), R (where, find), U
(update_attributes), D (destroy)
• Convention over configuration maps:
– model name to DB table name
– getters/setters to DB table columns
• Object in memory ≠ row in database!
– save must be used to persist
– destroy doesn’t destroy in-memory copy
Suppose we’ve done
movie = Movie.where(:title => 'Amelie’)
Then another user of the app changes the
movie’s rating, and updates the database. Just
after that instant, the value of movie:

☐ will be updated automatically because an ActiveRecord


model has “connected” your app to the database

will be updated automatically because of ActiveRecord’s


☐ use of metaprogramming

will not be updated automatically, but can be updated


☐ manually by re-executing
movie = Movie.where("title='Amelie'")

☐ may be undefined or implementation-dependent

42
cut
43
Controllers & Views
(Engineering Software as a Service §4.4)

Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Rails Cookery #2
• To add a new action to a Rails app
1.Create route in config/routes.rb if needed
2.Add the action (method) in the appropriate
app/controllers/*_controller.rb
3.Ensure there is something for the action to
render in app/views/model/action.html.haml

• We’ll do Show action & view (book walks


through Index action & view)
MVC responsibilities
• Model: methods to get/manipulate data
Movie.where(...), Movie.find(...)
• Controller: get data from Model, make available to
View Instance variables
def show set in Controller
@movie = Movie.find(params[:id]) available in View
end Absent other info, Rails will look for app/views/
movies/show.html.haml
• View: display data, allow user interaction
– Show details of a movie (description, rating)
• But… http://pastebin.com/kZCB3uNj

– What else can user do from this page?


– How does user get to this page?
How we got here: URI helpers

link_to movie_path(3)
def show
index. @movie =
html.
<a href="/movies/3">...</a> Movie.find(params[:id])
haml
end
GET /movies/:id
{:action=>"show", :controller=>"movies"}
params[:id]ç3
What else can we do?
• How about letting user return to movie list?
• RESTful URI helper to the rescue again:
• movies_path with no arguments links to Index
action
=link_to 'Back to List', movies_path

ESaaS, Fig. 4.7


Which statements are TRUE:
a) A route consists of both a URI and an HTTP method
b) A route URI must be generated by Rails URI helpers
c) A route URI may be generated by Rails URI helpers

☐ Only (a) is true

☐ Only (c) is true

☐ Only (a) and (b) are true

☐ Only (a) and (c) are true

49
cut
50
When things go wrong:
Debugging
(Engineering Software as a Service §4.5)
Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Preview: Joel Spolsky
• Joel On Software
blogger
• Co-Founder of Trello &
Fog Creek Software
• CEO, StackExchange
• How not to get flame-
broiled on
StackOverflow
• 2/24, right here…don’t
miss it
Debugging SaaS can be tricky
• Errors early in flow may manifest much later
URIèrouteècontrollerèmodelèviewèrender
• Error may be hard to localize/reproduce if
affects only some users, routes, etc.
What Dev? Prd?
“printf debugging” ✔
rails console ✔
Logging ✔ ✔
rails server --debugger ✔
RASP
• Debugging is a fact of life.
• Read the error message. Really read it.
• Ask a colleague an informed question.
• Search using StackOverflow, a search
engine, etc.
– Especially for errors involving specific versions
of gems, OS, etc.
• Post on StackOverflow, class forums, etc.
– Others are as busy as you. Help them help you
by providing minimal but complete information
Reading Ruby error messages
• The backtrace shows you the call stack
(where you came from) at the stop point

• A very common message:


undefined method 'foo' for nil:NilClass
• Often, it means an assignment silently failed
and you didn’t error check:
@m = Movie.find_by_id(id) # could be nil
@m.title # will fail: 'undefined method'
Instrumentation (a/k/a “Printing
the values of things”)
• In views:
= debug(@movie)
= @movie.inspect
• In the log, usually from controller method:
logger.debug(@movie.inspect)
• Don’t just use puts or printf! It has
nowhere to go when in production.
Search: Use the Internet to
answer questions
• Google it
– “How do I format a date in Ruby?”
– “How do I add Rails routes beyond CRUD?”
• Check the documentation
– api.rubyonrails.org, complete searchable Rails
docs (make sure correct Rails version!)
– ruby-doc.org, complete searchable Ruby docs
(including standard libraries)
• Check StackOverflow
1. DRY & RASP: Reuse
Others’ Work
• “With a little searching for gems/libraries, we could
work on new features rather than reinventing
wheel”
• “Learning to effectively use Google and
StackOverflow made us really productive”
• “For every 5 minutes I spent writing code, I spent
an hour searching the internet.”
• “Wish we had sought/asked for help sooner, vs.
banging head against the wall solo”
• “I didn't realize how much time was required to
create something from scratch without starter code
and student mentors.”
Other observations
• RASP
– for errors/bugs: give a narrative of what you’ve
already done so far (not a stack trace, and not a
bare message without filename or line#)
– for refactoring: what goal are you trying to
achieve by refactoring? what have you already
thought about doing?
• RTFB or WTFV (read the * book or watch
the * video)
cut
60
Forms
(Engineering Software as a Service §4.6)

Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Dealing with forms
• Creating a resource
usually takes 2 interactions
– new: Retrieve blank form
– create: Submit filled form
• How to generate/display?
• How to get values filled in
by user?
• What to “return” (render)?
Rails Cookery #3
• To create a new submittable form:
1.Identify the action that serves the form itself
2.Identify the action that receives submission
3.Create routes, actions, views for each
• Form elements’ name attributes will appear
as keys in params[]
• Helpers provided for many common
elements
Creating the Form
• Anatomy of a form in HTML http://pastebin.com/k8Y49EhE

– the action and method attributes (i.e., the route)


– only named form inputs will be submitted
• Generating the form in Rails
– often can use URI helper for action, since it’s
just the URI part of a route (still need method)
– form field helpers (see api.rubyonrails.org)
generate conveniently-named form inputs
http://pastebin.com/3dGWsSq8
Which of these would be valid for
generating the form that, when submitted,
would call the Create New Movie action?

☐ = form_tag movies_path do
... end
☐ %form{:action => movies_path,
:method => :post}
☐ %form{:action => '/movies',
:method => 'post'}
☐ All of the above

65
cut
66
Microquiz

goo.gl/NqkrBW
bit.ly/CS169-quiz4
cut
68
Redirection, the Flash and the
Session
(Engineering Software as a Service §4.7)
Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Receiving the form
• A neat trick: use debugger to inspect what’s
going on
– start with rails server --debugger
– insert debugger where you want to stop
– details & command summary: ESaaS §4.7
• To notice: params[:movie] is a hash,
because of the way we named form fields
– Conveniently, just what Movie.create! wants
What view should be rendered
for create action?
• Idiom: redirect user to a more useful page.
– e.g., list of movies, if create successful
– e.g., New Movie form, if unsuccessful
• Redirect triggers a whole new HTTP request
– How to inform user why they were redirected?
• Solution: flash[]—quacks like a hash that
persists until end of next request
– flash[:notice] conventionally for information
– flash[:warning] conventionally for “errors”
Flash & Session
• session[]: like a hash that persists forever
– reset_session nukes the whole thing
– session.delete(:some_key), like a hash
• By default, cookies store entire contents of
session & flash
– Alternative: store Rails sessions in DB table
– (Google “rails session use database table”)
– Another alternative: store sessions in a
“NoSQL” storage system, like memcached
cut
73
Ben Bitdiddle says: “You can put arbitrary
objects (not just “simple” ones like ints and
strings) into the session[].” What do you
think?
☐ True—knock yourself out!

☐ True—but a bad idea!

☐ False, because you can’t put arbitrary


objects into a hash
☐ False, because session[] isn’t really
a hash, it just quacks like one

74
cut
75
Finishing CRUD
(Engineering Software as a Service §4.8)

Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


Edit/Update pair is analogous
to New/Create pair
• What’s the same?
– 1st action retrieves form, 2nd action submits it
– “submit” uses redirect (to show action for movie)
rather than rendering its own view
• What’s different?
– Form should appear with existing values filled in:
retrieve existing Movie first http://pastebin.com/VV8ekFcn

– Form action uses PUT rather than POST


http://pastebin.com/0drjjxGa
Destroy is easy
• Remember, destroy is an instance method
– Find the movie first...then destroy it
– Send user back to Index

def destroy
@movie = Movie.find(params[:id])
@movie.destroy
flash[:notice] =
"Movie '#{@movie.title}' deleted."
redirect_to movies_path
end
If you set an instance variable in a
controller method, its value will be
retained for how long?

☐This request and all subsequent requests

☐ Only this request and the next request

☐ Only this request—once the view is


rendered, the variable is reset to nil
☐ It depends on whether the instance
variable was declared static

79
cut
80
Summary & Reflections:
SaaS Architecture,
Rails, From Sinatra to Rails
(Engineering Software as a Service
§2.9-2.10, 4.11)
Armando Fox

© 2013 Armando Fox & David Patterson, all rights reserved


81
Connecting Architectural Concepts to
Rails apps
Gemfile
Rakefile

app /models/, views/, controllers/


/helpers
/assets/stylesheets/application.css

config /routes.rb
/database.yml
db /development.sqlite3
/test.sqlite3
/migrate/
log /development.log, test.log
Pitfall: Fat controllers &
code in your views
• Really easy to fall into “fat controllers” trap
– Controller is first place touched in your code
– Temptation: start coding in controller method
• Fat views
– “All I need is this for-loop.”
– “....and this extra code to sort the list of movies
differently.”
– “...and this conditional, in case user is not
logged in.”
• No! That’s for model, controller, helpers
Code Clinic part 2
Where, for the love of God,
should you not put code?

☐ Controllers

☐ Controllers

☐ Controllers

☐ Controllers

85
SIMPLIFY YOUR CONTROLLERS

OR THE BUNNY GETS IT


Architecture is about
Alternatives
Pattern we’re using Alternatives
Client-Server Peer-to-Peer
Shared-nothing (cloud computing) Symmetric multiprocessor, shared
global address space
Model-View-Controller Page controller, Front controller,
Template view
Active Record Data Mapper
RESTful URIs (all state affecting Same URI does different things
request is explicit) depending on internal state

As you work on other SaaS apps beyond this course, you


should find yourself considering different architectural
choices and questioning the choices being made.
87
Summary:
From Sinatra to Rails
Create app with Gemfile, app.rb, Create app with rails new appname; specific
config.ru roles for various app subdirectories
No particular app architecture Strongly MVC, using relational-database-
defined backed ActiveRecord for models
Inline routes: Separate route descriptions in config/
post '/new_game' do...end routes.rb, supports CRUD RESTful resource
routes by default, can add others
Names (app, controller methods, Convention over configuration
view templates) arbitrary
No built-in database connectivity ActiveRecord relational database connector
and support for migrations
Same files, dependencies, settings, Three environments by default (development,
etc. for development & production production, testing); can define others

View templating is up to you MVC defines default rendering flow, supports


various templating engines
Start app with rackup Start app with rails server 88
From Sinatra to Rails cont’d.
• More moving parts between route & render
– Match route in config/routes.rb
– Call appropriate controller action
– Find view template using convention over config
– Also use routes.rb to generate routes in views
• Cool things Rails gives you over Sinatra
– rails console interactive REPL
– Routes that use PUT & DELETE
– Automatically reloads classes when you modify app
• Things that are similar
– session[], flash[], params[]
89
Frameworks, Apps, Design
patterns
• Many design patterns so far, more to come
• In 1995, it was the wild west: biggest Web
sites were minicomputers, not 3-tier/cloud
• Best practices (patterns) “extracted” from
experience and captured in frameworks
• But API’s transcended it: 1969 protocols +
1960s markup language + 1990 browser +
1992 Web server works in 2015

90
Which steps are ALWAYS required when adding a
new action 'foo' to the Movie model of a Rails app:
(a) Ensure there is a template to render in app/
views/movies/foo.html.haml (or .html.erb, etc)
(b) Ensure a route exists in config/routes.rb
(c) Implement helper method to generate
necessary route-helper URIs

☐ Only (a) and (b)

☐ Only (b)

☐ Only (b) and (c)

☐ Only (a) and (c)

91
cut
92

You might also like