Ruby on Rails - 2023/11/28
===
## 重點
- has_many vs belongs_to
- xxxxx_id
- 做出方法
- User model
- current_user, user_signed_in?
- 移到 controller + helper_method
- 登出 session.delete(:key)
- Comment model
- nested routes
- shallow route
## 關聯性
這個商品是誰的?
rails關聯性有三種:
- 1:1
- 一夫一妻制
- 1:N
| n | 1 | 補充 |
| -------- | -------- | -------- |
| Products | user | |
| user_id | id |關聯性|
- N:N
- 複雜的1:N
## 新增商品要登入
- 新增商品需要登入
- 每個controller都會遇到,再上一層,application.controller
```ruby!
# application_controller.rb
def authenticate_user!
#如果沒有登入就把你踢走
if not user_signed_in?
redirect_to sign_in_users_path. alert: '請先登入帳號'
end
end
```
### 在products_controller.rb,寫入方法
```ruby
#products_controller.rb
before_action :authenticate_user!, except: [:index, show]
```
- before_filter -> before_action 效果一樣
- 為什麼authenticate_user!要加驚嘆號呢?
- 有很多意思,大部分為提醒要看手冊
- 使用except,因為除了看商品外其他路經都需要登入
- 點擊會出現錯誤,因為helper為view helper,所以在controller不能使用
- 這時候可以用**module**的概念,如果想要飛,給他一對翅膀,不用當鳥的小孩(繼承 = 當鳥的小孩)
- 示範:
```ruby
modle Flyable
def fly
end
end
class Cat
include Flyable
# 利用include掛Flyable進來
end
class Dog
include Flyable
end
kitty = Cat.new
kitty.fly
```
- 所以可以在`application.controller`加入`include UsersHelper`
```ruby
# application.controller
include UsersHelper
```
- 此做法稱為合成(composion),你可以掛想要的模組進來
- Ruby一次只能繼承一個
### 方法二
把helper的`user_signed_in?`方法改放到`application.controller`,並加上`helper_method`,匯出
```ruby!
helper_method :current_user, :user_signed_in?
def current_user
User.find_by(id: session[:__user_ticket__])
end
def user_signed_in?
current_user.present?
end
```
### 新增user_id增加關聯性
- 增加一個migration
rails g migration add_user_to_product
- 內容加上`add_belongs_to :products, :user` or `add_reference(:products, :user, type:string)`,有加reference & add_index加索引(加快查詢速度)
- 這裡放user會自動幫忙加user_id
- 兩個效果一樣
- 也可以使用`add_colum`,但就只是加欄位
### urser.rb增加has_many 方法
```ruby
#urser.rb
has_many :products
```
- validates是方法, has_many也是方法,讓你可以搜尋到,加has_many其實沒有關聯,只是一個欄位而已
- 如果要有關聯,應該要=某個表格的意思,類似excel,A1 = B2,會連動
- User有user_id的話,Rails會到Product找user_id
- has_many :products意思是:
- 要去找:products model
- 原理是用方法定義方法,才會動(metaprogramming)
```ruby
[:a, :b, :c].each do |n|
define_method "eat#{n}" do
puts "eat xx"
end
end
eat_a
eat_b
eat_c
```
原理:
```ruby=
class Animal
def self.has_many(something)
defined_method "#{something}" do
puts "hi"
end
end
class Cat < Animal
has_many :food
has_many :hey
end
kitty = Cat.new
kitty.food
```
參考:[metaprogramming-ruby-2/](https://pragprog.com/titles/ppmetr2/metaprogramming-ruby-2/)
- 如果使用單數會對嗎?會對,但原則上都是寫複數
### product.rb增加belongs_to :user 方法
```ruby
# product.rb
belongs_to :user
```
可以想像成
```ruby
u2 = User.find(2)
u2.id = 2
u2.products
Select * from products where user_id = (u2.id)
p1 = Product.find(1)
p1.user_id = 3
p1.user
Select * rom users where id = (p1.user_id)
```
- 有has_many就有belong_to嗎?看會不會用到,都不是必需品
- 兩邊都寫就是可以互相查
## 嘗試登入後新增商品

## 在porduct.controller的create方法
加上
```ruby
#products.controller.rb
@product.user = current_user
# 原本應該寫@product.user_id = current_user,belongs_to :user 幫忙生的
```
再嘗試新增商品就可以成功:

### 另一種寫法
Product.new(title: 123, price: 111, user: current_user)
```ruby
#products.controller.rb
# 1 @product = Product.new(product_params)
# 2 @product.user = current_user
@product = current_user.products.new(product_params)
```
- 這行取代1,2
## 移除不是自己的商品
```ruby
current_user.products_ids.includes?(1)
current_user.own?(...)
列出使用者有的產品id
```
利用以上概念,使用`current_user.own?(...)`
- 到_show.erb.html寫if判斷式
```ruby
<% if user_signed_in? and current_user.own?(@product) %>
<%= link_to "編輯商品", edit_product_path, class: 'ink-btn' %>
<%= link_to '刪除商品', product_path(@product), class: 'ink-btn ink-btn-danger', data: {
turbo_method: 'delete',
turbo_confirm: '確認刪除?'
} %>
<% end %>
```
- 到user.rb寫own方法
```ruby
#user.rb
def own?(p)
product_ids.include?(p.id)
end
```
### 還沒完成
到
```ruby
# products.controller
before_action :find_owned_product, only: [:edit, :update, :destroy]
def find_owned_product
@product = current_user.products.find(params[:id])
end
```
### before_action順序
由上而下讀取,可以調整排序以符合邏輯
大家都可以看產品 -> 要登入 -> 登入才能新增修改刪除
```ruby=
before_action :find_product, only: [:show, :edit, :update, :destroy] #推薦使用
before_action :authenticate_user!, except: [:index, :show]
before_action :find_owned_product, only: [:edit, :update, :destroy]
```
### memorization方法
在商品列表頁查詢2次,因為呼叫current_user就查一次
navbar一次,find_by一次
查太多次了,可以利用改寫:
```ruby=
#application_controller
def current_user
#memorization
@__user__ ||= User.find_by(id: session[:__user_ticket__])
end
```
- `a ||= b` -> `a = a || b`
- 利用`邏輯短路`,如果`@__user__`存在,我就拿`@__user__`給你,如果沒有再`User.find_by(id: session[:__user_ticket__])`
### 登入才出現新增商品
```ruby
#index.html.erb
<%= link_to "新增商品", new_product_path, class: 'ink-btn' if user_signed_in? %>
```
```ruby=
#_navbar.html.erb
<%= link_to "新增商品", new_product_path if user_signed_in? %>
```
## 新增留言
### 架構
- user: int
- product_id: int
- content: text
- delete_at: datetime:index
### 新增model
```terminal
rails g model Comment content:text deleted_at:datetime:index user:belongs_to product:belongs_to
```
`rails db:migrate`
### Query String
### 新增路徑
```ruby
#routes.rb
/products/2 show
POST /products/2/comments, to: "comments#create"
DELETE /products/2/comments/3, to: "comments#destroy"
```
- 要刪掉二號商品的三號留言
- 但不用管幾號商品,所以可以改寫成
```ruby
#routes.rb
/products/2 show
POST /products/2/comments, to: "comments#create"
DELETE /comments/3, to: "comments#destroy"
```
所以routes.rb可以寫成
```ruby
resources :products do
resources comments, only: [:index, :new, :create]
end
resources comments, only: [:show, :edit, :update, :destroy]
```
- 僅新增留言需要product_id,所以寫在products裏面
- 因為comments本身有流水編號,所以拿留言編號去刪除就夠了
- 要都放裡面也沒關係(就是路徑會比較醜)
- 參考[rails-routing](https://rails.ruby.tw/routing.html)
- 因為rails有幫忙改成下面:
```ruby
resources :products do
resources :comments, shallow: true
end
```
### comments需要哪些頁面
- index X (顯示單則留言)
- new X
- 放在show就可以
- create O
- edit X update X
- destroy O
```ruby=
#show.html.erb
<%= form_with(model: @comment,
url: product_comments_path(@product),
data: { turbo: false }) do |form| %>
<% end %>
```
or
```ruby
<%= form_with(model: [@product, @comment],
data: { turbo: false }) do |f| %>
```
```ruby=
#product.controller
def show
@comment = Comment.new
end
```
### 新增comment的controller
```terminal
$ rails g controller comments
```
```ruby
def create
product = Product.find(params[:product_id])
render html: params
end
```
```ruby
#product.rb
had_amny :comments
```
### 資料驗證
```ruby=
#comments_controller.rb
def comment_params
prarms.require(:comment).permit(:content)
end
```
```ruby=
#comments_controller.rb
def create
product = Product.find(params[:product_id])
comment = product.comments.new(comment_params)
if comment.save
redirect_to product_path(product), notice: '新增留言成功'
else
redirect_to products_path(product), alert: '留言發生錯誤
end
end
```
```ruby=
#comment.rb
validates :content, presence: true
```
```ruby=
#user.rb
has_many :comments
```
要怎麼把user.id灌到裡面?
```ruby
h = {name: 123}.merge(a:1, b:2)
h[:cc] = 123
p h
{name:123, cc:123}
```
```ruby
h = {name: 123}.merge(a:1, b:2)
p h
{name:123, a:1, b:2}
```
- 改寫permit就可以得到`user.id`
```ruby=
#comments_controller.rb
def comment_params
prarms.require(:comment).permit(:content).merge(user: current_user)
end
```
### 設定好model
```ruby=
#products_controller.rb
def show
@comment = Comment.new
@comments = @product.comments
#把.order(id: :desc)改寫到model
end
```
### 寫表單小幫手
```ruby=
#show.html.erb
<section>
<h3 class="text-xl">留言</h3>
<%= form_with(model: [@product, @comment],
data: { turbo: false }) do |f| %>
<div>
<%= f.text_area :content, class: 'input-field' %>
</div>
<%= f.submit %>
<% end %>
</section>
```
### 利用each,列出留言
```ruby=
# show.html.erb
<ul>
<% @comments.each do |comment| %>
<li><%= comment.content %></li>
<% end %>
</ul>
```
### 排序留言
可以寫在`product.controller`裡 或是 `product.rb`
```ruby=
#product.rb
has_many :comments, -> {order(id: :desc)}
```