---
title: Astro課程 0723-0731 - Rails (Day1-Day4)
tags: astro, rails
---
# 一起來做BBS看板!
## Rails專案實作快速連結
[0723 Day1 建立看板](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#0724-%E7%AC%AC%E4%BA%8C%E5%A4%A9)
[0724 Day2 第一個Model: 新增看板](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#0724-%E7%AC%AC%E4%BA%8C%E5%A4%A9)
[0730 Day3 第二個Model: 新增文章](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#%E7%AC%AC%E4%B8%89%E5%A4%A9)
[0731 Day4 資料驗證、第三個Model: 新增使用者](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#0731-%E7%AC%AC%E5%9B%9B%E5%A4%A9)
## 提問時間
<200>
Q:網頁200 202 300 404......等是什麼意思?
A:[HTTP狀態碼](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status)
[Ref:傳紙條的故事](https://lidemy.com/courses/net101-js/lectures/9655079)
< Type >
Q:Type是什麼意思?
A: ex:`'Content-Type' => 'text/html'`告訴瀏覽器將這個網頁以html來看待
# 專案主題發想
V PTT forum / comment / message / mail
- 專案管理系統 Jira / Trello
- 記帳
- Forum / Reddit
- 購物網站
- Email https://hey.com/how-it-works/
- Flowdock / Slack / Line / Tinder (ActionCable - WebSocket)
- Podcast 平台
- POS系統 (iPad)
- https://www.ichefpos.com/zh-tw/delivery-integration
- https://sharelike.asia/
- TimeTree RWD
- https://timetreeapp.com/
- GitHub
目前比較不適合的專案
- Netflix 可以做的部分:前端、切版
- HackMD md -> html,即時互動
- Blog Medium
- 口罩地圖 / 等公車 / 前端
- 購物網站
- Game RPG Maker / Unity (C++)
- Facebook
# PTT功能
- 公佈欄
- 我的最愛
- 分類
- 信件
- 聊天
- 個人設定 UserInfo
## 新增首頁
1. 改routes
```
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root "pages#index"
get '/about', to: "pages#about"
end
```
2. rails g controller
```
pages_controller.rb # 檔名是蛇式
class PagesController # 類別名稱是駝峰式
```
3. 在pages下新增view檔案
# Board看板
Board的資料欄位
```
- title:string
- intro:text
- deleted_at:datetime, nil/null (知道何時被刪除)
-> 加index增加搜尋速度, 但會減慢寫入速度
(- is_deleted:boolean 是否被刪除, 預設false, 可以用deleted_at取代)
- state: [normal, disable, hidden]
```
SQL: 列出所有(沒被刪除)的看板
```
select *
from boards
where deleted_at is not null
```
## 建立model
```
rails g model Board title:string intro:text deleted_at:datetime:index state:string
```
以上指令會建出 `app/models/board.rb`
```
board.rb # 檔名是蛇式
class Board # 類別名稱是駝峰式
```
在migrate的create_boards.rb
把state設為normal
```
t.string :state, default: 'normal'
```
`rails db:migrate`
```
== 20200723061253 CreateBoards:
migrating =====================================
-- create_table(:boards)
-> 0.0012s
-- add_index(:boards, :deleted_at)
-> 0.0008s
== 20200723061253 CreateBoards: migrated (0.0021s) ============================
```
`rails db`指令可以進去sqlite查詢資料表
```
rails db
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite> select * from schema_migrations;
20200723061253
```
Q. 如果執行`migration`後要再修改欄位:
- 多人協作
`rails g migration [name]`--> rename_column
重新執行`rails db:migration`
- 一個人做
`rails db:rollback`
Q. 沒有存檔,欄位還沒更新就執行`rails db:migrate`,怎麼抓錯誤?
-> 查看`schema.rb`,看看欄位有沒有被更新過
## console模式新增資料
進`rails c`查看目前建立的Board
```
/PPT master rails c
Running via Spring preloader in process 1560
Loading development environment (Rails 5.2.4.3)
2.5.2 :001 > Board.all
Board Load (2.6ms) SELECT "boards".* FROM "boards" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
```
進入`--sandbox`模式假裝創建資料
```
/PPT master rails c --sandbox
Running via Spring preloader in process 1614
Loading development environment in sandbox (Rails 5.2.4.3)
Any modifications you make will be rolled back on exit
```
```
2.5.2 :001 > Board.create(title: 'Ruby')
(0.1ms) SAVEPOINT active_record_1
Board Create (1.6ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Ruby"], ["created_at", "2020-07-23 06:30:02.969252"], ["updated_at", "2020-07-23 06:30:02.969252"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<Board id: 1, title: "Ruby", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 06:30:02", updated_at: "2020-07-23 06:30:02">
```
## 追查類別的方法定義在哪一行 `.source_location`
```
2.5.2 :002 > Board
=> Board(id: integer, title: string, intro: text, deleted_at: datetime, state: string, created_at: datetime, updated_at: datetime)
2.5.2 :003 > Board.method(:all)
=> #<Method: Board(id: integer, title: string, intro: text, deleted_at: datetime, state: string, created_at: datetime, updated_at: datetime).all>
2.5.2 :004 > Board.method(:all).source_location
=> ["/Users/tingtinghsu/.rvm/gems/ruby-2.5.2/gems/activerecord-5.2.4.3/lib/active_record/scoping/named.rb", 26]
2.5.2 :005 > 3.days.method(:ago).source_location
=> ["/Users/tingtinghsu/.rvm/gems/ruby-2.5.2/gems/activesupport-5.2.4.3/lib/active_support/duration.rb", 366]
```
# 列出boards看板列表
## RESTful Routes
[wiki](Representational State Transfer)
用同個標準命名路徑
視路徑為`resources`
Eg. 看板可能的路徑path:
```
/boards
/boards/2
/boards/2/edit
```
1. rails g controller boards
2. 在controller新增methods
```
class BoardsController < ApplicationController
def index
@boards = Board.all #用實體變數裝起來傳給view
end
end
```
3. 在view印出所有看板
```
<h1>看板</h1>
<%= @boards %>
```
```
看板
#<Board::ActiveRecord_Relation:0x00007fc91ded30f8>
```
4. 用console c新增資料
```
2.5.2 :005 > Board.create(title: 'Ruby')
(0.1ms) begin transaction
Board Create (1.7ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Ruby"], ["created_at", "2020-07-23 07:45:47.861782"], ["updated_at", "2020-07-23 07:45:47.861782"]]
(1.3ms) commit transaction
=> #<Board id: 1, title: "Ruby", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:45:47", updated_at: "2020-07-23 07:45:47">
2.5.2 :006 > Board.create(title: 'PHP')
(0.4ms) begin transaction
Board Create (1.5ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "PHP"], ["created_at", "2020-07-23 07:46:35.574927"], ["updated_at", "2020-07-23 07:46:35.574927"]]
(1.4ms) commit transaction
=> #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35">
```
5. 在view用`.each`印出所有的檔案
```
<h1>看板</h1>
<ul>
<% @boards.each do |b| %>
<li><%= b.title %></li>
<% end %>
</ul>
```
網頁顯示
```
看板
Ruby
PHP
```
# 新增看板
## 建立連結
在view建立連結
```
<a href="/boards/new">新增看板</a>
```
改成`name_path`
1. 確保開發時路徑不會打錯
2. 路徑名稱更改時,連結自動對應
Q:主管說名稱從"boards"改為"cards"該怎麼做?
A:進到routes.rb檔執行以下
```
resources :boards, path: 'cards'
```
```
<a href="<%= new_board_path %>">新增看板</a>
```
rails的寫法`link_to`,
```
<%= link_to '新增看板', new_board_path if false %>
```
## 新增看板
new.html.erb,
`method`用`post`,不會像`get`把參數(如密碼)直接放在網址上
```
<h1>新增看板</h1>
<form action="/boards" method="post">
<label for="title">看板名稱</label>
<input type="text" name="title" id="title"><br>
<label for="intro">說明</label>
<textarea name="intro" id="intro"></textarea>
<input type="submit" value="送出">
</form>
````
在controller印出來傳進來的參數"create"看看
```
def create
render html: params
end
```
出現`InvalidAuthenticityToken`錯誤
```
ActionController::InvalidAuthenticityToken in BoardsController#create
```
如果沒有認證的Token,就沒有辦法寫入,資安上比較安全
params是從某一頁表單傳到另一列的參數
一個修正後的hash
`params["title"]`
`params[:title]`都可以拿到資料
先用爛方法在controller用render把答案印出來
```
def create
render html: params
end
```
頁面會顯示
http://localhost:3000/boards
```
{"authenticity_token"=>"KxKoyhFrzt59vnHbxTIRRtfX4eYzfRez5I7cY78DRN86H1nTdh+7joUBogmEIUPBf8HrnSWbMKOjeq7FAWU80A==", "title"=>"123", "intro"=>"456", "controller"=>"boards", "action"=>"create"}
```
只抓出重要欄位並存入
```
def create
Board.create(title: params[:title], intro: params[:intro])
redirect_to "/"
end
```
# 如何讓erb顯示出html的emmet?
設定

# 0724 第二天
## form
可以長表單出來
有3種
1. `form_for`後面要接model
`form_for(model)`
```
<%= form_for(Board.new) do |f| %>
<% end %>
```
2. `form_tag` 不需要model
eg. 輸入身高體重、計算BMI(不需要用到資料庫)
3. form_for 二合一
- form_with(model:, url: ...)
- form_with(url: ...)
## `form_for`
`form_for(Board.new)`
會猜是要新增,決定路徑往哪裡去
(有的時候會猜錯,所以會在後面加上URL)
```
<%= form_for(ApplicationRecord.new) do |f| %>
<% end %>
```
=> 會出錯,抽象類別不能new出實體
## 利用`form_for`建立欄位
```
<%= form_for(Board.new) do |f| %>
<%= f.label(:title), "看板名稱" %>
<%= f.text_field(:title)%>
<% end %>
```
和昨天做的form有麼不同?
檢查網頁原始碼,原本的欄位
```
<label for="title">看板名稱</label>
<input type="text" id="title" name="title"><br />
<label for="intro">說明</label>
<textarea id="intro" name="intro"></textarea>
```
```
Parameters: {"authenticity_token"=>"35/KxPh1Xkg==", "title"=>"456", "intro"=>"789"}
```
利用`form_for`做出來
```
<label for="board_title">看板名稱</label>
<input type="text" name="board[title]" id="board_title" />
<label for="board_intro">看板說明</label>
<textarea name="board[intro]" id="board_intro">
```
`form_for`根據`model的名字`和`欄位的名字`會包成一包`hash`
```
Parameters: {"utf8"=>"✓", "authenticity_token"=>"YnNwIUkCkvm=",
"board"=>{"title"=>"123", "intro"=>"456"}, "commit"=>"送出"}
```
## 改寫: 實體變數應該要放在controller
controller
```
def new
@board = Board.new
end
```
view
```
<%= form_for(@board) do |f| %>
<%= f.label :title, "看板名稱" %>
<%= f.text_field :title %>
<%= f.label :intro, "看板說明" %>
<%= f.text_area :intro %>
<%= f.submit "送出" %>
<% end %>
```
## `create method`
```
def new
@board = Board.new
#要先new實體變數不然表單的引數會是nil
end
def create
# http沒有狀態,每次request都是全新的開始
# 兩次request 會產生兩個不一樣的實體
# 所以這裡的@board和new方法的@board是不同的
@board = Board.new(params[:board])
end
```
## params的作用?
params是一個方法,負責把傳過來的字串打包好成hash
```
#想像params方法定義在上層,作用看起來像這樣子:
def params
{:board => "aa"}
end
```
```
def create
@board = Board.new(params[:board])
if @board.save
# OK
else
# NG
end
# Board.create(title: params[:title], intro: params[:intro])
# redirect_to "/"
end
```
在index上用param的話,只能在console裡看到,頁面上不會顯示
```
def index
puts "---------------------"
p params
p params[:title]
puts "---------------------"
@boards = Board.all
@boards = Board.all
end
```
```
<ActionController::Parameters
{"controller"=>"boards", "action"=>"index"}
permitted: false>
nil
```
## 新增看板成功寫入時,回到首頁
```
def create
@board = Board.new(params[:board])
if @board.save
redirect_to boards_path
else
# NG
end
```
# strong parameters
strong parameters處理controller的 Forbidden Attributes Error問題
防止有心人士透過開發者工具塞參數進來
```
def create
# Strong Parameters
clean_params = params.require(:board).permit(:title, :intro)
@board = Board.new(clean_params)
if @board.save
redirect_to boards_path
else
# NG
end
end
```
改寫成private方法
```
private
# Strong Parameters
def board_params
params.require(:board).permit(:title, :intro)
end
```
```
def create
@board = Board.new(board_params)
end
```
## 思考controller
controller幾乎不能重新使用,但是model可以
每個controller的method只能做一件事情
```
def index
@boards = Board.all
render html: "hi"
redirect_to root
end
```
錯誤訊息: `DoubleRenderError`
```
AbstractController::DoubleRenderError in BoardsController#index
```
controller的method預設會去尋找同名的view檔案,但一個method只能執行一個動作
eg. 如果又加了render -> 畫面顯示404頁面
```
def index
@boards = Board.all
render file: "/public/404.html"
# redirect_to '/'
end
```
## `flash[:notice]`
controller: 新增成功時出現訊息
```
if @board.save
flash[:notice] = "新增成功!"
redirect_to boards_path
else
# NG
end
```
view
```
<h2><%= flash[:notice] %></h2>
or
<h2><%= notice %></h2>
```
或是放在公板`application.html.erb`裡
flash太常使用了,所以也可以直接在controller提示:
```
if @board.save
redirect_to boards_path, notice: "新增成功"
else
# NG
end
```
## 寫入失敗的條件
model的驗證條件
```
class Board < ApplicationRecord
validates :title, :intro, presence: true, length: {minimum: 2}
# 舊式寫法
# validates_presence_of :title
end
```
```
~/Documents/projects/PPT master ● rails c --sandbox
Running via Spring preloader in process 6455
Loading development environment in sandbox (Rails 5.2.4.3)
Any modifications you make will be rolled back on exit
2.5.2 :001 > b1 = Board.new
=> #<Board id: nil, title: nil, intro: nil, deleted_at: nil, state: "normal", created_at: nil, updated_at: nil>
2.5.2 :002 > b1.errors.any?
=> false
2.5.2 :003 > b1.save
(0.2ms) SAVEPOINT active_record_1
(0.2ms) ROLLBACK TO SAVEPOINT active_record_1
=> false
2.5.2 :004 > b1.errors.any?
=> true
2.5.2 :006 > b1.errors.full_messages
=> ["Title can't be blank", "Title is too short (minimum is 2 characters)", "Intro can't be blank", "Intro is too short (minimum is 2 characters)"]
```
欄位出錯的時候,`form_for`會在錯誤欄位包一層東西,
如何用css呈現錯誤的欄位?
[SASS](https://sass-lang.com/guide)
修改css
```
form {
label {
display: block;
}
.field_with_errors {
input,textarea {
border: 2px solid red;
}
}
}
```
## 把錯誤的原因印出來
在view跑迴圈
重要:不要在view裡面new出實體!
```
<% if @board.errors.any? %>
<ul>
<% @board.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
```
# 建立各個看板的頁面
- index頁面跑迴圈加上超連結
```
<ul>
<% @boards.each do |board| %>
<li><%= link_to board.title, board_path(board) %></li>
<% end %>
</ul>
```
- 建立show.html.erb
- controller加上方法
試著把抓的param在show method印出來看看
```
def show
puts "-"* 50
p params
puts "-"* 50
end
```
點入任一個版面的連結
server會抓到參數
```
Started GET "/boards/1" for 127.0.0.1 at 2020-07-24 15:23:20 +0800
Processing by BoardsController#show as HTML
Parameters: {"id"=>"1"}
--------------------------------------------------
<ActionController::Parameters {"controller"=>"boards", "action"=>"show", "id"=>"1"} permitted: false>
--------------------------------------------------
```
透過`param[:id]`可以抓到數字
## 透過參數撈資料的方法
```
def show
Board.where(id: params[:id]) => [1, 1]
Board.find_by(id: params[:id]) # => 1
Board.find(params[:id]) # 同id有多筆時會出錯
# 以上3種都會翻譯成SQL
# select * from boards where id = ?
```
## `find` 和 `find_by`的差別
1.
`find` 只能找`id`
`find_by`可以接hash
`Board.find_by(id: param[:id], intro: "aaa")`
2. 找不到時
`.find_by` 回傳`nil`
```
2.5.2 :008 > Board.find_by(id: 1234)
Board Load (0.5ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 1234], ["LIMIT", 1]]
=> nil
```
`.find`直接噴錯誤訊息
```
2.5.2 :011 > Board.find(1234)
Board Load (0.3ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 1234], ["LIMIT", 1]]
Traceback (most recent call last):
1: from (irb):11
ActiveRecord::RecordNotFound (Couldn't find Board with 'id'=1234)
```
直接寫rescue
```
def show
begin
@board = Board.find(params[:id])
rescue
render file: '/public/404.html', status: 404
end
end
```
讓controller method裡面更乾淨的方法:
所有controller只要找不到就回傳404
```
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def not_found
render file: '/public/404.html', status: 404
end
end
```
讓Font-end Controller 和 Back-end Controller分開
# Update
view上建立新增連結
```
<% @boards.each do |board| %>
<li>
<%= link_to board.title, board_path(board) %>
<%= link_to '編輯', edit_board_path(board) %>
</li>
<% end %>
```
controller跟新增方法的流程類似
```
def update
@board = Board.find(params[:id])
if @board.update(board_params)
redirect_to boards_path, notice: "更新成功"
else
render :edit
end
end
```
# Delete
delete是特殊的動詞,使用時必須標注 `method: 'delete' `(預設`method: 'get'`)
重要:加上`data: {confirm: '確認刪除?'}`確認刪除
```
<% @boards.each do |board| %>
<li>
<%= link_to board.title, board_path(board) %>
<%= link_to '編輯', edit_board_path(board) %>
<%= link_to '刪除', board_path(board), method: 'delete',
data: {confirm: '確認刪除?'} %>
</li>
<% end %>
```
controller新增destroy方法
```
def destroy
@board = Board.find(params[:id])
@board.destroy
redirect_to boards_path, notice: "刪除成功"
end
```
## 調整成假的刪除
改成`update`
```
def destroy
@board = Board.find(params[:id])
#@board.destroy
@board.update(deleted_at: Time.now)
redirect_to boards_path, notice: "刪除成功"
end
```
列出沒有被刪掉的看板
```
def index
@boards = Board.where(deleted_at: nil)
end
```
## 自己寫一個destroy把它蓋掉
board.rb
```
class Board < ApplicationRecord
validates :title, :intro, presence: true, length: {minimum: 2}
# validates_presence_of :title
def destroy
update(deleted_at: Time.now)
end
end
```
# 整理重複的程式碼
1. 在某幾個action做事情
```
before_action :find_board, only: [:show, :edit. :updat, :destroy]
```
```
private
def find_board
@board = Board.find(params[:id])
end
```
2. `_form.html.erb` 引入渲染的檔案
渲染的檔案盡量不要有區域變數
讓其他的地方帶實體變數進來,重複使用的好處
`_form.html.erb`
```
<% if board.errors.any? %>
<ul>
<% board.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<%= form_for(board) do |f| %>
<%= f.label :title, "看板名稱" %>
<%= f.text_field :title %>
<%= f.label :intro, "看板說明" %>
<%= f.text_area :intro %>
<%= f.submit "送出" %>
<% end %>
```
new, show
```
<%= render 'form', board: @board %>
```
# 第三天
讓所有搜尋結果加上條件而不是只有`Board.all`
Eg. 找product價錢大於50元
```
Product.where(price > 50)
=> select * from products where price > 50
```
可以寫成 Product.cheap 或 Product.expensive
賦予這段程式碼意義
```
class Product
def self.expensive
where("price > 100")
end
end
```
## 什麼時候用類別方法? 什麼時候用實體方法?
```
Product.expensive => 使用類別方法
p = Product.new
p.save => 使用實體方法
```
## 類別方法
找出存在的看板
```
def self.avaliable
where(deleted_at: nil)
end
```
## scope的寫法
一行程式碼可以解決的話就用`scope`
回呼函數 `callback function`
前面不要忘記加逗號`,`
```
scope :available, -> { where(deleted_at: nil) }
```
## 預設`default_scope`
作為預設的過濾條件
```
default_scope { where(deleted_at: nil) }
```
抵銷預設scope
[Rails API] (https://github.com/rubysherpas/paranoia)
`Board.unscoped`
# 使用`paranoia`套件
實作假刪除功能
## 用法
[Rubygem](https://rubygems.org/gems/paranoia) 複製最新版的套件
```
gem 'paranoia', '~> 2.4', '>= 2.4.2'
```
路徑:Gemfile/# Reduces boot times through caching; required in config/boot.rb
## 安裝套件的兩種方式
1. Gemfile <- gem_name `$ bundle(install)`
第一個方法會去更新`Gemfile.lock檔`,所以速度比較慢
2. `$ install gem_name`
# 新的model: Post 文章
- title:string 必填
- content: text
- board_id: integer 必填
- board:references
- board:belongs_to
- 1.作用:產生board_id
- 2. belongs_to board
- state: string [draft/published/hidden]
- deleted_at: datetime:nil:index
- ip: string 必填
- serial: string:unique
```
$ rails g model Post title content:text board:belongs_to deleted_at:datetime:index ip_address serial:string:unique
```
做錯了的話把`rails g` 改成`rails d`,就會刪掉原本建立的model
# Post model
建立多對多關聯
一個看板有很多文章
## has_many
board.rb : `has_many :posts`
```
class Board < ApplicationRecord
acts_as_paranoid
has_many :posts
validates :title, presence: true, length: { minimum: 2 }
end
```
進去rails c來看
```
2.5.2 :005 > b = Board.find(2)
Board Load (0.4ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35">
2.5.2 :007 > b.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
```
如果剛開始建立的欄位不是`board_id`,而是`b_id`,要自己手動加上`foreign_key`
```
has_many :posts, foreign_key: b_id
```
# 看板內建立新post
## belongs_to
```
class Post < ApplicationRecord
belongs_to :board, optional: true
end
```
如果沒有指定`optional: true`就一定要`board_id`才能存入
有很多種方法:
```
[找到後指定id]
px = Post.new(title: "aa")
px.board_id = 9
px.save
---
[跟上面的方法類似]
Post.create(title: "aa", board_id: 9)
---
[比較安全:先找到看板再新增文章然後存進去]
bb = Board.find(9)
px = Post.new(title: "aa")
px.board = bb
px.save
---
[從belongs_to]的源頭往下寫
bb = Board.find(9)
bb.posts.create(title: "aa")
```
方法一 從post指定`board_id`
```
2.5.2 :001 > p = Post.new
=> #<Post id: nil, title: nil, content: nil, board_id: nil, deleted_at: nil, ip_address: nil, serial: nil, created_at: nil, updated_at: nil>
2.5.2 :003 > p.board_id = 2
=> 2
2.5.2 :004 > p.save
(0.2ms) begin transaction
Board Load (0.6ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Post Create (2.9ms) INSERT INTO "posts" ("board_id", "created_at", "updated_at") VALUES (?, ?, ?) [["board_id", 2], ["created_at", "2020-07-30 05:58:46.270668"], ["updated_at", "2020-07-30 05:58:46.270668"]]
(1.7ms) commit transaction
=> true
```
方法二: 從board對應
因為belongs_to幫忙post產生了兩個方法
```
class Post < ApplicationRecord
belongs_to :board
# .board
# .board=[]
end
```
```
2.5.2 :006 > b = Board.second
Board Load (8.8ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL ORDER BY "boards"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=> #<Board id: 3, title: "Python", intro: "123", deleted_at: nil, state: "normal", created_at: "2020-07-23 09:11:39", updated_at: "2020-07-24 08:31:13">
2.5.2 :007 > p = Post.new
=> #<Post id: nil, title: nil, content: nil, board_id: nil, deleted_at: nil, ip_address: nil, serial: nil, created_at: nil, updated_at: nil>
2.5.2 :008 > p.board = b
=> #<Board id: 3, title: "Python", intro: "123", deleted_at: nil, state: "normal", created_at: "2020-07-23 09:11:39", updated_at: "2020-07-24 08:31:13">
2.5.2 :009 > p.save
(1.2ms) begin transaction
Post Create (1.9ms) INSERT INTO "posts" ("board_id", "created_at", "updated_at") VALUES (?, ?, ?) [["board_id", 3], ["created_at", "2020-07-30 06:05:30.718942"], ["updated_at", "2020-07-30 06:05:30.718942"]]
(1.2ms) commit transaction
=> true
2.5.2 :011 > b.posts.count
(0.5ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."board_id" = ? [["board_id", 3]]
=> 1
```
方法三: 直接從看板的角度建立一篇文章
因為`has_many`幫我們作出了`create`和`build`方法
```
b.posts.create(title: "aa")
```
```
2.5.2 :012 > b.posts.create(title: "從board的方式 create post")
(0.2ms) begin transaction
Post Create (1.2ms) INSERT INTO "posts" ("title", "board_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "從board的方式 create post"], ["board_id", 3], ["created_at", "2020-07-30 06:10:30.054054"], ["updated_at", "2020-07-30 06:10:30.054054"]]
(1.2ms) commit transaction
=> #<Post id: 3, title: "從board的方式 create post", content: nil, board_id: 3, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 06:10:30", updated_at: "2020-07-30 06:10:30">
```
## has_one和 has_many的差別?
兩種方法長得有點不一樣
```
Has Many
bb.posts.create ...
bb.posts.build / save
Has One
bb.create_post
bb.build_post
```
Eg.
```
class Board < ApplicationRecord
validates :title, :intro, presence: true, length: {minimum: 2}
acts_as_paranoid
has_many :posts
has_one :post
end
```
`has_one`的SQL語法只找一筆
```
2.5.2 :001 > b = Board.find 2
Board Load (0.5ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35">
2.5.2 :002 > b.posts
Post Load (2.0ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 1, title: nil, content: nil, board_id: 2, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 05:58:46", updated_at: "2020-07-30 05:58:46">]>
2.5.2 :003 > b.post
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 1]]
=> #<Post id: 1, title: nil, content: nil, board_id: 2, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 05:58:46", updated_at: "2020-07-30 05:58:46">
```
# 從頁面上新增文章
不好的網址設計:
```
Rails.application.routes.draw do
resources :boards do
resources :posts
end
end
```
進階的寫法:把文章包進看板裡,把不需要的功能抽掉
```
Rails.application.routes.draw do
resources :boards do
resources :posts, only: [:index, :new, :create]
end
resources :posts, except: [:index, :new, :create]
end
```
[官網](https://rails.ruby.tw/routing.html)
shallow (淺層嵌套)
```
Rails.application.routes.draw do
resources :boards do
resources :posts, shallow: true
end
```
## 練習
- post controller
- new頁面
- form_for
```
<%= form_with(model: @post) do |f| %>
<%= f.label :title, "文章名稱" %>
<%= f.text_field :title %>
<%= f.label :content, "文章內容" %>
<%= f.text_area :content %>
<%= f.submit "送出" %>
<% end %>
```
`form_with` 要塞model給它, 然後指定url(不然猜不到)
new 完之後送到create,路徑是`board_posts_path`
```
<%= form_with(model: @post, url: board_posts_path) do |form| %>
<%= form.submit %>
```
# form_for 和 form_with差異
[rails在建立表單的時候-form-for-跟-form-with-有什麼不同](https://medium.com/@anneju/rails%E5%9C%A8%E5%BB%BA%E7%AB%8B%E8%A1%A8%E5%96%AE%E7%9A%84%E6%99%82%E5%80%99-form-for-%E8%B7%9F-form-with-%E6%9C%89%E4%BB%80%E9%BA%BC%E4%B8%8D%E5%90%8C-ec45cebbbf92)
form_with是用Ajax方式送資料,所以畫面上不會有動作
[送資料的方式:turbolinks](https://www.writershelf.com/article/rails-turbolinks%E2%84%A2-5-%E6%B7%B1%E5%BA%A6%E7%A0%94%E7%A9%B6?locale=zh-TW)
寫法的差異
```
<%= form_with(model: @post, url: board_posts_path) do |form| %>
<%= form_for(@post, url: board_posts_path) do |form| %>
```
# 0731 第四天 資料驗證
補充昨天說明:rails scafford在form_with也是先設定 `local: true` (先逃避turbolinks的問題)
## 刪除看板會發生什麼事?
回想資料庫的資料的相依性(PK, FK)
[資料的association: dependent](https://guides.rubyonrails.org/association_basics.html#options-for-has-one-dependent)
Eg. 如果設定 `dependent: :destroy`
```
has_many :posts, dependent: :destroy
```
SQL會先幫我們砍看板,然後跟著砍掉文章
但是因為我們已經裝了`paranoid`假刪除功能
刪了等於沒刪 (`dependent: :destroy`這句無效)
```
class Board < ApplicationRecord
acts_as_paranoid
has_many :posts, dependent: :destroy
end
```
# 幫文章設計序號
## 問題:如何產生8位數亂碼?
```
def serial_generator(n)
# a-z / A-Z / 0-9 產生亂數
end
puts serial_generator(8)
```
## 思考
先把三組陣列蒐集起來
```
list = [*'A'..'Z'] + [*'a'..'z'] + [*'1'..'9']
```
## 解法
解法一. 把陣列利用`sample`取出八個數字,再`join`成字串
```
list.sample(8).join
```
解法二. 如果不知道`sample`這個method,想像洗牌的方式,
先`shuffle`洗牌再取出前8張
```
list.shuffle.first(8).join
```
## 問題:8位數亂碼扣掉容易混淆的字元?
```
list = [*'A'..'Z'] + [*'a'..'z'] + [*'1'..'9']
pool = list - ['i','I', 'l', 'L', '1', '0', 'o', 'O']
pool.shuffle.first(8).join
```
排列組合的機率,保證序號重複可能性很低!

## 把驗證序號加入Post Model
[Rails Guide - 2.11 uniqueness](https://guides.rubyonrails.org/active_record_validations.html#uniqueness)
```
class Post < ApplicationRecord
validates :title, presence: true, length: {minimum: 2}
validates :serial, presence: true, uniqueness: true
belongs_to :board
end
```
`uniqueness`也可以加上`scope`
官方手冊的例子
- 每一年都有一次聖誕節(名稱會重複) `scope: :year`
- 但是同一年的節日名稱不會重複 `validates :name, uniqueness`
```
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
```
## 撰寫Post Model 裡的method
注意:不要寫在controller裡
[Callback回呼流程](https://railsbook.tw/chapters/19-model-validation-and-callback.html)
rails model的存檔流程

先用鴕鳥方式處理舊文章:`allow_nil: true`
```
class Post < ApplicationRecord
validates :title, presence: true, length: {minimum: 2}
# 資料庫已經有很多文章是nil,非uniqueness
validates :serial, presence: true, uniqueness: true, allow_nil: true
belongs_to :board
before_create :create_serial #存檔前先建立序號
private
def create_serial
self.serial = serial_generator(10)
# attr_reader的方法,省略小括弧
# self.serial=(serial_generator(10))
end
def serial_generator(n)
[*'a'..'z', *'A'..'Z', *0..9].sample(n).join
end
end
```
## Rake
寫一個rake來更新還沒有序號的舊文章
結構
```
namespace :db do
desc "更新文章序號"
task :update_post_serial do
# puts "hi"
end
end
```
`Rake -T` 指令列出所有的task
```
~/Documents/projects/PPT master ● rake -T
rake about # List
rake db:structure:dump # Dumps the database structure to db/...
rake db:structure:load # Recreates the databases from the st...
rake db:update_post_serial # 更新文章序號
```
把剛剛寫的`亂數產生序號`方法塞進去
挑出序號為nil的文章,`update post`的序號
```
namespace :db do
desc "更新文章序號"
task :update_post_serial do
Post.where(serial: nil).each do |post|
post.update(serial: serial_generator(10))
end
end
private
def serial_generator(n)
[*'a'..'z', *'A'..'Z', *0..9].sample(n).join
end
end
```
## 執行Rake
`task :update_post_serial => :environment`
指定環境變數,不然會找不到`Post`
```
namespace :db do
desc "更新文章序號"
task :update_post_serial => :environment do
puts "-----------------"
puts " updating serial "
puts "-----------------"
Post.where(serial: nil).each do |post|
post.update(serial: serial_generator(10))
print "."
end
puts "done!"
end
private
def serial_generator(n)
[*'a'..'z', *'A'..'Z', *0..9].sample(n).join
end
end
```
印出提示字元,讓使用者知道已經做完
```
~/Documents/projects/PPT master ● rails db:update_post_serial
-----------------
updating serial
-----------------
......done!
```
注意:呼叫方法時要注意receiver是誰
(例如private method的話就不能明確指出receiver)
如果self沒寫出來(隱含的self),跟寫出來self的意義不一樣
```
task :update_post_serial => :environment do
Post.where(serial: nil).each do |post|
post.update(serial: serial_generator(10))
end
puts "done!"
end
```
## Ruby `Self` 和 JS `this`的差別
(待補充)
## Ruby查找檔案的過程
`Cat` -> `Animal` -> `Object` -> `BasicObject`
檔案查找到Basic還找不到的時候會再回來Cat問有沒有`method_missing`
如果有的話,印出來
找不到的話,呼叫`super`上層來做
```
module Kernal
def method_missing(...)
# 錯誤訊息印在這裡
end
end
```
這就是Ruby效能比較不好的原因
```
# 效能差
Board.find_by_id_title(1, "aaa")
# 直接用hash找,效能好
Board.find_by(id: 1, title: "aaa")
```
# User 使用者登入登出功能
```
model: User
- accout:string
- password:string
- email:string
- nickname:string
- gender:string
- state:string
- deleted_at:datetime:index
model: Profile
- 簽名檔
- 名片檔
```
Counter Cache: 用在上站次數。每次登入的時候先存入,增加效能
## 建Model之前,先了解資料庫的正規化
讓每個資料表只存該存的資料就好
### 第一正規化 (1NF)
每個欄位應該只有一筆資料 => 拆開來
刪除資料中的重複群組
至少要做到第一正規化,不然就失去了使用資料庫的意義
### 第二正規化 (2NF)
1. 需要滿足 1NF
2. 去除「部份相依性(Partial Dependency)」
Eg.三個model
- student model (一個學生有很多技能)
- skill model (每個技能都有很多學生使用)
- skillset model (把相依性拆出來)
### 第三正規化 (3NF)
1. 需要滿足 2NF
2. 移除「遞移相依」(Transitive Dependency) / 間接相依
- skills model 只留teacher id,不需要teacher的其他資訊
- 拆出 teacher model
## rails g model
```
rails g model User account:string:uniq password email:string:uniq nickname gender state deleted_at:datetime:index
```
然後rails db:migrate
## user model 加上驗證
密碼加密
md5 -> 不安全
SHA-1
SHA-256
[Digest::SHA1](https://ruby-doc.org/stdlib-2.5.1/libdoc/digest/rdoc/Digest.html)
rails c會載入常用的標準函式庫
require只會載入一次
irb
```
~/Documents/projects/PPT master ● irb
2.5.2 :001 > require 'digest'
=> true
2.5.2 :002 > require 'digest'
=> false
2.5.2 :003 > require 'digest'
=> false
```
## 撰寫加密method
```
# keyword argument 關鍵字引數
def self.login(options)
if options[:account] && options[:password]
find_by(account: options[:account], # 帳號: aa
password: add_salt(options[:password])) # 密碼: 已經加密為xyz
# else
# return false
end
end
private
def encrypt_password
# 註冊時加密
self.password = User.add_salt(self.password)
end
def self.add_salt(password)
Digest::SHA1.hexdigest("x#{password}y")
end
```
console c
```
2.5.2 :001 > User.login(account: "cc", password: "123")
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."account" = ? AND "users"."password" = ? LIMIT ? [["account", "cc"], ["password", "9eb8e9439adfea0f1615e2dc268117a2a32cb3bf"], ["LIMIT", 1]]
=> nil
```
# user route
單數和複數會長出不一樣的路徑
```
resources :users
/users/2/edit #=> 不應該給使用者看到id
resource :users
/user/edit #=> 比較好的寫法
```
```
resources :users, only: [:new, :create] do # 只有新增時看到自己序號, index user列表是給後台管理用的
member do
get :profile_member
end
collection do # 擴充網址時,collection沒有id
get :profile_collection
end
end
```
做出來路徑的差別
```
profile_member_user GET /users/:id/profile_member(.:format) users#profile_member
profile_collection_users GET /users/profile_collection(.:format) users#profile_collection
```
logout動詞用`delete`
```
resources :users, only: [:new, :create] do
collection do
get :profile
get :login
delete :logout
end
end
```
## 跳脫CRUD制式的網址設計
不想用原本的八條路徑,更進階的寫法
Eg.把原本new的路徑改成sign_up,英文比較符合一般使用者註冊時的流程
```
resources :users, only: [:create] do
collection do
get :sign_up
get :edit
patch :update
get :sign_in
post :login
delete :sign_out
end
end
```