# 0412 假刪除/ 使用者註冊/ 使用者登入
###### tags: `Ruby / Rails` `bug待處理`
## 軟刪除/假刪除資料 - Soft Delete
- 讓前端使用者看起來像資料已刪除,但是其實資料還存在資料庫中
- 新增欄位註記,在 View 顯示時用條件篩選掉資料
- ==不==使用 SQL 語法中的 DELETE 刪除資料庫中的資料。
- [Migration 修改筆記](https://hackmd.io/19RGr-SwTOaMUmlsWYNx0g#Rails%E8%A3%A1%E4%BF%AE%E6%94%B9Migration)
### 建立新的欄位
```shell=
$ rails g migration add_deleted_at
(建立叫做 'add_deleted_at' 的空檔案)
```
* 用終端機建立 Migration 的慣例:[官方文件說明](https://guides.rubyonrails.org/active_record_migrations.html#creating-a-migration)
### 編輯 Migration 建立欄位
```ruby=
deleted: boolean, default: false
# 同 add_column :restautants, :deleted, :boolean, default: false
# 同 add_column :restautants, :delete_flag, :boolean, default: false
# boolean 值標記
add_column :restaurants, :deleted_at, :datetime, default: nil
# 時間戳記標記欄位
# 在 controller 撈資料的時候使用下面語法,這樣就可以篩掉已刪除的東西
Restaurant.where(deleted: true)
# 同 Restaurant.where.not(deleted: false)
add_index :restaurants, :deleted_at
# 建立索引
# 降低資料增加後查詢速度的成長曲線,讀取速度比較快
# 缺點:效能變慢,寫入速度變慢,多佔記憶體空間
```
- 上方第六行用的 `:datetime`:[API - datetime](https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html)
- Migration 使用的方法:[官方文件說明](https://guides.rubyonrails.org/active_record_migrations.html#using-the-change-method)
- 加入索引列的好處:[文章說明](https://dev.to/khalyomede/reduce-time-complexity-by-indexing-your-arrays-3ci6)
- 索引頁
- 文件中如果有特定的內容或名詞是具有參考價值的,那麼通常會在文件的最後建立索引頁,以方便讀者查詢。
### 編輯 View 讓清單只顯示未刪除部分
```ruby=
def index
@restaurant = Restaurant.where(deleted_at: nil)
end
```
### 修改 Model 讓 controller 更乾淨
到 Model 裡直接修改 destroy 方法
```ruby=
class Restaurant < ApplicationRecord
def destroy
update(deleted_at: Time.now)
end
end
```
### scope
* 可視做簡化版類別方法
* 把一群條件整理成一個 scope ,賦予他意義,並簡化邏輯
* 減少在 Controller 裡寫一堆 Where 組合
* 預設回傳最後的結果,類別方法則可能回傳其他結果 ( 根據最後一行 )
* Scope 可連發
* 將商業邏輯包到 Model 裡面,簡化 Controller 要做的事情
* Scope 可以跟自己再組合出新的 Scope 方法
* Scope 會產生 self.方法名稱
```ruby=
scope :available, -> { where(deleted_at: nil, and permission: true) }
scope :cheaper_than, -> (x) { where("price < #{x}")}
scope :cheap, -> { available.cheaper_than(500) }
```
- 因為 block 本身不是物件,所以用 lambda, proc 將 block 物件化
- ==箭頭「 -> 」和逗號不能省略!!!==
### default_scope
- 做任何查詢都會插入這個 scope(只要能夠有where語法的修改)
- [官方文件說明](https://api.rubyonrails.org/v6.1.3/classes/ActiveRecord/Scoping/Default/ClassMethods.html#method-i-default_scope)
- 只會有一個,但是在設定時裡面可以放多個指令
```ruby=
default_scope {where(deleted_at: nil)} # 可單獨存在
default_scope {where(deleted_at: nil).order(id: :desc)} # 多個指令
# ------
scope :available, -> { where(deleted_at: nil) }
default_scope { available } # 或適當的做分類
```
### unscope
- 不想要預設值就使用 unscope
- 在指令後面加 `unscope(指令)` 就可以排除特定的預設指令
- 例: `Restaurant.all.unscope(:order)` 或 `Restaurant.all.upscope(:where)`
- [官方文件說明](https://guides.rubyonrails.org/active_record_querying.html#unscope)
- [unscope 手冊說明](https://apidock.com/rails/ActiveRecord/QueryMethods/unscope)
### 建立假刪除後又要真刪除
- 因為 destroy 已經被前面的 update 蓋掉了,所以要呼叫最近一層的同名方法,而最近一層的地方在於繼承ActiveRecord::Base
```ruby=
def destroy
super
end
```
super本身是nil,沒有加括號的時候,將所有收到的沒有加括號的時候,將所有收到的參數往上丟,並呼叫上一層的方法,加括號的話可以控制給的參數數量
- 使用 Ruby 的 super 指令呼叫上==一層==的指令
- [super 手冊說明](https://ruby-doc.org/docs/keywords/1.9/Object.html#method-i-super)
>[假刪除paranoia 套件](https://github.com/rubysherpas/paranoia)
## 使用者註冊介面
### 欄位設定
欄位|資料型態|說明
---|---|---
email|string|unique
password|string|
role| string|[user/admin/staff]
* uniq = unique 唯一值,比對索引值才會快,所以才會自動生成一個index,索引值是在硬體層直接對資料表做索引
### 建立 新的model - User
```shell=
$ rails g model User email:string:uniq password role
## 會建立一個叫 User 的 model,還有一個 migration
## email後面的string不能省略!!!
```
#### 產生的 migration 檔
```ruby=
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :email # 也可直接在這裡接 ",unique: true"
t.string :password
t.string :role
t.timestamps
end
add_index :users, :email, unique: true # 因為指令有下 uniq,框架會自動產出
end
end
# 總共會有 6 個欄位[users.email.password.role.timestamps*2(create_at.update_at)]
```
### 建立 route
用 member 有 id
用 collection 沒有 id
```ruby=
Rails.application.routes.draw do
resources :restaurants
resources :users, only: [:create] do
collection do
get :sign_up,controller: 'restaurants', action: 'new'
# 可以在後方指定他要去的controller跟action
end
end
```
### 建立 controller 、 action 跟 view
瀏覽器驗證
* 使用特定的 input 格式
* 常用:text_field, check_box, radio_button
* email_field、password_field...
* [官方文件說明 form helpers](https://guides.rubyonrails.org/form_helpers.html#other-helpers-of-interest)
```erb=
<h1>註冊帳號</h1>
<%= form_for(@user) do |form| %>
<div class="field">
<%= form.label :email %>
<%= form.email_field :email %>
</div>
<div class="field">
<%= form.label :password %>
<%= form.password_field :password %>
</div>
<div class="field">
<%= form.label :password_confirmation %>
<%= form.password_field :password_confirmation %>
</div>
<%= form.submit %>
<% end %>
```
因為沒有預設 password_confirmation 的欄位,所以在user model 裡建立 `attr_writer :password_confirmation` 讓他有個虛擬欄位可以放
此時如果要驗證是否相同要用
if .....
如果在 model 裡有用 `confirmation: true` 的話,框架會自動生成 attr_writer,所以可以拿掉上面的用法,框架也會直接驗證是否相同
### 資料驗證
[官方文件 關於驗證](https://guides.rubyonrails.org/active_record_validations.html)
ruby regular expression: [https://rubular.com/](https://rubular.com/)
如果用 confirmation 的話,欄位名字也要是xxx_confirmation
使用 format => 驗證是否遵循特定格式輸入
```ruby=
validates :email, presence: true,
uniqueness: true,
format: { with: /.+\@.+\..+/ } #正規表達式(regular expression)
validates :password, presence: true,
confirmation: true
```
[0408 驗證錯誤訊息筆記](https://hackmd.io/-fgcjJmnRPioD8T76JfafA?view#%E6%9F%A5%E7%9C%8B%E9%8C%AF%E8%AA%A4%E8%A8%8A%E6%81%AF)
### callback 或 lifecycle
```ruby=
before_save :aaa
before_create :bbb
```
[官方文件說明callback](https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks)
### 密碼加密
- 使用 ruby 內建方法:[用 ruby 執行 SHA1 加密](https://ruby-doc.org/stdlib-2.4.0/libdoc/digest/rdoc/Digest/SHA1.html)
- 在 model 中直接設定
```ruby=
before_create :encrypt_password
private
def encrypt_password
self.password = Digest::SHA1.hexdigest(self.password)
end
```
- 放在 before_create 因為只在建立時加密
如果放在 before_save 會在每次更動資料的時候都加密 => 造成已加密的密碼再次被加密
```ruby=
self.password = Digest::SHA1.hexdigest(self.password)
# 兩邊都可以用
self.password = Digest::SHA1.hexdigest(password)
self.password=(Digest::SHA1.hexdigest(password)) #上下相等
# 因為也是呼叫實體方法 password
password = Digest::SHA1.hexdigest(password)
# 不會出錯,但僅只是指定區域變數
```
- 在密碼裡"加鹽"
- 如果直接加密會很容易被猜到是用哪種加密法,進而直接解密
=> 用密碼學中"加鹽巴"的方法增加安全性
[WIKI 說明](https://zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6))
```ruby=
private
def encrypt_password
self.password = Digest::SHA1.hexdigest(salted_pwd)
end
def salted_pwd
"123#{self.password}xx" # => 加密前在使用者指定的密碼前後加入 123 跟 xx 表示在前後位置插入特定的字串
end
```
## 使用者登入介面
### 建立 route
(在這邊利用另一種方法示範)
```ruby=
resources :sessions, path:'users', only: [] do
# 加 path 改變路徑
collection do
get :sign_in, action: 'new' # 登入頁面
post :sign_in, action: 'create' # 登入動作
end
end
```
如果直接在 resources 設定 only: :create 跟 :destroy,會有兩個重複路徑,所以改成自己刻路徑
### 建立 sessions_controller
```ruby=
def new
@user = User.new
end
def create
# email = user_params[:email]
# password = user_params[:password]
if User.login(user_params)
# 發號碼牌
# 轉去首頁
redirect_to root_path
else
redirect_to sign_in_sessions_path
end
# 查有無帳號 / 密碼
# 轉址 / 重登
end
private
def user_params
params.require(:user).permit(:email, :password)
end
```
### 修改 User 的 Model ==待補充==
- 因為在 controller 裡面設定要 follow User Model,所以才去 User 的 Model
```ruby=
def self.login(params)
email = params[:email]
password = params[:password]
encrypted_password = Digest::SHA1.hexdigest("123#{password}xx")
find_by(email: email, password: encrypted_password)
end
```
- 簡化程式碼
- 把加密跟灑鹽兩個動作分開
- ==有錯誤喔喔喔,待龍哥講解==
```ruby=
def self.login(params)
email = params[:email]
password = params[:password]
salted_password = salted(password)
encrypted_password = encrypted(salted_password)
find_by(email: email, password: encrypted_password)
end
private
def encrypt_password
salted_pwd = salted(password)
self.password = encrypt(salted_pwd)
end
def encrypt(password)
Digest::SHA1.hexdigest(password)
end
def salted(password)
"123#{password}xx"
end
```
## 題外話
### 拿到新專案時
1. git clone 專案名 ( 將專案資料夾下載到本地端 )
2. cd 專案名 ( 移動到專案資料夾)
3. 確認 `README.md` ( 確認說明檔及有無需要先下載的套件 )
4. `$ bundle install` ( install gem,根據專案有無gemfile而選擇安裝 )
5. `$ yarn install` ( install node_modules )
6. `$ rails db:migrate` ( 沒做會噴錯誤訊息pending叫你去 `db:migrate` )
- 噴錯的話也可以去看 config 裡有沒有 .sample 檔案
8. `$ rails s` ( 開啟伺服器 )
> * bundle install:把想要使用的套件放在 Gemfile 裡,接者輸入指令 bundle install 即可使用
> * 只要在 schema 裡面有欄位,框架就會幫你產出 attr_writer 跟 attr_reader
---
### self 使用
```ruby=
class Cat
def Cat.aa
end
def self.aa # 等於 Cat.aa
end
def cc
p self # 等於實體本身 (下面例子裡是 kitty)
end
end
kitty = Cat.new
kitty.cc
```
[嘗試說明 self 用法的文章](https://www.rubyguides.com/2020/04/self-in-ruby/)
```ruby=
class Cat
def Cat.aa
end
def self.aa # 等於 Cat.aa
end
def xx
self.yy
yy
end
def yy
end
def cc
p self # 等於實體本身 (下面例子裡是 kitty)
end
end
kitty = Cat.new
kitty.cc
```
---
### session vs cookie
* 登入成功的當得到一個cookie, 存在瀏覽器裡, session 也會得到一個cookie
* cookie可以跟session( 後端 )比對資訊
> Both **Functional programming** and **object-oriented programming** uses a different method for storing and manipulating the data. In **functional programming**, data cannot be stored in objects and it can only be transformed by creating functions. In **object-oriented programming**, data is stored in objects.