---
# System prepended metadata

title: Astro課程 0723-0731 - Rails (Day1-Day4)
tags: [astro, ' rails']

---

---
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?
設定
![](https://i.imgur.com/8VVhibu.png)


# 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
```

排列組合的機率，保證序號重複可能性很低！
![](https://i.imgur.com/ITFUHXb.png)


## 把驗證序號加入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的存檔流程

![](https://i.imgur.com/Vj9hF1M.png)

先用鴕鳥方式處理舊文章：`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
```