Getting Started

Create controller and views

  • list all the things rails know how to generate

      $ rails generate
    
  • Information about how to generate controller

      $ rails generate controller
    

    Stubs out a new controller and its views. Pass the controller name, either CamelCased or under_scored, and 一个coder加了一个column,另一个只要运行migration,就能达到同样状态 as arguments.

Hello World!

$rails generate controller demo index
  • 修改app/controllers/demo_controller.rb
class DemoController < ApplicationController
  
  layout false
  
  def index
  end
end
  • 修改app/views/demo/index.html.erb
<h1>Demo#index</h1>
<p>Hello World!</p>
  • config/下会生成文件routes.rb
Rails.application.routes.draw do
  get 'demo/index'
end

File Structure

.
├── Gemfile
├── Gemfile.lock
├── README.rdoc
├── Rakefile
├── app
│   ├── assets
│   │   ├── images
│   │   ├── javascripts
│   │   │   ├── application.js
│   │   │   └── demo.js
│   │   └── stylesheets
│   │       ├── application.css
│   │       └── demo.css
│   ├── controllers
│   │   ├── application_controller.rb
│   │   ├── concerns
│   │   └── demo_controller.rb
│   ├── helpers
│   │   ├── application_helper.rb
│   │   └── demo_helper.rb
│   ├── mailers
│   ├── models
│   │   └── concerns
│   └── views
│       ├── demo
│       │   └── index.html.erb
│       └── layouts
│           └── application.html.erb
├── bin
│   ├── bundle
│   ├── rails
│   ├── rake
│   ├── setup
│   └── spring
├── config
│   ├── application.rb
│   ├── boot.rb
│   ├── database.yml
│   ├── environment.rb
│   ├── environments
│   │   ├── development.rb
│   │   ├── production.rb
│   │   └── test.rb
│   ├── initializers
│   │   ├── assets.rb
│   │   ├── backtrace_silencers.rb
│   │   ├── cookies_serializer.rb
│   │   ├── filter_parameter_logging.rb
│   │   ├── inflections.rb
│   │   ├── mime_types.rb
│   │   ├── session_store.rb
│   │   └── wrap_parameters.rb
│   ├── locales
│   │   └── en.yml
│   ├── routes.rb
│   └── secrets.yml
├── config.ru
├── db
│   └── seeds.rb
├── lib
│   ├── assets
│   └── tasks
├── log
│   └── development.log
├── public
│   ├── 404.html
│   ├── 422.html
│   ├── 500.html
│   ├── favicon.ico
│   └── robots.txt
├── spring-1000
│   ├── 4b53b2ff8b7674a6a782c3be813c38da
│   └── 4b53b2ff8b7674a6a782c3be813c38da.pid
├── test
│   ├── controllers
│   │   └── demo_controller_test.rb
│   ├── fixtures
│   ├── helpers
│   ├── integration
│   ├── mailers
│   ├── models
│   └── test_helper.rb
├── tmp
│   ├── cache
│   │   └── assets
│   ├── pids
│   ├── sessions
│   └── sockets
└── vendor
    └── assets
        ├── javascripts
        └── stylesheets
  • models, views, controllers都在app下
  • 在models和controllers下的concerns文件夹用来存放它们模块间需要共享的代码
  • helpers里放views需要的helper method
  • mailer for sending emails
  • assets for images javascripts and stylesheets
  • bin command line executable
  • config包含database、application以及environment的config
  • config/initializer包含application launch需要的代码
  • config/locale internationalization
  • config.ru for Rack based applications to work with Rails.
  • db/ code to manage database migrations
  • Gemfile Gemfile.lock work with Bundler
  • lib/ storing libraries of code
  • log/ logging information
  • public/ 静态文件
  • test/ test code
  • vendor/ third party code outside gem

Basic route types

Simple route (matching exact string)

Rails.application.routes.draw do
	get 'demo/index'
end
# 完整版本是
	match 'demo/index',
		:to => 'demo#index;,
		:via => :get

因为simple route需要完全匹配,所以去掉index无法route

Default route

structure: :controller/:action/:id

GET /students/edit/52 对应 StudentsController, edit action,写法:

match ':controller(/:action(/:id))' 
:via => :get
# 括号表示可选项,默认match到index
match ':controller(/:action(/:id(.:format)))'
:via => :get
# 可以添加format例如json

Default route通常放在Root和Simple Route的下面

Root route

root :to => 'demo#index'
# 或者
root 'demo#index'

Controllers, Views and Dynamic Content

render template

routes呼叫controller,controller会根据定义的method做回应,如果未定义,则会寻找view里有没有match的html文件。如果有,就render给client。一般来说最好显式地定义controller,例如:

class DemoController < ApplicationController
  
  layout false
  
  def index
    #   render(:template => 'demo/hello')
    #   render('demo/hello')
      render('hello')
  end
  def hello
  end
end

在console可以看到过程

Started GET "/demo/index" for xxx.xx.xx.xxx at 2016-03-01 08:11:57 +0000
Processing by DemoController#index as HTML
  Rendered demo/hello.html.erb (0.1ms)
Completed 200 OK in 4ms (Views: 3.3ms | ActiveRecord: 0.0ms)

Redirect actions

案例:

An user request a password required area, controller check if he logged in. If he is, get the page, else, redirect to a login page.

class DemoController < ApplicationController
  
  def other_hello
    #   redirect_to(:controller => 'demo', :action => 'index')
      redirect_to(:action => 'index')
      # 在同一个controller下,controller => 'demo'可以省略
  end
  
  def example # 也可以跳转其它网站
      redirect_to('http://example.com')
  end
end

cosole

Started GET "/demo/other_hello" for xxx.xx.xx.xxx at 2016-03-01 08:24:49 +0000
Processing by DemoController#other_hello as HTML
Redirected to https://ror-nickyfoto.c9users.io/
Completed 302 Found in 1ms (ActiveRecord: 0.0ms)

Started GET "/" for xxx.xx.xx.xxx at 2016-03-01 08:24:49 +0000
Processing by DemoController#index as HTML
  Rendered demo/hello.html.erb (0.1ms)
Completed 200 OK in 5ms (Views: 4.3ms | ActiveRecord: 0.0ms)

haml

Official Site

Examples:

%h1 Demo#hello
%p Hello World!
	
- @array.each do |i|
  = i 
  %br/
  
= link_to 'Hello Page 1', 'hello'
<!--相当于-->
<a href="/demo/hello">Hello Page 1</a>
	
= link_to 'Hello with params', {:action => 'hello', :page => 5, :id => 20}
<!--相当于-->
<a href="/demo/hello/20?page=5">Hello with params</a>
  • % 表示tag
  • - 表示ruby code(not recommended)
  • @variable instance variable
  • =表示eval这个值
  • link_to 创建链接,第一个参数表示超链接字符,第二个表示要链接到的action。
  • 如果要传递参数到url,用hash给出。当用户访问这个url时,参数会以params传递给controller,params是特殊的hash,叫做HashWithInifferentAccess,取值时它中括号内可以是symbol也可以是string。
  • 注意rails里id在问号左边。

debugging

  • Read the Error Message!
  • 利用log来debug

    all controller method have logger:

    logger.debug(@movie.inspect)
    # also logger also has warning and error level 
    
  • A very common message:

      undefined method 'foo' for nil:NilClass
    

Often it means an assignment silently failed and you didn’t error check.

因为很多ruby method 有错误时并不报错,而是返回nil。例如:

@m = Movie.find(id) #could be nil
@m.title # will fail: 'undefined method'
  • print to views via haml
= debug(@movie) # 更好
= @movie.inspect
  • use rails console

Database and Migrations

  • 1 table = 1 model
  • table 名都是小写复数
  • attributes corresponds to columns

rake

A simple ruby helper program help perform tasks by running scripts. “rake” = “ruby make”, for example:

ask rake to dump our database schema

rake db:schema:dump

它的配置文件是根目录的Rakefile。更多的task可以放在lib/tasks下

$ rake -T # 返回它所有能执行的tasks list
$ rake -T db # 只返回跟db有关的

我们可以给rake传递环境变量,用全大写作为变量名,等于相应的值。

$ rake db:schema:dump RAILS_ENV=production
$ rake db:schema:dump RAILS_ENV=development

Migration

What is migration?

  • Set of database instruction written in ruby
  • “Migrate” your database from one state to another
  • Describe database changes
    • create table
    • add column
    • change name of column
    • drop a table
  • Contains instructions for:
    • Moving “up” to a new state
    • Moving back “down” to the previous state

Why use migrations?

  • Keeps database schema with application code
  • Executable and repeatable
  • Allow sharing schema changes
  • Helps with versioning
  • Allows writing ruby instead of SQL

Generating migrations

$ rails generate migration <name>

会在db/migrate下生成一个文件

class DoNothingYet < ActiveRecord::Migration
  # def change
  # end
  
  def up
  end
  
  def down
  end
end

为了更清晰,我们可以把这个文件中的change改为updown

Generate Model

  • Command line
    • rails generate model SingularName
    • rails generate model Subject
  • Create file in db/migrate
    • File name: 20130101000000_create_subjects.rb
    • Class name: CreateSubjects
    • Inherits from: ActiveRecord::Migration
    • create_table :subjects, drop_table :subjects
  • Create file in app/models
    • File name: subject.rb
    • Class name: Subject
    • Inherits from: ActiveRecord::Base
  • File names, class names and table names matter

Example:

$ rails generate model User
Running via Spring preloader in process 602
      invoke  active_record
      create    db/migrate/20160302022648_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

Files generated and modified:

class CreateUsers < ActiveRecord::Migration
  # def change
  def up
    # create_table :users, {:id => false} do |t|
    # suppress automatic additon of id
    create_table :users do |t|
      t.column 'first_name', :string, :limit => 25
      t.string 'last_name', :limit => 50
      t.string 'email', :default =>'', :null => false
      t.string 'password', limit => 40
      
      # t.datetime 'created_at' 
      # t.datetime 'updated_at'
      # 'created_at' and 'updated_at' are
      # special column names, rails will autoupdate them for us
      
      t.timestamps null: false #short hand for aboveå
    end
  end
  
  def down
    drop_table :users
  end
end

Table column types

binary, boolean, date, datetime, decimal, float, integer, string, text, time

Table column options

  • :limit => size
  • :default => value
  • :null => true/false
  • :precision => number
  • :scale => number

Running Migrations

$ rake db:migrate <RAILS_ENV=development>

所有创建了但没有运行过的migration,rake都会run。

进入mysql查看

mysql> SHOW TABLES;
+----------------------------------+
| Tables_in_simple_cms_development |
+----------------------------------+
| schema_migrations                |
| users                            |
+----------------------------------+
2 rows in set (0.00 sec)

mysql> SHOW FIELDS FROM users;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| first_name | varchar(25)  | YES  |     | NULL    |                |
| last_name  | varchar(50)  | YES  |     | NULL    |                |
| email      | varchar(255) | NO   |     |         |                |
| password   | varchar(40)  | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)

mysql> SHOW FIELDS FROM schema_migrations;
+---------+--------------+------+-----+---------+-------+
| Field   | Type         | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| version | varchar(255) | NO   | PRI | NULL    |       |
+---------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec)

mysql> SELECT * FROM schema_migrations;
+----------------+
| version        |
+----------------+
| 20160302021538 |
| 20160302022648 |
+----------------+
2 rows in set (0.00 sec)

db/schema.rb文件也有变化

ActiveRecord::Schema.define(version: 20160302022648) do

  create_table "users", force: :cascade do |t|
    # t represents table
    t.string   "first_name", limit: 25
    t.string   "last_name",  limit: 50
    t.string   "email",      limit: 255, default: "", null: false
    t.string   "password",   limit: 40
    t.datetime "created_at",                          null: false
    t.datetime "updated_at",                          null: false
  end

end

它代表我们database的当前状态

Migration down

$ rake db:migrate VERSION=0
== 20160302022648 CreateUsers: reverting ======================================
-- drop_table(:users)
   -> 0.0070s
== 20160302022648 CreateUsers: reverted (0.0071s) =============================

== 20160302021538 DoNothingYet: reverting =====================================
== 20160302021538 DoNothingYet: reverted (0.0000s) ============================

再回到mysql

mysql> SHOW TABLES;                                                                                 
+----------------------------------+
| Tables_in_simple_cms_development |
+----------------------------------+
| schema_migrations                |
+----------------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM schema_migrations;
Empty set (0.00 sec)

run特定的migration

$ rake db:migrate:status
database: simple_cms_development

 Status   Migration ID    Migration Name
--------------------------------------------------
  down    20160302021538  Do nothing yet
  down    20160302022648  Create users
  
$ rake db:migrate VERSION=20160302021538

== 20160302021538 DoNothingYet: migrating =====================================
== 20160302021538 DoNothingYet: migrated (0.0000s) ============================

$ rake db:migrate

== 20160302022648 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0102s
== 20160302022648 CreateUsers: migrated (0.0103s) =============================

# 再回到上一个版本,只会退一步
rake db:migrate VERSION=20160302021538
== 20160302022648 CreateUsers: reverting ======================================
-- drop_table(:users)
   -> 0.0080s
== 20160302022648 CreateUsers: reverted (0.0081s) =============================

$ rake db:migrate:status

database: simple_cms_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20160302021538  Do nothing yet
  down    20160302022648  Create users

还有三个命令我们没提到

$ rake db:migrate:up VERSION=20160302021538 # 只做up method
$ rake db:migrate:down VERSION=20160302021538 # 只做down method
$ rake db:migrate:redo VERSION=20160302021538 # 先做down,后做up

Migration methods

Table migration methods
create_table(table, options) do |t|
   # t represents table
	...columns...
end

drop_table(table)

rename_table(table, new_name)
Column migration methods
add_column(table, column, type, options) # 不能用short format
remove_column(table, column)
rename_column(table, column, new_name)
change_column(table, column, type, options) # non-destructively
Index migration methods
add_index(table, column, options)
remove_index(table, column)

options:

:unique => true/false :name => “your_custom_name”

Execute migration method
execute("any SQL string")

案例:

$ rails generate migration AlterUsers
Running via Spring preloader in process 1687
      invoke  active_record
      create    db/migrate/20160302034635_alter_users.rb

change method

在migration中change即可以up,也可以down,例如

class AlterUsers < ActiveRecord::Migration
  def change
    rename_table('users', 'admin_users')
  end
end

但是有些时候change并不能完全返回,例如你删除了一个column,是无法恢复的。所以最好还是用up和down,这样每一步做什么都很清晰。

测试:

class AlterUsers < ActiveRecord::Migration
  # def change
  def up
    rename_table('users', 'admin_users')
    add_column('admin_users', 'username', :string, :limit => 25,
      :after => 'email')
      #:after works with mysql, need to check availability for other db system
    change_column('admin_users', 'email', :string, :limit => 100)
    rename_column('admin_users', 'password', 'hashed_password')
    puts "*** Adding an index is next ***"
    add_index('admin_users', 'username')
    # always add index to foreign key
  end
  
  def down
    remove_index('admin_users', 'username')
    rename_column('admin_users', 'hashed_password', 'password')
    change_column('admin_users', 'email', :string, :default =>'', :null => false)
    remove_column('admin_users', 'username')
    rename_table('admin_users', 'users')
  end
end

要测试并保证每一个migration正反都能运行。

$ rake db:migrate
$ rake db:migrate VERSION=0

Debug Migration

  • Comment out executed code lines to get back on track
    • Write SQL commands as a last resort
  • Keep migrations small and concise
  • Test all migrations thoroughly in development