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