读《为你自己学RUBY ON RAILS--高见龙》提取记录

写在前面:这里的提取是个人提取(只记录不知道的东西)。

电子书地址:http://railsbook.tw/

提示:在学RUBY基础时,有时不要把它和rails混在一起思考。先有ruby才有rails,ruby不了解rails,但rails很了解ruby....
开发工具和常用指令
  1. 如果出门在外,忘了带mac,怎么写代码???答案是可以用三线编辑器:

后两款:自带开发环境!!!!

  1. 常用指令:
  • sudo 暂时取得权限
第一个应用程式
  1. scaffold的作用:自动完成增删改查的功能,包括MVC。

  2. 在新增migration时,数据类型string可以省略(因为是默认)。

  3. N+1查询问题,就是会额外查询关联的资料库,对系统资源很浪费)。解法:

  • includes来减少不必要的资料库查询。


    def index
    @posts = Post.all
    end

    改成:(里是关联了user)


    def index
    @posts = Post.includes(:user)
    end

變數、常數、流程控制、迴圈
  1. 如果想交换x,y两个变数的值,怎么写最ruby???


x, y = y, x

  1. if可以放到程式的后面(这样读起来更像英文文章)


a=1 if b > a #如果b > a,那么a = 1

  1. 三元连算子:(if ….. else …..这个语法放到同一行)

```
if a > b
a = 1
else
a = 2
end

#上面的程式码,可以压缩成一行:
a > b ? a = 1 : a = 2

#帅到掉渣,有木有!
```

數字、字串、陣列、雜湊、符號
  1. 注意= ==的问题。一个是赋值,一个是比较!
  2. 整数的除法,其中一个数为小数,结果才为小数。
  3. 取余数%
    • 判断是否为单数:.odd?
    • 判断是否为复数: .even?
  4. 四舍五入.round(x)
  5. 字串中要插入变量,可以用#{}!!!
  6. 字串可以直接当成数组来用,取字。
  7. 如何获得字串的长度:
    • 长度: .size
    • 字节: .bytesize
  8. 计算几个字几个单词: .sqlite.count
  9. 大小写转换:
    • 转大写: upcase
    • 转小写: downcase
  10. 对比首尾字母:
    • start_with?
    • end_with?
  11. 替换掉其中的片段:
    • 只换第一个: sub
- 全部换掉:`gsub`
​```
  puts "PHP is good, and I love PHP".sub(/PHP/, "Ruby")
  # sub 只會換掉最先遇到的那個字串
  # => 印出「Ruby is good, and I love PHP」

  puts "PHP is good, and I love PHP".gsub(/PHP/, "Ruby")
  # gsub 會換掉全部符合的字串
  # => 印出「Ruby is good, and I love Ruby」
​```
  1. map方法示例:


list = [1, 2, 3, 4, 5]
p list.map { |i| i * 2 - 1 } # [1, 3, 5, 7, 9]

  1. reduce方法示例:(只作数值与index的运算)


puts [*1..100].reduce(:+)

  1. select方法示例:(相当于一个过滤器)


p [*1..100].select(&:odd?)

  1. 取随机数的方法:
  • shuffle洗牌:


    puts [*1..52].shuffle.first(5)

  • sample不重复的乱数:


    puts [*1..100].sample(5)

  1. object_id可以检测是否同一物件。

  2. 什么是symbol一个带有名字的物件(其他物件都没有名字,只有它有)

  3. stringsymbol的对比:

  • string可以被改变,symbol不行
  • symbol可以直接从内存中取得相同的东西,不会像string总是新建物件,所以一定程度上symbol是比较省内存的。
  • symbol的比较会比string快!
  • string和symbol是可以相互转换的!
方法與程式碼區塊(block)
  1. 在 Ruby 執行方法,經常省略小括號,目的是為了讓程式碼看起來更不像程式碼,反而像是一般的文章。

  2. 定义方法时,可以加个预设值:

```
def say_something(message = "something")
"message: #{message}"
end

 p say_something "hi"     # => message: hi
 p say_something          # => message: something

```

  1. ruby很爱省略小括号和大括号,这里日后可能会掉坑~

  2. 什么是程式码区块

{ ... } 以及 do ... end,在 Ruby 稱之一個程式碼區塊(Block)

  1. 程式码区块不是:
  • 不是物件:无法单独存在

  • 不是参数:无法当参数,放在方法后一般不会执行。要如何才能执行:

    ```
    def say_hello
    puts "開始"
    yield # 把控制權暫時讓給 Block
    puts "結束"
    end

    say_hello {
    puts "這裡是 Block"
    }
    ```

    6666,原来这两个地方都可以传参数……:

    ```
    def say_hello
    puts "開始"
    yield 123 # 把控制權暫時讓給 Block,並且傳數字 123 給 Block
    puts "結束"
    end

    say_hello { |x| # 這個 x 是來自 yield 方法
    puts "這裡是 Block,我收到了 #{x}"
    }
    ```

  1. block的物件化:

```
say_hello_to = Proc.new { |name| puts "你好,#{name}"}
say_hello_to.call("尼特羅會長")

 say_hello_to.call("尼特羅會長")    # 使用 call 方法
 say_hello_to.("尼特羅會長")        # 使用小括號(注意,有多一個小數點)
 say_hello_to["尼特羅會長"]         # 使用中括號
 say_hello_to === "尼特羅會長"      # 使用三個等號
 say_hello_to.yield "尼特羅會長"    # 使用 yield 方法

```

類別(Class)與模組(Module)
类别
  1. 什么类别:
物件(object) = 狀態(state) + 行為(behavior)

一个精妙的类比:

不知道大家有沒有在夜市有看過或吃過雞蛋糕,有小貓、小狗等可愛動物造型,只要把調配好的麵粉糊倒進模具,蓋上蓋子,幾分鐘後就會有香噴噴又造型可愛的雞蛋糕可以吃了。

photo by Bryan Liu

那個烤盤模具,就是類別(Class)的概念。如果沒意外,一樣形狀的模具,放一樣的原料進去,做出來雞蛋糕的型狀應該都會長得一樣。而這個做出來的雞蛋糕,以物件導向程式設計的概念來說便稱之「實體(Instance)」。

  1. 传参数初始化:

```
class Cat
def initialize(name, gender)
@name = name
@gender = gender
end

   def say_hello
     puts "hello, my name is #{@name}"
   end
 end

 kitty = Cat.new("kitty", "female")
 kitty.say_hello    # => hello, my name is kitty

```

  1. attr_accessor的作用:

帮助设定和取用实体变数的值!(ruby没有【属性】一说!)

所以,以下这个:

```
class Cat
def initialize(name, gender)
@name = name
@gender = gender
end

   def say_hello
     puts "hello, my name is #{@name}"
   end

   def name
     @name
   end

   def name=(new_name)
     @name = new_name
   end
 end

 kitty = Cat.new("kitty", "female")
 kitty.name = "nancy"
 puts kitty.name        # => nancy

```

可以直接变成:

```
class Cat
attr_accessor :name

   def initialize(name, gender)
     @name = name
     @gender = gender
   end

   def say_hello
     puts "hello, my name is #{@name}"
   end
 end

```

有了这个设定后,我们就可以直接用这个参数,像属性一样方便用!!!

  1. 什么实体方法,什么是类别方法:
  • 作用在实体上的方法,叫做:实体方法。

  • 作用在类别上的方法,叫做:类别方法。这个有几种表现:

    • class Cat def self.all # ... end end

    ```

    • class Cat class << self def all # ... end end end

    有很多类别方法时,可以用这种!

  1. 继承:可以让代码更简洁

  2. open class: 可以方便地为方法“加料”!

  3. 如果想要分开定义一些方法,可以用到模组,方法:


module Flyable
def fly
puts "I can fly!"
end
end


class Dog
include Flyable
end

  1. 模组和类别的不同之处:
  • 模组没办法new一个新的实体出来。
  • 模组没办法继承别的模组。
  1. 定义外部键时,使用references来自动增加外部键:


rails g model post user:references

这样,references会做多两件事:

  1. 自动加上索引,加快查询速度。
  2. 自动帮model加入关联。
Layout,Render 與 View Helper
  1. 在特定的controller 的action里,如何你想用不同的layout可以这样:

```
class CandidatesController < ApplicationController

   def index
     @candidates = Candidate.all
     render layout: "backend"
   end

   # ...[略]...
 end

```

  1. 如果想给每个页面定义网页title,这个好!可以这样做:
  • 方法1:使用 provide:

```
<!DOCTYPE html>


<%= yield :my_title %>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
   <body>
     <div class="container">
     <%= yield %>
     </div>
   </body>
 </html>

```

```
<% provide :my_title, "你好,我是 Title" %>

 <h1>候選人列表</h1>

 <%= link_to "新增候選人", new_candidate_path %>

 <table class="table">
   <thead>
     <tr>
       <td>投票</td>
       <td>候選人姓名</td>
 ...[略]...                                        

```

  • 使用 content_for:

```
<% content_for :my_title do %>
你好,我是 Title
<% end %>

 <h1>候選人列表</h1>

 <%= link_to "新增候選人", new_candidate_path %>

 <table class="table">
   <thead>
     <tr>
       <td>投票</td>
       <td>候選人姓名</td>
 ...[略]...

```

  1. 更好地用render:
  • partial里尽量用区域变量。
  • 使用时再传递这个变量。

例子:


<div class="advertisement">
<div>廣告</div>
<div> <%= content %> </div>
</div>


<%= render "banner", content: "我是廣告的內容" %>

  • partial里有一个collection的方法,使用时传递这个参数,就可以迭代输出partial。(回圈效果)


<%= render partial: "candidate", collection: @candidates %>

这里还可以简化成:


<%= render @candidates %>

但要满足以下条件:(利用了rails的惯例)

  • _xxx.html.erb的名字必须是这包资料的单数
  • 位置必须放在同一目录下。
  • _xxx.html.erb里不要写回圈,里面的区域变数必须是单数。
  1. 善用View Helper !
Model 基本操作
  1. 当我们需要引用外部数据库时,假如我们想改名字,可以这样做:


class User < ActiveRecord::Base
self.table_name = "example"
end

  1. 什么是主键(primary key):每個資料表的流水編號欄位(ID).

当然这个主键也可以自定义

  • class User < ActiveRecord::Base self.primary_key = "user_id" end

```

```

  1. 什么是ORM?

我們如果想要存取資料庫裡的內容,以前必需透過資料庫查詢語言(SQL)向資料庫進行查詢,但透過 ORM 的包裝之後,可以讓我們用操作「物件」的方式來操作資料庫。

  1. 平均数求和最大值最小值 直接让数据库来计算!

```
$ bin/rails console

Candidate.sum(:age)
(0.2ms) SELECT SUM("candidates"."age") FROM "candidates"
=> 44

Candidate.average(:age).to_f
(0.1ms) SELECT AVG("candidates"."age") FROM "candidates"
=> 14.6666666666667
```

```
$ bin/rails console

Candidate.maximum(:age)
(0.2ms) SELECT MAX("candidates"."age") FROM "candidates"
=> 22
Candidate.minimum(:age)
(0.2ms) SELECT MIN("candidates"."age") FROM "candidates"
=> 2
```

  1. 更新数据的方法有 saveupdateupdate_attributeupdate_attributes
  • ```
    # 使用 save 方法
    candidate.name = "剪彩倫"
    candidate.save

    # 使用 update_attribute 方法更新單一欄位的值(注意:方法名字是單數)
    candidate.update_attribute(:name, "剪彩倫")

    # 使用 update 更新資料,可一次更新多個欄位,且不需要再呼叫 save 方法
    candidate.update(name: "剪彩倫", age: 20)

    # 使用 update_attributes 方法
    candidate.update_attributes(name: "剪彩倫", age: 20)
    ```

``
單數的
update_attribute方法會跳過驗證(Validation),等於是save(validate: false)` 的效果,在使用的時候要稍微注意一下。

  • 如果想整个资料表一起修改:

```


Candidate.update_all(name: "剪彩倫", age: 18)

```

```

  1. scope可以嵌套,利用嵌套,可以方便我们:
  • 使代码整洁。
  • 方便我们修改。

还可以:预设scope,


class Product < ActiveRecord::Base
default_scope { order('id DESC') }
scope :available, -> { where(is_available: true) }
end

要取消預設的 scope,必須使用 unscope 方法:

```
$ bin/rails console

Product.unscope(:order)
Product Load (0.2ms) SELECT "products".* FROM "products"

Product.unscope(:order).order(:title)
Product Load (0.3ms) SELECT "products".* FROM "products" ORDER BY "products"."title" ASC

```

這樣才能把預設的 scope 的效果移除。

Model Migration
  1. 什么是migration: 描述数据表长什么样的档案。

  2. 为什么要这样设计:一切为了方便多人协作!

  3. 刚执行rake db:migrate发现要改一下栏位,怎么办?

  • 可以用rollback : rake db:rollback (这样一次可以退回一个migration文件。)
  • 新增migration文件来修改。(推荐!)
  1. 新增migration文件时,原来可以直接上index索引啊,酷!:


$ bin/rails g migration add_candidate_id_to_articles candidate_id:integer:index
Running via Spring preloader in process 7765
invoke active_record
create db/migrate/20170101081538_add_candidate_id_to_articles.rb


class AddCandidateIdToArticles < ActiveRecord::Migration[5.0]
def change
add_column :articles, :candidate_id, :integer
add_index :articles, :candidate_id
end
end

  1. rake db:setup可以一口氣把資料表建完,順便把預設資料寫入。
model的关联性
  1. 什么是外部键:用于对应其他model主键的栏位,例如:user_id product_id

  2. 可以用user.create_post(:title => "hello")来新建对应的资料,生成的资料自动对应好user_id。

  3. 之前商店大赛加油站,无法先新建address再对应user_id,现在终于找到原因和解决方法!:

  • 原因:rails5之后,必须先建立才能建立

  • 解法:加多一个optional: true


    class Store < ApplicationRecord
    belongs_to :user, optional: true
    end

  1. has_one 跟 belongs_to 方法需要同時設定嗎?

不一定,端看需求,一樣以我們上面 User 跟 Store 的例子來看,如果你不需要「從 Store 反查 User」的功能的話,那 belongs_to 是不需要設定的。

  1. 其实多对多没有我们rails101教材上的那么复杂,其实可以这么简单:


class WareHouse < ApplicationRecord
belongs_to :store
belongs_to :product
end

```
class Store < ApplicationRecord
belongs_to :user

   has_many :ware_houses
   has_many :products, through: :ware_houses
 end

```


class Product < ApplicationRecord
has_many :ware_houses
has_many :stores, through: :ware_houses
end

看到没有,第二个has_many只要写两个字段就可以啦!之前是这样的:

有点让新手摸不着头脑。。。

  1. 酷!还有一种叫HABTM(has_and_belongs_to_many)!

```
# Store Model
class Store < ActiveRecord::Base
has_and_belongs_to_many :products
end

 # Product Model
 class Product < ActiveRecord::Base
   has_and_belongs_to_many :stores
 end

```

就這樣,不需要另外新增第三方 Model 即可完成多對多關連。注意,我是說「不需要第三方 Model」,不是「不需要第三方資料表」,畢竟還是要有一個資料表存放雙方的資訊,只是這個資料表因為不重要也不會存取它,所以可以不需要 Model 對應。

這個第三方資料表的名字是有規定的,預設是「兩個資料表依照英文字母先後順序排序,中間以底線分格」,所以以我們這個例子來說,這個資料表的名字就是「products_stores」。

Model 驗證及回呼
  1. 资料验证:有三个方法:
  • 前端驗證:在 HTML 頁面使用 JavaScript 在使用者填寫資料的時候就先檢查。
  • 後端驗證:資料傳進來在寫入資料庫之前之後再檢查。
  • 資料庫驗證:直接由資料庫本身所提供的功能來做資料驗證。

推荐model层来做这件事!

  1. 验证是否为空:
  • class Article < ApplicationRecord validates :title, presence: true end

```


  • class Article < ActiveRecord::Base
    validates_presence_of :title
    end

    这两种效果是一样的。

  1. 想查看错误的提示信息?
  • 检查是否有错误提示:a1.errors.any?
  • 检查错误提示是什么:a1.errors.full_messages
  1. 只有这些方法会触发验证:
  • create
  • create!
  • save
  • save!
  • update
  • update!

其它方法不會經過驗證流程喔

有驚嘆號版本的,如果驗證未通過會產生錯誤訊息,而沒有驚嘆號版本則僅會回傳該 Model 的一個空物件。这大概就是惊叹号的好处了!

  1. 如何跳过验证?


user1 = User.new
user1.save(validate: false)

  1. 检查是否能通过验证?

```

user1.valid?
=> false
```

  1. 回呼(Callback):

其中,顏色比較深的那幾個流程是有機會可以掛上一些方法,又稱之回呼(Callback)

注意:before_savebefore_create 的差別,在於 before_save 是每次存檔的時候都會經過,但 before_create 只有在「新增」的時候才會觸發。

除了這樣的寫法,如果內容單純的話,也是可以使用 Block 的方式來寫:


require 'digest'
class User < ActiveRecord::Base
before_create do
self.email = Digest::MD5.hexdigest(email)
end
end

寄發信件
  1. 之前的奇怪,现在不奇怪了,寄送信件,rails已内置好这个功能了,使用很容易!
背景工作及工作排程(异步任务)

慌然大悟,原来这就是我之前一直想学的异步任务!!!!相见恨晚!

如果我要写一个异步任务,可以这样做:

  1. 执行rails g job user_confirm_email

  2. 设置 app/jobs :

```
class UserConfirmEmailJob < ApplicationJob
queue_as :default

   def perform(user)
     # 在這裡寄發確認信...
   end
 end

```

  1. 在controller里作用:


def create
@user = User.new(user_params)
if @user.save
UserConfirmEmailJob.perform_later(@user)
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end

搞定!

一些小技巧:

  • 其中,queue_as 方法是指這件工作急不急,預設值是 :default,如果這件工作不急,可把 :default 改成 :low_priority,如果是急件則可設定成 :urgent

  • ```

    這樣是 5 秒之後做

    UserConfirmEmailJob.set(wait: 5.seconds).perform_later(@user)

    這樣是「明天下午有空再做」

    UserConfirmEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(@user)
    ```

  • 通常异步任务会被放在缓存中,如果宕机,重开机这些资料会不见。安全起见,可以用第三方gem:(把任务放入资料库)常用:有 SidekiqDelayed Job

下面以Delayed Job为例:

  • Gemfile.rb 中增加:


    gem 'delayed_job_active_record'


  • $bundle

  • 修改:app/configs/application.rb

API 模式
  1. 直接用render json: @users会输出所有的json数据。如果想过滤一些数据的话,可以这样:
  • 新增app/views/users/index.json.jbuilder:


    json.array! @users, partial: 'users/user', as: :user

  • 新增app/views/users/_user.json.jbuilder:


    json.extract! user, :id, :name, :email, :created_at, :updated_at
    json.url user_url(user, format: :json)

  1. 有一个叫API-Only 的东西,在新增专案时,选择这种模式,可以帮rails减肥!


$ rails new my_blog --api

寫測試讓你更有信心 Part 1
  1. 为什么要写测试:

前期看起来有点花功能 ,但却是为后面多次开发的测试节省时间!

不写测试可能还有各种偷懒的理由,但,跟着大神学着写吧~

不要為了測試而測試,你寫的是「規格」(Spec)

  1. 开发前写一写“测试“,这里作者说是规格
下面以RSpec来实作:
  1. 安装RSpec:


$ gem install rspec

  1. 把要有哪些项目要测试,先写出来:

```
# 檔案:bank_account_spec.rb

 RSpec.describe BankAccount do
   describe "存錢功能" do
     it "原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元" do
     end

     it "原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額)" do
     end
   end

   describe "領錢功能" do
     it "原本帳戶有 10 元,領出 5 元之後,帳戶餘額變 5 元" do
     end

     it "原本帳戶有 10 元,試圖領出 20 元,帳戶餘額還是 10 元,但無法領出(餘額不足)" do
     end

     it "原本帳戶有 10 元,領出 -5 元之後,帳戶餘額還是 10 元(不能領出小於或等於零的金額)" do
     end
   end
 end

```

然后开始写内容:

  1. 然后,开始$ rspec bank_account_spec.rb进行测试,自然会出错。

  2. 根据错误提示,把所有bug都解掉!

  3. 一些小技巧:

  • before(:each)
  • before(:all)
  • let(:account){BankAccount.new(10)}这个语句相当于动态地为整个方法提供一个区块变数。
  1. 寫測試是算是業界很常見的標準技能,所以:写吧!
代码重构:
  1. 页面上的逻辑可以这样处理:
    • 写进viewhelper
    • 在model里写义一个实体方法,然后在页面就只可以直接用啦!
  2. 善用继承,不能继承的就引入模组功能。
代码重构(进阶版):
  1. 設計 Service Object 類別,新增纯ruby的类别。
  2. 使用 Form Object

表示暂时看不太懂。。。先放过。。

后面的内容实操一遍,基本没有什么重要的知识了……。。

当然这个课程还有一些不错的内容在更新中……

comments powered by Disqus