## 建立專案 ```bash # 不指定版本 $ rails new project_name # 指定版本 $ rails _6.1.4.6_ new project_name ``` ### 啟動 ```bash $ rails s ``` ### 安裝套件 ```bash # 安裝特定套件 $ gem install rubocop # 安裝 Gemfile 內列出內容 $ bundle install ``` ## 2025/11/10 rails install ```bash # 安裝 rbenv & ruby-build brew install rbenv ruby-build # 把 rbenv 加到 zsh 啟動腳本 echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc source ~/.zshrc rbenv -v # 用 rbenv 安裝 & 切換 Ruby 版本,gem 安裝 Rails rbenv install 3.3.0 rbenv global 3.3.0 gem install rails rails -v # 如果 gem install rails 顯示 ou don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory # 需要檢查當前路徑在哪裡,讓 rails 指向自己安裝的不是使用 mac 本身的 ruby exec $SHELL -l rbenv rehash # 用已安裝好的 rails build project rails new myapp cd myapp rails s # 如果要指定版本 # 先安裝指定版本的 Rails gem gem install rails -v 6.1.4.6 # 用該版本去產生專案 rails _6.1.4.6_ new myapp cd myapp_6_1 bundle exec rails -v ``` ## 指令 - rollback 會有風險,如果已經有資料存在,但必需得回滾的話,建議在新增一個 migration 修正。 - 新增欄位會產生一個空白的檔案,需要自行加上`add_column :articles, :photo, :string`,第一參數:資料表名稱、第二參數:欄位名稱、第三參數:資料型態。 ```rb # 建立 controller $ rails g controller user # 建立 model $ rails g model User name:string age:integer # 資料遷移 $ rails db:migrate # 資料回滾 $ rails db:rollback # 查看哪些 migration 已執行 # up:已執行、down:未執行 $ rails db:migrate:status # 新增、修改、刪除資料表欄位 $ rails g migration add_phone_cache_to_user $ rails g migration RenameDescription ``` ## 數字 number ```rb puts 10 / 3 # 3 puts 1 + 2 # 3 ``` 問題:要怎麼知道某個數字是不是奇數? ```rb number = 10 if number % 2 == 0 puts "偶數" else puts "奇數" end ``` 或使用內建判斷做法 ```rb puts 10.odd? # false puts 10.even? # true ``` 問題:要怎麼做「四捨五入到小數點第二位」? ```rb # 四捨五入到小數點第二位 puts 3.3333333.round(2) # 3.33 # 四捨五入 puts 3.834.round # 4 ``` ## 字串 string 字串安插 ```rb name = "Jeter" age = 18 puts "你好,我是#{name},我今年#{age}。" # 你好,我是Jeter,我今年18。 ``` 字串長度計算 ```rb puts "Jeter".size # 5 ``` 計算有多少個指定字元 ```rb # 先使用 split 方法拆解成陣列,在使用 count 計算 words = "Ut in aliquam mauris. Donec dolor quam, sagittis id efficitur vel, convallis vitae tortor" puts words.split.count # 14 ``` 字串的大小寫轉換 ```rb puts "Jeter".upcase # 全大寫 JETER puts "Jeter".downcase # 全小寫 jeter puts "Jeter".swapcase # 自首小寫,其餘大寫 jETER puts "jeter".capitalize # 自首大寫,其餘小寫 Jeter ``` 想要知道某個字母在字串中共出現幾次 ```rb words = "Lorem Ipsum Dolor Sit Amet, Consectetur Adipiscing Elit." puts words.count("i") # 5 # for loop words = "Lorem Ipsum Dolor Sit Amet, Consectetur Adipiscing Elit." count = 0 words.each_char do |word| if word == "i" count += 1 end end puts "字母 i 出現 #{count} 次。" # 字母 i 出現 5 次。 ``` 想知道字串是不是特定字元開題或結尾?或是包含指定字串或字元? ```rb puts "Jeter".start_with?("J") # true puts "Jeter".start_with?("j") # false puts "Jeter".include?("J") # true puts "Jeter".include?("j") # false ``` 想把字串裡的某些字換成其它字 ```rb # sub 方法只會替換第一個遇到的字串。 puts "PHP is good, and I love PHP".sub(/PHP/, "Ruby") # Ruby is good, and I love PHP # gsub 方法會替換掉所有符合的字串。 puts "PHP is good, and I love PHP".gsub(/PHP/, "Ruby") # Ruby is good, and I love Ruby ``` ### empty? 檢查字串是否為空字串 ```rb puts "Jeter".empty? # false puts " ".empty? # false,空白字元不算空字串 puts "".empty? # true ``` ## 陣列 array 建立陣列的方法 ```rb p Array.new # [] p Array.new(5) # [nil, nil, nil, nil, nil] p Array.new(5, "Ruby") # ["Ruby", "Ruby", "Ruby", "Ruby", "Ruby"] ``` 通常會使用`[]`方式 ```rb friends = ["Jeter", "Jason", "Jay"] ``` 陣列取用 ```rb friends = ["Jeter", "Jason", "Jay"] puts friends[0] # Jeter ``` 取得陣列第一或最後元素 ```rb friends = ["Jeter", "Jason", "Jay"] puts friends.first # Jeter puts friends.last # Jay ``` 1. 把陣列`[1, 2, 3, 4, 5]`變成`[1, 3, 5, 7, 9]` ```rb number = [1, 2, 3, 4, 5] result = [] number.each do |num| result << num * 2 - 1 end p result # [1, 3, 5, 7, 9] ``` 或是可以使用`map` `map`方法會對陣列中的元素做某件事後再收集成新的陣列。 ```rb number = [1, 2, 3, 4, 5] p number.map { |num| num * 2 - 1} # [1, 3, 5, 7, 9] ``` 2. 計算從 1 加到 100 的總和 ```rb total = 0 (1..100).to_a.each do |i| total = total + i end puts total # 5050 ``` 或是使用`*`展開 ```rb total = 0 [*1..100].each do |i| total += i end puts total # 5050 ``` `reduce`方法會對每個元素不斷的`互動(例子為相加)`,再把最後結果收集起來。 ```rb puts [*1..100].reduce(0) { |total, num| total + num } #. 5050 ``` 3. 請印出 1 ~ 100 數字中所有的單數 ```rb result = [] [*1..100].each do |num| if num % 2 != 0 result << num end end p result # [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99] ``` `select`方法可篩選出符合條件的元素並收集成陣列。 ```rb p [*1..100].select { |num| num % 2 != 0 } # [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99] ``` 要計算奇數、偶數可利用數字物件內建方法 ```rb p [*1..100].select { |i| i.odd? } ``` 4. 如果想要從 1 到 100 中隨便取 5 個不重複的亂數 ```rb puts [*1..100].sample(5) ``` ## 範圍 range .. 和 ... 差異 ```rb (1..5).to_a # [1, 2, 3, 4, 5] (1...5).to_a # [1, 2, 3, 4] ``` 範圍也可使用`each` or `map` ```rb (1..5).each { |x| puts x } # 1 2 3 4 5 (1..5).map { |x| x * 2 } # [2, 4, 6, 8, 10] ``` 取陣列內容也可使用範圍 ```rb languages = ["ruby", "python", "php", "javascript", "rails"] languages[1..3] # ["python", "php", "javascript"] ``` ## 雜湊 hash Ruby 1.9 版本前後差異如下,但意思上是相同的。 ```rb old_hash = {:title => "Ruby", :price => 350} new_hash = {title: "Ruby", price: 350} puts old_hash == new_hash # true ``` Ruby 中要取得`hash`內容是需要使用`symbol`也就是`:`冒號,`:`和`" "`是不同東西,例如:`profile[:name]` and `profile["name"]`。 ```rb profile = {name: "Jeter", age: 10} puts profile[:name] ``` 1. 要怎麼把 Hash 裡的東西一個一個印出來? ```rb profile = {name: "Jeter", age: 10} p profile.keys # [:name, :age] profile = {name: "Jeter", age: 10} p profile.values # ["Jeter", 10] ``` 如果要一個一個打印出來 ```rb profile = {name: "Jeter", age: 10} profile.each do |elements| p elements end # [:name, "Jeter"] # [:age, 10] ``` 如果要個別取得`key`and`value` ```rb profile = {name: "Jeter", age: 10} profile.each do |key, value| puts key puts value end # name # Jeter # age # 10 ``` ## 符號 symbol ```rb class Order attr_reader :status def initialize(items, status = :pending) @items = items @status = status end def complete @status = :complete end end order = Order.new(["item A", "item B", "item C"]) if order.status == :pending puts "order is pending" elsif order.status == :complete puts "order is complete" else puts "not started" end # order is pending # 如果呼叫 complete 會改變輸出內容 order.complete if order.status == :pending puts "order is pending" elsif order.status == :complete puts "order is complete" else puts "not started" end # order is complete ``` `symbol` and `string` 差異 前者無法改變內容,後者可以,所以可把`symbol`看成`不可變`的字串。 ```rb "hello"[0] = "k" # k :hello[0] = "k" # `<main>': undefined method `[]=' for :hello:Symbol (NoMethodError) ``` `symbol` and `string` 是可以互相轉換的 ## method and block `method`定義和`python`是一樣的,都是使用`def`作為開頭,差別在`ruby`在結尾會加上`end`。 ```rb def hello(name) puts "hello, #{name}" end hello("Jeter") # hello, Jeter ``` `ruby`可將方法使用`symbol`表示,依照下列程式碼來看可以把陣列中的`符號symbol`使用`send`方法直接呼叫,意思是會讓`:hello`這個符號去尋找對應的名為`hello`的方法然後執行。 ```rb def hello puts "hello." end def goodbye puts "goodbye." end methods = [:hello, :goodbye] methods.each do |method| send(method) end # hello. # goodbye. ``` 參數預設值 如果有帶參數給方法那就會使用參數內容,否則就會使用預設值 "hi"。 ```rb def hello(message = "hi") "message: #{message}" end p hello("goodgood") # "message: goodgood" p hello() # "message: hi" ``` 在 Ruby 中`return`可被省略 ```rb def bmi(height, weight) weight / height ** 2 end puts bmi(1.60, 60) # 23.43 ``` Ruby 定義方法時可以在命名後方加上`?`或是`!`,記住!只能加在命名後方。 `?` => 布林值 `!` => 改變原始資料結構 ```rb def hello?(age) age >= 20 end puts hello?(18) # false ``` ```rb # 未使用 ! number_list = [1, 2, 3, 4, 5] reversed_list = number_list.reverse p reversed_list # [5, 4, 3, 2, 1] p number_list # [1, 2, 3, 4, 5] # 使用 ! number_list = [1, 2, 3, 4, 5] reversed_list = number_list.reverse! p reversed_list # [5, 4, 3, 2, 1] p number_list # [5, 4, 3, 2, 1] ``` Block 使用 yield 方法,暫時把控制權交棒給 Block,等 Block 執行結束後再把控制權交回來 ```rb def hello puts "開始" yield puts "結束" end hello { puts "這裡是 block" # 開始 # 這裡是 block # 結束 ``` Block 並不是物件,所以無法單獨存在於 Ruby 語言中,所以需要依附在方法或是物件後方。 但如果使用`Proc`類別就可以將 Block 物件化,要使用時需要再該物件使用`call` ```rb greeting = Proc.new { puts "hello world." } greeting.call # hello world. ``` 也可以帶參數 ```rb hello = Proc.new { |name| puts "hello, #{name}" } hello.call("Jeter") # hello, Jeter ``` `Proc`呼叫方法 ![截圖 2025-04-22 15.36.24](https://hackmd.io/_uploads/BJEbD6Nkge.png) ## 類別 class 和 模組 module 類別必需使用常數命名,有了類別後就可以使用`new`方法產生實體(instance)。 ```rb class Cat def eat(food) puts "#{food} 好好吃" end end cat = Cat.new cat.eat "肉泥" # 肉泥 好好吃 ``` 在 Ruby 中如果要透過`new`方法傳入參數,就必需得在物件中定義一個名為`initialize`的方法,不能取名為`init`、`__init__`等。 ```rb class Cat def initialize(name, gender) @name = name @gender = gender end def hello puts "hello, my name is #{@name}." end end cat = Cat.new("Jeter", "female") cat.hello # hello, my name is Jeter. ``` 實體方法和類別方法 ```rb class Cat def initialize(name, gender) @name = name @gender = gender end def hello puts "hello, my name is #{@name}." end end cat = Cat.new("Jeter", "female") cat.hello # 作用在 cat 所產生的實體,所以 hello 為實體方法 class PostsController < ApplicationController def index # 取得所有 Post 資料,這裡的 all 方法作用在 Post 類別上,所以稱作類別方法 @posts = Post.all end end ``` 定義類別方法有幾種方式: 1. 在方法前方加上`self`。 ```rb class Cat def self.all end end ``` 這樣就可使用`Cat.all`呼叫。 2. ```rb class Cat class << self def all end end end ``` 方法的存取控制 - public => 預設公開,所有人都可以使用。 - protected => 可在同個類別內部使用,或是繼承該類別的子類別可使用。 - private => 只在類別內部使用。 ```rb class Cat def eat puts "好吃" end protected def sleeping puts "zzzz....." end private def gossip puts "hello!!!" end end cat = Cat.new cat.eat # 好吃 cat.sleeping # NoMethodError cat.gossip # NoMethodError ``` 但也不是`private`就無法被取得,利用`send`方法把要呼叫的方法當作參數傳遞就可以被取得。 ```rb cat.send(:gossip) # hello!!! ``` 繼承 ```rb class Animal def food(eat) puts "#{eat}好好吃!" end end class Cat < Animal end class Dog < Animal end food = Cat.new food.food("罐頭") # 罐頭好好吃! ``` 模組 不一定都需要使用繼承方式,利用`module`就可讓類別`include`後使用。 ```rb module Flyable def fly puts "我會飛!!!" end end class Cat include Flyable end cat = Cat.new cat.fly # 我會飛!!! ``` ## Routes 使用`resources`會讓 rails 自動產生 8 條路徑,並對應到 controller 的 7 個方法 ```bash Prefix Verb URI Pattern Controller#Action users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy ``` 分別會是: - index --> 使用者列表 - create --> 新增使用者 - new --> 新增使用者頁面 - show --> 檢視單一使用者 - update --> 更新資使用者 - edit --> 編輯使用者頁面 - destroy --> 刪除使用者 prefix 用意,在後面接上`_path`或`_url`後可以變成「產生相對應的路徑或網址」的 View Helper。如果是站內連結,通常會使用`_path`寫法來產生站內的路徑,例如: ```rb products + path = products_path => /products new_product + path = new_product_path => /products/new edit_product + path = edit_product_path(2) => /products/2/edit ``` 如果是使用`_url`則會產生完整的路徑,包括主機網域名稱: ```rb products + url = products_url => http://kaochenlong.com/products new_product + url = new_product_url => http://kaochenlong.com/products/new edit_product + url = edit_product_url(2) => http://kaochenlong.com/products/2/edit ``` 轉址 ```rb # 當使用者進入到 /me 這路徑會轉址到 redirect 頁面,當然路徑也是可以調整的, # 例如:/me.php 這樣做看起來是個 php 做的網站,但其實是使用 Rails 做的,所以這部分滿特別!? get "/me", to: redirect("https://www.highercloud.com.tw/") ``` 在路徑顯示部分,如不需要一次產生 8 條路徑可以使用`only`或`except`來控制,例如: ```rb # 只顯示 index and create routes resources :users, only: [:index, :create] # 這些不顯示 resources :users, except: [:new, :edit, :update, :destroy] ``` 單數 resource 不會產生`:id`路徑且只會產生 7 條路徑(沒有 index)。 ```rb resource :user # new_user GET /user/new(.:format) users#new ``` #### 單複數 resources 使用情境大致可分成,設計使用者只能夠 CRUD 自己的個人資料時使用單數 resource,如果是管理員可以 CRUD 使用者的個人資料時就使用複數 resources。 #### 大腸包小腸設計 可以理解成: ```bash /users/1 --> 檢視使用者 1 號的資料 /users/1/posts --> 檢視使用者 1 號的所有文章 /users/1/posts/2 --> 檢視使用者 1 號的 2 號文章 /users/1/posts/2/edit --> 編輯使用者 1 號的 2號文章 ``` ```bash Prefix Verb URI Pattern Controller#Action user_posts GET /users/:user_id/posts(.:format) posts#index POST /users/:user_id/posts(.:format) posts#create new_user_post GET /users/:user_id/posts/new(.:format) posts#new edit_user_post GET /users/:user_id/posts/:id/edit(.:format) posts#edit user_post GET /users/:user_id/posts/:id(.:format) posts#show PATCH /users/:user_id/posts/:id(.:format) posts#update PUT /users/:user_id/posts/:id(.:format) posts#update DELETE /users/:user_id/posts/:id(.:format) posts#destroy users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy ``` 在路徑設計上因檢視特定資料、編輯、刪除、更新的操作並不一定得知道使用者是誰,所以可以不需要列出所有 routes,從下方設定來看,可看出文章部分因 index、new、create 需要知道 users_id,所以需要包在 users 內,但是 show、update、edit、destroy 並不一定需要知道 users_id 所以可以獨立在外層。 ```rb Rails.application.routes.draw do resources :users do resources :posts, only: [:index, :new, :create] end resources :posts, only: [:show, :edit, :update, :destroy] end ``` 後台網址設定,就是加一個 /admin 路徑在前方,想要做到這樣的網址設計可以直接在 resources 後面接上 path。 ```rb Rails.application.routes.draw do resources :products, path: "/admin/products" end ``` ```bash Prefix Verb URI Pattern Controller#Action products GET /admin/products(.:format) products#index POST /admin/products(.:format) products#create new_product GET /admin/products/new(.:format) products#new edit_product GET /admin/products/:id/edit(.:format) products#edit product GET /admin/products/:id(.:format) products#show PATCH /admin/products/:id(.:format) products#update PUT /admin/products/:id(.:format) products#update DELETE /admin/products/:id(.:format) products#destroy ``` 但在做後台的時候,更常使用 namespace 方法來把 resources 包起來,產生後台的專屬路徑,這樣產生的專屬路徑會在前方加上 /admin。 ```rb Rails.application.routes.draw do namespace :admin do resources :products resources :articles end end ``` ```bash Prefix Verb URI Pattern Controller#Action admin_products GET /admin/products(.:format) admin/products#index POST /admin/products(.:format) admin/products#create new_admin_product GET /admin/products/new(.:format) admin/products#new edit_admin_product GET /admin/products/:id/edit(.:format) admin/products#edit admin_product GET /admin/products/:id(.:format) admin/products#show PATCH /admin/products/:id(.:format) admin/products#update PUT /admin/products/:id(.:format) admin/products#update DELETE /admin/products/:id(.:format) admin/products#destroy admin_articles GET /admin/articles(.:format) admin/articles#index POST /admin/articles(.:format) admin/articles#create new_admin_article GET /admin/articles/new(.:format) admin/articles#new edit_admin_article GET /admin/articles/:id/edit(.:format) admin/articles#edit admin_article GET /admin/articles/:id(.:format) admin/articles#show PATCH /admin/articles/:id(.:format) admin/articles#update PUT /admin/articles/:id(.:format) admin/articles#update DELETE /admin/articles/:id(.:format) admin/articles#destroy ``` 如果不希望後台路徑讓有心人士猜到,可以使用加入 path,這樣一來就把原本的 /admin 改變成自定義的內容,不易猜測。 ```rb Rails.application.routes.draw do # namespace namespace :admin, path: "saoidfai" do resources :products end end ``` ```bash Prefix Verb URI Pattern Controller#Action admin_products GET /saoidfai/products(.:format) admin/products#index POST /saoidfai/products(.:format) admin/products#create new_admin_product GET /saoidfai/products/new(.:format) admin/products#new edit_admin_product GET /saoidfai/products/:id/edit(.:format) admin/products#edit admin_product GET /saoidfai/products/:id(.:format) admin/products#show PATCH /saoidfai/products/:id(.:format) admin/products#update PUT /saoidfai/products/:id(.:format) admin/products#update DELETE /saoidfai/products/:id(.:format) admin/products#destroy ``` ## RESTful 設計 根據`CRUD`有不同的操作方式,例如: ```bash # 取得所有會員列表 /users # 取得 user id 為 123 的資料 /users/123 # 取得 user id 為 123 的貼文 /users/123/posts # 編輯 user id 為 123 的資料 /users/123/edit ``` ## Controller 定義邏輯的地方,當使用者進入到某頁面例如:`/hello_world`,這時會去`controller`尋找對應的控制器找尋對應方法,像是到`PagesController`中找尋`hello方法`,接著`Rails`會自動判斷到`app/views`資料夾中找`controller`名字目錄裡同名的檔案,因`controller`取名為`PagesController`所以會到`app/views/pages`資料夾中找`controller`裡和方法相同的檔案。 流程會是:`app/controller/pages_controller.rb --> app/views/pages/hello.html.erb` ```rb # config/routes.rb get "/hello_world", to: "pages#hello" # resources 可自動產生 8 條不同的路徑,並對應到 controller 的 7 個方法 resources :posts resources :users # app/controllers/pages_controller.rb class PagesController < ApplicationController def hello end end # app/views/pages/hello.html.erb <h1>hello world</h1> <h2>這是個 HTML 檔案!!!!!</h2> ``` ## params ```rb # app/controllers/pages_controller.rb class PagesController < ApplicationController def hello render json: params end end ``` 當使用者輸入:`http://127.0.0.1:3000/hello_world?name=Jeter&age=20`。 會得到以下像是 hash 的回傳內容。 ```bash {"name":"Jeter","age":"20","controller":"pages","action":"hello"} ``` 如果只想取得 name 參數可以這樣操作 ```rb class PagesController < ApplicationController def hello render plain: params["name"] end end ``` 不管是`get`or`post`方法傳遞的參數,都會被收集到`params`中。 ## 練習:BMI 計算器 `form_tag`會被轉換成`<form> tag`、 `text_field_tag`會被轉換成`<input type="text"/>`、 `submit_tag`會被轉換成`<input type="submit"/>` 以上這些方法都統稱為`View Helper`。 ```rb # config/routes.rb Rails.application.routes.draw do get "bmi", to: "bmi#index" post "bmi/result", to: "bmi#result" end # app/controllers/bmi_controller.rb class BmiController < ApplicationController def index end def result height = params[:body_height].to_f / 100 weight = params[:body_weight].to_f @bmi = (weight / (height * height)).round(2) if @bmi >= 27 @bmi_info = "肥胖。" elsif @bmi >= 24 && @bmi <27 @bmi_info = "過重。" elsif @bmi >= 18.5 && @bmi < 24 @bmi_info = "健康。" else @bmi_info = "過輕。" end end end # app/views/bmi/index.html.erb <h1>BMI 計算機</h1> <%= form_tag '/bmi/result' do %> 身高:<%= text_field_tag 'body_height' %> 公分<br /> 體重:<%= text_field_tag 'body_weight' %> 公斤<br /> <%= submit_tag "開始計算" %> <% end %> # app/views/bmi/result.html.erb <h1>您的 BMI 為:<%= @bmi %></h1> ``` ## form_for 和 form_tag 通常在`form_for`後面 block 裡會放`f`變數,是一種 FormBuilder物件,可以透過這個物件上的`text_field`、`text_area`、`submit`方法做出對應的`<input> tag`。 form_for: - 可接一個 Model 物件參數 - 根據 Model 屬性自動生成輸入欄位 - 表單自動加入 Model 實例的值 form_tag: - 不一定需要綁定到 Model,常用於通用表單 - 需要手動指定動作 URL 在`Controller`的`new`方法設定的實體變數`@candidate`,就是要給剛剛前面`form_for`用的。 `form_for`除了可以產生`<form> tag`之外,它的`action`,也就是當你按下送出按鈕要去的那個地方,會根據傳給它的這個物件是新的還是舊的而自己判斷。 在這裡因為是剛剛才做出來的,`form_for`會認為你現在是要做「新增」這件事。 ## 資料關聯 - 一對一 `optional: true`表示允許 user 為填寫,如果沒有這樣做會產生 nil。 1. has_one 跟 belongs_to 方法需要同時設定嗎? 不一定,看實際需求而定。以 User 跟 Store 的例子來看,如果不需要「從 Store 反查 User」的功能,那 belongs_to 就不需要設定了。 ```rb # app/models/user.rb class User < ApplicationRecord has_one :store end # app/models/store.rb class Store < ApplicationRecord belongs_to :user, optional: true end ``` - 一對多 ```rb # app/models/store.rb class Store < ApplicationRecord belongs_to :user has_many :products end # app/models/product.rb class Product < ApplicationRecord belongs_to :store end ``` - 多對多 沒辦法在兩邊的 model 定義`has_many`or`belongs_to`來做關聯,通常需要第三方資料表儲存兩邊的資訊。 1. `references`會自動加上索引(index),加速查詢速度。 2. 自動幫 model 加上 belongs_to ```bash $ rails g model WareHouse store:references product:references ``` ```rb # app/models/ware_house.rb class WareHouse < ApplicationRecord belongs_to :store belongs_to :product end # app/models/store.rb class Store < ApplicationRecord belongs_to :user has_many :ware_houses has_many :products, through: :ware_houses end # app/models/product.rb class Product < ApplicationRecord has_many :ware_houses has_many :stores, through: :ware_houses end ``` 多對多關聯除了透過`has_many...through`還可以透過`has_and_belongs_to_many`,這樣就不需透過第三方 model,但還是得有第三方資料表。 ```rb # app/models/product.rb class Product < ApplicationRecord has_and_belongs_to_many :stores end # app/models/store.rb class Store < ApplicationRecord has_and_belongs_to_many :products end ``` ## 備註 `increment`方法是`Rails`中`Active Record`提供的一個方法,用於增加資料庫記錄中特定欄位的值並儲存到資料庫。 - increment 只在記憶體中增加值,並不會儲存進資料庫。 - increment! 增加值並儲存進資料庫。 ```rb @candidate.increment!(:votes) ``` ## before_action 方法 在 controller 中有個 before_action 方法,指的是讓某個 action 在執行之前先做某件事。 ```rb class CandidatesController < ApplicationController before_action :find_candidate, only: [:edit, :update, :destroy, :vote, :show] def vote if @candidatemodel 寫入到 vote_logs 資料表 @candidate.vote_logs.create(ip_address: request.remote_ip) end redirect_to candidates_path, notice: "完成投票。" end def index @candidates = Candidate.order(id: :asc) end def new @candidate = Candidate.new end def create @candidate = Candidate.new(candidate_params) if @candidate.save redirect_to candidates_path, notice: "新增候選人成功。" else render :new end end def show end def edit end def update if @candidate.update(candidate_params) redirect_to candidate_path, notice: "資料更新成功。" else render :edit end end def destroy if @candidate @candidate.destroy end redirect_to candidate_path, notice: "候選人資料已刪除。" end # strong parameters,可對 params 內容資料做過濾,permit 方法表示只允許這些參數通過,其他的無視,算是開一個白名單概念。 private def candidate_params params.require(:candidate).permit(:name, :age, :party, :politics) end def find_candidate # 透過 params 取得的所有資料型態都會是字串,如果要直接拿來做數學運算,需要使用 to_i 或 to_f 方法轉換型態 @candidate = Candidate.find_by(id: params[:id]) end end ``` ## yield 當 controller 在處理 view 時會先透過`app/views/layouts/application.html.erb`然後將`.hmtl.erb`的檔案填入`yield`中,這樣做主要是不需寫一堆重複的`HTML`。 1.`csrf_meta_tags`方法會在頁面上產生`<csrf-param>`跟`<csrf-token>`兩個`<meta>`標籤,用途主要是確保網站較不容易受到 CSRF(Cross-site request forgery)攻擊。 2. `stylesheet_link_tag`方法會轉換成 CSS 的`<link rel="stylesheet">`標籤。 3.`javascript_include_tag`方法會轉換成`JavaScript`的`<script>`標籤。 在`app/views/layouts/application.html.erb`定義好`yield`空格位置後,可在需要的模板使用`provide`填空。 ```rb # app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title><%= yield :my_title %></title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> </head> <body> <%= notice %> <%= yield %> </body> </html> # app/views/candidates/index.html.erb <% provide :my_title, "你好,我是Title" %> <h1>候選人列表</h1> # app/views/candidates/new.html.erb <% provide :my_title, "新增候選人" %> <h1>新增候選人</h1> <%= render "form", candidate: @candidate %> <br /> <%= link_to "回候選人列表", candidates_path %> ``` ## View Helper `link_to`、`form_for`都算是一種 view helper ## attr_accessor 定義`getter`和`setter`方法,建立`可讀取`、`可設定`的屬性。 ## validates 驗證機制,檢查資料是否符合規定。 ## Model Model 跟資料表通常是一對一沒錯,但並不是絕對的,有時候 Model 跟資料表一點關係都沒有。 ### 慣例 Model 在命名上必需是大寫開頭的單數,例如:User。 資料表則是小寫或下底線的複數命名,例如:user_projects ## CRUD ### C 透過 model 新增一筆資料有`new`和`create`兩種方法, 使用`new`的話後續還要接上`save`才會把資料儲存進資料表, 如果使用`create`方法則不需要接上`save`。 ### R 讀取資料, `first & last` --> 取得第一筆或是最後一筆資料。 `find & find_by` --> 找到指定 id 的人。 差別是`find_by`方法如果找不到指定 id 的資料,僅會回傳 nil 物件,但`find`方法會直接產生`ActiveRecord::RecordNotFound`的例外訊息。 ```rb irb(main):001:0> Candidate.find_by(id: 9487) Candidate Load (1.3ms) SELECT "candidates".* FROM "candidates" WHERE "candidates"."id" = $1 LIMIT $2 [["id", 9487], ["LIMIT", 1]] => nil ``` ```rb Candidate.first # 取得第一筆資料 Candidate.first(5) # 取得前三筆資料 Candidate.order(age: :desc).first # 取出年紀最大的候選人 Candidate.order(age: :desc).limit(3) # 限制取得筆數 Candidate.order(:age) # 小排到大 Candidate.find(1) Candidate.find_by(id: 1) Candidate.all # 取得所有資料 Candidate.where("age > 18") # 條件篩選 ``` ### U 更新資料常用的有`save`、`update`、`update_attribute`、`update_attributes`方法。 ```rb # 先找出要更新的人 user = Candidate.find_by(id: 1) # 使用 save candidate.name = "Jeter" candidate.save # 使用 update_attribute 方法更新單一欄位的值(注意:方法名字是單數) Candidate.update_attribute(:name, "Jeter") # 使用 update 更新資料,可一次更新多個欄位,且不需要再呼叫 save 方法 candidate.update(name: "Jeter", age: 18) # 使用 update_attributes 方法 candidate.update_attributes(name: "Jeter", age: 18) ``` ### D 刪除可以使用`delete`或`destroy` ```rb # 先找出要操作的資料 user = Candidate.find_by(id: 1) # 再把資料刪除 user.destroy # destroy 會有完整回呼(callback) user.delete # 執行 SQL 的 DELETE FROM ``` ## Migration 檔名符合某些字樣時,例如:add...to... 或是 remove...from ...,後面再加一些欄位就可以自動幫你產生一個寫好的 Migration 檔案。 ```rb $ rails g migration add_candidate_id_to_articles candidate_id:integer:index ``` ```rb class AddCandidateIdToArticles < ActiveRecord::Migration[5.0] def change add_column :articles, :candidate_id, :integer add_index :articles, :candidate_id end end ``` ## schema 這檔案內容是再執行`rails db:migrate`時自動產生的。 ## seeds 要在建立資料表時順便產生初始資料時可以在`db/seeds.rb`檔案中做。 ```rb $ rails db:seed ``` ## 資料驗證 - 在 Model 加上驗證 希望每篇文章的標題都是必填的。 ```rb # app/models/article.rb class Article < ApplicationRecord validates :title, presence: true end ``` 只有以下方式會觸發驗證: - create - create! - save - save! - update - update! ## 背景工作及排程 產生一個任務 ```bash rails g job user_confirm_email ``` ## 寫測試 - 安裝測試工具 ```bash $ gem install rspec ``` - 在任意地方建立資料夾並建立檔案 在根目錄建立`TDD-test`資料夾,裡面有個`bank_account_spec.rb`檔案 ```bash TDD-test/bank_account_spec.rb ``` - 得知測試結果 ```bash $ rspec bank_account_spec.rb ``` - 測試結果 分為三種: -- . 表示成功 -- F 表示失敗 -- * 表示待辦狀態pending