---
# System prepended metadata

title: Ruby on Rails - 2023/11/28
tags: [Ruby, rails]

---

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嗎？看會不會用到，都不是必需品
- 兩邊都寫就是可以互相查

## 嘗試登入後新增商品
![截圖 2023-11-28 下午2.06.32](https://hackmd.io/_uploads/BkGFCQXHT.png)



## 在porduct.controller的create方法
加上

```ruby
#products.controller.rb
@product.user = current_user 
# 原本應該寫@product.user_id = current_user，belongs_to :user 幫忙生的
```
再嘗試新增商品就可以成功：

![截圖 2023-11-28 下午2.11.29](https://hackmd.io/_uploads/SJqOA7mBp.png)


### 另一種寫法

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)}
```


