# Ruby(10/14) 五倍紅寶石(第八屆共筆)(2021/8/17)
## n + 1
在迴圈裡面做查詢
用迴圈跑,每跑一次都要查一次
一開始用 each 也會查一次
- 如何解決:
- Rails
includes(:user)
- DB
in 語法
可以轉換成一筆查詢
## 建立model
- :belongs_to 資料類型
只會幫你加 belongs_to
has_many、has_one 要自己加
:index 資料類型
會在 Rails 內增加 add_index,並且指定 INT 型態
## Route:shallow nesting 淺的槽狀(專案必用)
路徑上的差別,看起來比較不囉唆
看3號 comment 不用從2號 note 裡面找
因為index new create 的路徑沒有 id
其他的有,才需要這樣分

- 原效果:
```rb
resources :notes do
resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]
```
- evernote 專案:
```rb
resources :notes do
# shallow nesting
resources :comments, shallow: true, except: [:new, :edit, :update]
end
```
- 以上寫法會自動變成以下效果:
```rb
resources :notes do
resources :comments, only: [:index, :create]
end
resources :comments, only: [:show, :destroy]
```
- 印所有 params 方法:
render html: params
## session
HTTP是一種無狀態的通訊協定,在這邊做任何事情後,跳到其他頁面再回來並不會保留資訊。
可以透過Params 或 Cookies 讓瀏覽器能夠在切換頁面時記住資訊,
Rails 提供了所謂的 Session 可以更方便的操作,**用來記住登入的狀態、記住使用者購物車的內容等等**。
發 session(cookie) = 一種認證機制
拿到 cookie 不等於登入
登入 = 拿到 session 而且可以做裡面的事
登入成功會發令牌(cookie)給你,瀏覽器會留一個 session 做比對,看是否正確
會有時間性,如果過期或是對方 server 重新啟動都要再重新登入
Session 是一個實體方法,用法跟 Hash 很像。
可以把 Session 當作是 Hash,把 user.id這個值存到屬性 thankyou9527,取名隨意。
```rb
session[:thankyou9527] = user.id
```
## scope
商業邏輯,以後如果要改需求改這邊就好
```rb
# 定義類別方法:假刪除,刪掉的檔案只是讓他看不見而已
# 用 where 過濾,deleted_at 預設 nil,沒點刪除的都是 nil
def self.available
where(deleted_at: nil)
end
scope :available
```
### default_scope
所有的搜尋都會加上後面的條件,
預設欄位值是deleted_at: nil的都看得到
可以用 unscope 把他去掉
```rb
# 一般的 scope 還是要在類別後面加這個方法名稱,default不用 => 不用寫 available
default_scope { where(deleted_at: nil) }
```
### paranoia
[paranoia](https://github.com/rubysherpas/paranoia)
放在 Note Model 裡面
直接用 acts_as_paranoid 內建套件取代 default_scope { where(deleted_at: nil) }
使用前先在 Gemfile 寫入 gem "paranoia", "~> 2.2"
接著輸入 bundle install,如果沒反應就重開 server
## AJAX(可查查看)
非同步JavaScript,演給使用者看
用 XHR 送資料
render js:"alert:123"
將字串用 js 格式執行它
格式只能是瀏覽器有支援的
controller沒用render會去找同名檔案
js.erb 只有在Rails適用
正常是不行的,不是業界標準
## 多對多關聯(專案必用)
用起來像多對多,但事實上是透過第三方(Join Table)約診簿串連兩者
- g model Bookmark belongs_to…
建立第三方 table
## JS
- insertAdjacentHTML
在子層最前面新增物件
### form helper
- form_for (model):建立model
- form_tag:建立一般表單
- form_with:都可以
兩個都可以當 有參數就用form_for,沒有就用form_tag
以前第三個參數預設 local:false = remote:true
rails 6.1 後預設 local:true,要改成 local:false 才能用 Ajax 傳送資料
- form_with(model: @user)
- form_with()
- fetch:
使用非同步,會去 Web API 排隊,預設用get去抓東西
fetch 回傳 promise
用 then 來做事,繼續往下做
用 json 函式解讀,會回傳 promise
可以再繼續用 then 做事
resp 是 fetch 的結果
data 是 then 的結果
回傳 username
```js
fetch("https://jsonplaceholder.typicode.com/users")
.then((resp) => resp.json()) // Promise
.then((data) => {
for (let { username } of data) {
console.log(username);
}
// data.forEach((d) => {
// console.log(d.username);
// });
});
```
### 解構 destructing (常見)
let {}
let [x,y,z] = list
### postman
取外部資訊(政府平台)
會遇到問題 => 搜尋關鍵字:CORS
## evernote 專案
### 留言功能
1. 新增 Comment model
rails g model Comment user:belongs_to content:text deleted_at:datetime:index note:belongs_to
2. 新增關聯
note.rb
```rb
has_many :comments
```
3. 建立 show畫面
show.html.erb
```rb
<div class="note-heading">
<h1>筆記: <%= @note.title %></h1>
<a href="#" id="favorite_btn">
<div class="favorite_icon"></div>
</a>
</div>
<%= @note.content %>
<%# remote 用非同步方式送資料,local: false = remote: true %>
<%= form_with(model: @comment, url: note_comments_path(@note), local: false) do |f| %>
<%= f.text_area :content %>
<%= f.submit "新增留言" %>
<% end %>
<ul class= "comments">
<% @comments.each do |comment| %>
<li><%= comment.content %></li>
<% end %>
</ul>
```
4. 運用 has_many 的方法創造物件
notes_controller.rb
```rb
def show
# comments 由來: note model has_many comments
@note = Note.find(params[:id])
@comment = @note.comments.new
@comments = @note.comments.order(id: :desc)
end
```
5. shallow nesting
routes.rb
```rb
resources :notes do
# shallow nesting
# 做出 /comments/3/edit 效果
resources :comments, shallow: true, except: [:new, :edit, :update]
end
```
6. 新增 comments_controller 內容
comments_controller.rb
```rb
before_action :check_login!
before_action :find_user_note
def create
@note.comments.new(comment_params)
# @note.user = current_user
if @note.save
@content = comment_params[:content]
else
redirect_to "/"
end
end
private
def comment_params
params.require(:comment)
.permit(:content)
# comment 的 model 有寫兩個 belongs_to 所以這邊也要兩個欄位才能正常送出
# 語法糖衣 原本:應該寫在前面,merge 的參數要是 hash
# 用 merge 把 user_id 加進去,讓他變兩個欄位
# merge 只會回傳我添加的東西(必須是 hash),不會改變我原始物件的內容
.merge(user_id: current_user.id)
end
# 把current user 改掉,讓每個人都可以看
def find_user_note
@note = Note.find(params[:note_id])
end
```
7. 不能編輯刪除別人文章
notes_controller.rb
```rb
def find_user_note
# 從個人角度去找文章,只有目前使用者能找
@note = current_user.notes.find(params[:id])
end
```
### 註冊、登入、登出功能
rails g model Bookmark user:belongs_to note:belongs_to
routes.rb
```rb
# 路徑: GET /users/sign_up 註冊表單
# 路徑: GET /users/sign_in 登入表單
# collection 追加路徑,方法 :方法名稱
resources :users, only: [:create] do
collection do
get :sign_up
get :sign_in
end
end
# post :sign_in 拉出去做
# 登入跟註冊是兩件事情,註冊是一般的 CRUD,但登入會用到 sessions,所以把他獨立拉出來寫
# 寫入 sessions = 登入,用 as 換 path 名字,看起來比較乾淨
# 刪除 sessions = 登出
post "/users/sign_in", to: "sessions#create", as: "login"
delete "/users", to: "sessions#destroy", as: "logout"
```
application_controller.rb
```rb
# view helper 跟 controller 都會用到 helper_method 裡面的方法
# 用 helper_method 把 controller 功能匯出到 view helper,寫一次就好
# 或是寫在 view helper 再外掛模組進來也可以
helper_method :user_signed_in?, :current_user
private
# 有 session
def user_signed_in?
session[:thankyou9527] != nil
end
# 如果有目前用戶的 session 就可以登入
def current_user
if user_signed_in?
User.find(session[:thankyou9527])
else
nil
end
end
# 沒有 session 就要重新登入
def check_login!
if not user_signed_in?
redirect_to "/users/sign_in"
end
end
```
sessions_controller.rb
```rb
# 把 user.id 存入 session id 來找到該位 user = 登入
def create
session[:thankyou9527] = user.id
end
# 把 user.id 變成 nil 讓他找不到 user = 登出
def destroy
session[:thankyou9527] = nil
end
```
- Digest(摘要)
將數據轉換成 Hash 值,讓他不易被解密
user.rb
```rb
# 使用 Digest 方法要先 require 進來
class User < ApplicationRecord
# 可以用 attr accessor 騙他,實際上沒有這個欄位(不建議)
# attr_accessor: :password_confirmation
# 不要在資料庫存明碼
# confirmation = 確認兩個欄位值是一樣的, uniqueness = 唯一值,
# 兩個都是驗證器
validates :email, presence: true, uniqueness: true
validates :password, presence: true, confirmation: true
validates :account, presence: true, uniqueness: true
# 各種 callback https://guides.rubyonrails.org/active_record_callbacks.html
# 放before_create 因爲怕會跟其他的 callback 重複,新增跟修改都會觸發
# before after 自己要看,知道他們在做啥
# 使用者被建立前先做加密
before_create :encrypt_password
# has_many 會動態增加四種方法,可隨便取名,但最後要幫他指定 model(source: :note) 不然會找錯
# notes 查詢
# noets= 設定
# build 需要使用.save才會寫進資料庫
# create 會直接寫進資料庫裡
# https://rails.ruby.tw/association_basics.html#has-many-%E9%97%9C%E8%81%AF
has_many :notes
has_many :bookmarks
has_many :favorite_notes,
through: :bookmarks,
source: :note
# 加密方法:
# MD5 已被破解
# SHA 比較安全
# 密碼只能被加密一次,兩次以上會看不懂
require "digest"
def encrypt_password
# Digest::MD5.hexdigest(pw)
# salting:在密碼裡面加東西,讓他比較複雜
salted_pw = "xyz#{self.password}827128#{self.password}82-12j23h"
self.password = Digest::SHA1.hexdigest(salted_pw)
end
end
```
show.html.erb
```rb
<%# remote 用非同步方式送資料,local: false = remote: true %>
<%= form_with(model: @comment, url: note_comments_path(@note), local: false) do |f| %>
<%= f.text_area :content %>
<%= f.submit "新增留言" %>
<% end %>
```
application.html.erb
```rb
<%# 如果要寫在別的地方要寫好路徑讓他找,建shared資料夾%>
<%# 用render 去抓另外一個公版到這邊呈現,看起來比較不那麼亂%>
<%= render "shared/navbar" %>
```
_navbar.html.erb
```rb
<%# 共用的通常會創一個 shared 資料夾來放 %>
<nav>
<% if user_signed_in? %>
<%# link_to 方法預設 GET,所以要改成 delete %>
<%= link_to "登出", logout_path, method: "delete" %> |
<%= current_user.email %>
<% else %>
<%#使用者登入跟沒登入會出現不同資訊%>
<%= link_to "登入", sign_in_users_path %> |
<%= link_to "註冊", sign_up_users_path %>
<% end %>
</nav>
```
create.js.erb
```rb
<%# 用 let 或 const 會造成重複宣告,沒辦法送出多筆,改用 var %>
<%# 拿到show.html.erb 的 .comments class,並清空 value%>
var comment_area = document.querySelector(".comments").value = ""
var content = "<li><%= @content</li>"
<%# 把內容加在 ul 的 li 裡面,塞在最前面%>
comment_area.insertAdjacentHTML('afterbegin', content)
<%# 或是不要宣告,直接寫在一起 %>
document.querySelector("#comment_content").value = ""
document.querySelector(".comments")
.insertAdjacentHTML('afterbegin', "<li><%= @content %></li>")
```
---
###### tags: `Ruby` `Rails`