0723 Day1 建立看板
0724 Day2 第一個Model: 新增看板
0730 Day3 第二個Model: 新增文章
0731 Day4 資料驗證、第三個Model: 新增使用者
<200>
Q:網頁200 202 300 404…等是什麼意思?
A:HTTP狀態碼
< Type >
Q:Type是什麼意思?
A: ex:'Content-Type' => 'text/html'
告訴瀏覽器將這個網頁以html來看待
V PTT forum / comment / message / mail
專案管理系統 Jira / Trello
記帳
Forum / Reddit
購物網站
Flowdock / Slack / Line / Tinder (ActionCable - WebSocket)
Podcast 平台
POS系統 (iPad)
TimeTree RWD
GitHub
目前比較不適合的專案
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root "pages#index"
get '/about', to: "pages#about"
end
pages_controller.rb # 檔名是蛇式
class PagesController # 類別名稱是駝峰式
Board的資料欄位
- title:string
- intro:text
- deleted_at:datetime, nil/null (知道何時被刪除)
-> 加index增加搜尋速度, 但會減慢寫入速度
(- is_deleted:boolean 是否被刪除, 預設false, 可以用deleted_at取代)
- state: [normal, disable, hidden]
SQL: 列出所有(沒被刪除)的看板
select *
from boards
where deleted_at is not null
rails g model Board title:string intro:text deleted_at:datetime:index state:string
以上指令會建出 app/models/board.rb
board.rb # 檔名是蛇式
class Board # 類別名稱是駝峰式
在migrate的create_boards.rb
把state設為normal
t.string :state, default: 'normal'
rails db:migrate
== 20200723061253 CreateBoards:
migrating =====================================
-- create_table(:boards)
-> 0.0012s
-- add_index(:boards, :deleted_at)
-> 0.0008s
== 20200723061253 CreateBoards: migrated (0.0021s) ============================
rails db
指令可以進去sqlite查詢資料表
rails db
SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.
sqlite> select * from schema_migrations;
20200723061253
Q. 如果執行migration
後要再修改欄位:
rails g migration [name]
–> rename_columnrails db:migration
rails db:rollback
Q. 沒有存檔,欄位還沒更新就執行rails db:migrate
,怎麼抓錯誤?
-> 查看schema.rb
,看看欄位有沒有被更新過
進rails c
查看目前建立的Board
/PPT master rails c
Running via Spring preloader in process 1560
Loading development environment (Rails 5.2.4.3)
2.5.2 :001 > Board.all
Board Load (2.6ms) SELECT "boards".* FROM "boards" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
進入--sandbox
模式假裝創建資料
/PPT master rails c --sandbox
Running via Spring preloader in process 1614
Loading development environment in sandbox (Rails 5.2.4.3)
Any modifications you make will be rolled back on exit
2.5.2 :001 > Board.create(title: 'Ruby')
(0.1ms) SAVEPOINT active_record_1
Board Create (1.6ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Ruby"], ["created_at", "2020-07-23 06:30:02.969252"], ["updated_at", "2020-07-23 06:30:02.969252"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<Board id: 1, title: "Ruby", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 06:30:02", updated_at: "2020-07-23 06:30:02">
.source_location
2.5.2 :002 > Board
=> Board(id: integer, title: string, intro: text, deleted_at: datetime, state: string, created_at: datetime, updated_at: datetime)
2.5.2 :003 > Board.method(:all)
=> #<Method: Board(id: integer, title: string, intro: text, deleted_at: datetime, state: string, created_at: datetime, updated_at: datetime).all>
2.5.2 :004 > Board.method(:all).source_location
=> ["/Users/tingtinghsu/.rvm/gems/ruby-2.5.2/gems/activerecord-5.2.4.3/lib/active_record/scoping/named.rb", 26]
2.5.2 :005 > 3.days.method(:ago).source_location
=> ["/Users/tingtinghsu/.rvm/gems/ruby-2.5.2/gems/activesupport-5.2.4.3/lib/active_support/duration.rb", 366]
[wiki](Representational State Transfer)
用同個標準命名路徑
視路徑為resources
Eg. 看板可能的路徑path:
/boards
/boards/2
/boards/2/edit
class BoardsController < ApplicationController
def index
@boards = Board.all #用實體變數裝起來傳給view
end
end
<h1>看板</h1>
<%= @boards %>
看板
#<Board::ActiveRecord_Relation:0x00007fc91ded30f8>
2.5.2 :005 > Board.create(title: 'Ruby')
(0.1ms) begin transaction
Board Create (1.7ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Ruby"], ["created_at", "2020-07-23 07:45:47.861782"], ["updated_at", "2020-07-23 07:45:47.861782"]]
(1.3ms) commit transaction
=> #<Board id: 1, title: "Ruby", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:45:47", updated_at: "2020-07-23 07:45:47">
2.5.2 :006 > Board.create(title: 'PHP')
(0.4ms) begin transaction
Board Create (1.5ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "PHP"], ["created_at", "2020-07-23 07:46:35.574927"], ["updated_at", "2020-07-23 07:46:35.574927"]]
(1.4ms) commit transaction
=> #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35">
.each
印出所有的檔案<h1>看板</h1>
<ul>
<% @boards.each do |b| %>
<li><%= b.title %></li>
<% end %>
</ul>
網頁顯示
看板
Ruby
PHP
在view建立連結
<a href="/boards/new">新增看板</a>
改成name_path
Q:主管說名稱從"boards"改為"cards"該怎麼做?
A:進到routes.rb檔執行以下
resources :boards, path: 'cards'
<a href="<%= new_board_path %>">新增看板</a>
rails的寫法link_to
,
<%= link_to '新增看板', new_board_path if false %>
new.html.erb,
method
用post
,不會像get
把參數(如密碼)直接放在網址上
<h1>新增看板</h1>
<form action="/boards" method="post">
<label for="title">看板名稱</label>
<input type="text" name="title" id="title"><br>
<label for="intro">說明</label>
<textarea name="intro" id="intro"></textarea>
<input type="submit" value="送出">
</form>
在controller印出來傳進來的參數"create"看看
def create
render html: params
end
出現InvalidAuthenticityToken
錯誤
ActionController::InvalidAuthenticityToken in BoardsController#create
如果沒有認證的Token,就沒有辦法寫入,資安上比較安全
params是從某一頁表單傳到另一列的參數
一個修正後的hash
params["title"]
params[:title]
都可以拿到資料
先用爛方法在controller用render把答案印出來
def create
render html: params
end
頁面會顯示
http://localhost:3000/boards
{"authenticity_token"=>"KxKoyhFrzt59vnHbxTIRRtfX4eYzfRez5I7cY78DRN86H1nTdh+7joUBogmEIUPBf8HrnSWbMKOjeq7FAWU80A==", "title"=>"123", "intro"=>"456", "controller"=>"boards", "action"=>"create"}
只抓出重要欄位並存入
def create
Board.create(title: params[:title], intro: params[:intro])
redirect_to "/"
end
設定
可以長表單出來
有3種
form_for
後面要接modelform_for(model)
<%= form_for(Board.new) do |f| %>
<% end %>
form_tag
不需要modeleg. 輸入身高體重、計算BMI(不需要用到資料庫)
form_for
form_for(Board.new)
會猜是要新增,決定路徑往哪裡去
(有的時候會猜錯,所以會在後面加上URL)
<%= form_for(ApplicationRecord.new) do |f| %>
<% end %>
=> 會出錯,抽象類別不能new出實體
form_for
建立欄位<%= form_for(Board.new) do |f| %>
<%= f.label(:title), "看板名稱" %>
<%= f.text_field(:title)%>
<% end %>
和昨天做的form有麼不同?
檢查網頁原始碼,原本的欄位
<label for="title">看板名稱</label>
<input type="text" id="title" name="title"><br />
<label for="intro">說明</label>
<textarea id="intro" name="intro"></textarea>
Parameters: {"authenticity_token"=>"35/KxPh1Xkg==", "title"=>"456", "intro"=>"789"}
利用form_for
做出來
<label for="board_title">看板名稱</label>
<input type="text" name="board[title]" id="board_title" />
<label for="board_intro">看板說明</label>
<textarea name="board[intro]" id="board_intro">
form_for
根據model的名字
和欄位的名字
會包成一包hash
Parameters: {"utf8"=>"✓", "authenticity_token"=>"YnNwIUkCkvm=",
"board"=>{"title"=>"123", "intro"=>"456"}, "commit"=>"送出"}
controller
def new
@board = Board.new
end
view
<%= form_for(@board) do |f| %>
<%= f.label :title, "看板名稱" %>
<%= f.text_field :title %>
<%= f.label :intro, "看板說明" %>
<%= f.text_area :intro %>
<%= f.submit "送出" %>
<% end %>
create method
def new
@board = Board.new
#要先new實體變數不然表單的引數會是nil
end
def create
# http沒有狀態,每次request都是全新的開始
# 兩次request 會產生兩個不一樣的實體
# 所以這裡的@board和new方法的@board是不同的
@board = Board.new(params[:board])
end
params是一個方法,負責把傳過來的字串打包好成hash
#想像params方法定義在上層,作用看起來像這樣子:
def params
{:board => "aa"}
end
def create
@board = Board.new(params[:board])
if @board.save
# OK
else
# NG
end
# Board.create(title: params[:title], intro: params[:intro])
# redirect_to "/"
end
在index上用param的話,只能在console裡看到,頁面上不會顯示
def index
puts "---------------------"
p params
p params[:title]
puts "---------------------"
@boards = Board.all
@boards = Board.all
end
<ActionController::Parameters
{"controller"=>"boards", "action"=>"index"}
permitted: false>
nil
def create
@board = Board.new(params[:board])
if @board.save
redirect_to boards_path
else
# NG
end
strong parameters處理controller的 Forbidden Attributes Error問題
防止有心人士透過開發者工具塞參數進來
def create
# Strong Parameters
clean_params = params.require(:board).permit(:title, :intro)
@board = Board.new(clean_params)
if @board.save
redirect_to boards_path
else
# NG
end
end
改寫成private方法
private
# Strong Parameters
def board_params
params.require(:board).permit(:title, :intro)
end
def create
@board = Board.new(board_params)
end
controller幾乎不能重新使用,但是model可以
每個controller的method只能做一件事情
def index
@boards = Board.all
render html: "hi"
redirect_to root
end
錯誤訊息: DoubleRenderError
AbstractController::DoubleRenderError in BoardsController#index
controller的method預設會去尋找同名的view檔案,但一個method只能執行一個動作
eg. 如果又加了render -> 畫面顯示404頁面
def index
@boards = Board.all
render file: "/public/404.html"
# redirect_to '/'
end
flash[:notice]
controller: 新增成功時出現訊息
if @board.save
flash[:notice] = "新增成功!"
redirect_to boards_path
else
# NG
end
view
<h2><%= flash[:notice] %></h2>
or
<h2><%= notice %></h2>
或是放在公板application.html.erb
裡
flash太常使用了,所以也可以直接在controller提示:
if @board.save
redirect_to boards_path, notice: "新增成功"
else
# NG
end
model的驗證條件
class Board < ApplicationRecord
validates :title, :intro, presence: true, length: {minimum: 2}
# 舊式寫法
# validates_presence_of :title
end
~/Documents/projects/PPT master ● rails c --sandbox
Running via Spring preloader in process 6455
Loading development environment in sandbox (Rails 5.2.4.3)
Any modifications you make will be rolled back on exit
2.5.2 :001 > b1 = Board.new
=> #<Board id: nil, title: nil, intro: nil, deleted_at: nil, state: "normal", created_at: nil, updated_at: nil>
2.5.2 :002 > b1.errors.any?
=> false
2.5.2 :003 > b1.save
(0.2ms) SAVEPOINT active_record_1
(0.2ms) ROLLBACK TO SAVEPOINT active_record_1
=> false
2.5.2 :004 > b1.errors.any?
=> true
2.5.2 :006 > b1.errors.full_messages
=> ["Title can't be blank", "Title is too short (minimum is 2 characters)", "Intro can't be blank", "Intro is too short (minimum is 2 characters)"]
欄位出錯的時候,form_for
會在錯誤欄位包一層東西,
如何用css呈現錯誤的欄位?
SASS
修改css
form {
label {
display: block;
}
.field_with_errors {
input,textarea {
border: 2px solid red;
}
}
}
在view跑迴圈
重要:不要在view裡面new出實體!
<% if @board.errors.any? %>
<ul>
<% @board.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<ul>
<% @boards.each do |board| %>
<li><%= link_to board.title, board_path(board) %></li>
<% end %>
</ul>
建立show.html.erb
controller加上方法
試著把抓的param在show method印出來看看
def show
puts "-"* 50
p params
puts "-"* 50
end
點入任一個版面的連結
server會抓到參數
Started GET "/boards/1" for 127.0.0.1 at 2020-07-24 15:23:20 +0800
Processing by BoardsController#show as HTML
Parameters: {"id"=>"1"}
--------------------------------------------------
<ActionController::Parameters {"controller"=>"boards", "action"=>"show", "id"=>"1"} permitted: false>
--------------------------------------------------
透過param[:id]
可以抓到數字
def show
Board.where(id: params[:id]) => [1, 1]
Board.find_by(id: params[:id]) # => 1
Board.find(params[:id]) # 同id有多筆時會出錯
# 以上3種都會翻譯成SQL
# select * from boards where id = ?
find
和 find_by
的差別find
只能找id
find_by
可以接hash
Board.find_by(id: param[:id], intro: "aaa")
.find_by
回傳nil
2.5.2 :008 > Board.find_by(id: 1234)
Board Load (0.5ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 1234], ["LIMIT", 1]]
=> nil
.find
直接噴錯誤訊息
2.5.2 :011 > Board.find(1234)
Board Load (0.3ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 1234], ["LIMIT", 1]]
Traceback (most recent call last):
1: from (irb):11
ActiveRecord::RecordNotFound (Couldn't find Board with 'id'=1234)
直接寫rescue
def show
begin
@board = Board.find(params[:id])
rescue
render file: '/public/404.html', status: 404
end
end
讓controller method裡面更乾淨的方法:
所有controller只要找不到就回傳404
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def not_found
render file: '/public/404.html', status: 404
end
end
讓Font-end Controller 和 Back-end Controller分開
view上建立新增連結
<% @boards.each do |board| %>
<li>
<%= link_to board.title, board_path(board) %>
<%= link_to '編輯', edit_board_path(board) %>
</li>
<% end %>
controller跟新增方法的流程類似
def update
@board = Board.find(params[:id])
if @board.update(board_params)
redirect_to boards_path, notice: "更新成功"
else
render :edit
end
end
delete是特殊的動詞,使用時必須標注 method: 'delete'
(預設method: 'get'
)
重要:加上data: {confirm: '確認刪除?'}
確認刪除
<% @boards.each do |board| %>
<li>
<%= link_to board.title, board_path(board) %>
<%= link_to '編輯', edit_board_path(board) %>
<%= link_to '刪除', board_path(board), method: 'delete',
data: {confirm: '確認刪除?'} %>
</li>
<% end %>
controller新增destroy方法
def destroy
@board = Board.find(params[:id])
@board.destroy
redirect_to boards_path, notice: "刪除成功"
end
改成update
def destroy
@board = Board.find(params[:id])
#@board.destroy
@board.update(deleted_at: Time.now)
redirect_to boards_path, notice: "刪除成功"
end
列出沒有被刪掉的看板
def index
@boards = Board.where(deleted_at: nil)
end
board.rb
class Board < ApplicationRecord
validates :title, :intro, presence: true, length: {minimum: 2}
# validates_presence_of :title
def destroy
update(deleted_at: Time.now)
end
end
before_action :find_board, only: [:show, :edit. :updat, :destroy]
private
def find_board
@board = Board.find(params[:id])
end
_form.html.erb
引入渲染的檔案渲染的檔案盡量不要有區域變數
讓其他的地方帶實體變數進來,重複使用的好處
_form.html.erb
<% if board.errors.any? %>
<ul>
<% board.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<%= form_for(board) do |f| %>
<%= f.label :title, "看板名稱" %>
<%= f.text_field :title %>
<%= f.label :intro, "看板說明" %>
<%= f.text_area :intro %>
<%= f.submit "送出" %>
<% end %>
new, show
<%= render 'form', board: @board %>
讓所有搜尋結果加上條件而不是只有Board.all
Eg. 找product價錢大於50元
Product.where(price > 50)
=> select * from products where price > 50
可以寫成 Product.cheap 或 Product.expensive
賦予這段程式碼意義
class Product
def self.expensive
where("price > 100")
end
end
Product.expensive => 使用類別方法
p = Product.new
p.save => 使用實體方法
找出存在的看板
def self.avaliable
where(deleted_at: nil)
end
一行程式碼可以解決的話就用scope
回呼函數 callback function
前面不要忘記加逗號,
scope :available, -> { where(deleted_at: nil) }
default_scope
作為預設的過濾條件
default_scope { where(deleted_at: nil) }
抵銷預設scope
[Rails API] (https://github.com/rubysherpas/paranoia)
Board.unscoped
paranoia
套件實作假刪除功能
Rubygem 複製最新版的套件
gem 'paranoia', '~> 2.4', '>= 2.4.2'
路徑:Gemfile/# Reduces boot times through caching; required in config/boot.rb
$ bundle(install)
Gemfile.lock檔
,所以速度比較慢$ install gem_name
$ rails g model Post title content:text board:belongs_to deleted_at:datetime:index ip_address serial:string:unique
做錯了的話把rails g
改成rails d
,就會刪掉原本建立的model
建立多對多關聯
一個看板有很多文章
board.rb : has_many :posts
class Board < ApplicationRecord
acts_as_paranoid
has_many :posts
validates :title, presence: true, length: { minimum: 2 }
end
進去rails c來看
2.5.2 :005 > b = Board.find(2)
Board Load (0.4ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35">
2.5.2 :007 > b.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
如果剛開始建立的欄位不是board_id
,而是b_id
,要自己手動加上foreign_key
has_many :posts, foreign_key: b_id
class Post < ApplicationRecord
belongs_to :board, optional: true
end
如果沒有指定optional: true
就一定要board_id
才能存入
有很多種方法:
[找到後指定id]
px = Post.new(title: "aa")
px.board_id = 9
px.save
---
[跟上面的方法類似]
Post.create(title: "aa", board_id: 9)
---
[比較安全:先找到看板再新增文章然後存進去]
bb = Board.find(9)
px = Post.new(title: "aa")
px.board = bb
px.save
---
[從belongs_to]的源頭往下寫
bb = Board.find(9)
bb.posts.create(title: "aa")
方法一 從post指定board_id
2.5.2 :001 > p = Post.new
=> #<Post id: nil, title: nil, content: nil, board_id: nil, deleted_at: nil, ip_address: nil, serial: nil, created_at: nil, updated_at: nil>
2.5.2 :003 > p.board_id = 2
=> 2
2.5.2 :004 > p.save
(0.2ms) begin transaction
Board Load (0.6ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Post Create (2.9ms) INSERT INTO "posts" ("board_id", "created_at", "updated_at") VALUES (?, ?, ?) [["board_id", 2], ["created_at", "2020-07-30 05:58:46.270668"], ["updated_at", "2020-07-30 05:58:46.270668"]]
(1.7ms) commit transaction
=> true
方法二: 從board對應
因為belongs_to幫忙post產生了兩個方法
class Post < ApplicationRecord
belongs_to :board
# .board
# .board=[]
end
2.5.2 :006 > b = Board.second
Board Load (8.8ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL ORDER BY "boards"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=> #<Board id: 3, title: "Python", intro: "123", deleted_at: nil, state: "normal", created_at: "2020-07-23 09:11:39", updated_at: "2020-07-24 08:31:13">
2.5.2 :007 > p = Post.new
=> #<Post id: nil, title: nil, content: nil, board_id: nil, deleted_at: nil, ip_address: nil, serial: nil, created_at: nil, updated_at: nil>
2.5.2 :008 > p.board = b
=> #<Board id: 3, title: "Python", intro: "123", deleted_at: nil, state: "normal", created_at: "2020-07-23 09:11:39", updated_at: "2020-07-24 08:31:13">
2.5.2 :009 > p.save
(1.2ms) begin transaction
Post Create (1.9ms) INSERT INTO "posts" ("board_id", "created_at", "updated_at") VALUES (?, ?, ?) [["board_id", 3], ["created_at", "2020-07-30 06:05:30.718942"], ["updated_at", "2020-07-30 06:05:30.718942"]]
(1.2ms) commit transaction
=> true
2.5.2 :011 > b.posts.count
(0.5ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."board_id" = ? [["board_id", 3]]
=> 1
方法三: 直接從看板的角度建立一篇文章
因為has_many
幫我們作出了create
和build
方法
b.posts.create(title: "aa")
2.5.2 :012 > b.posts.create(title: "從board的方式 create post")
(0.2ms) begin transaction
Post Create (1.2ms) INSERT INTO "posts" ("title", "board_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "從board的方式 create post"], ["board_id", 3], ["created_at", "2020-07-30 06:10:30.054054"], ["updated_at", "2020-07-30 06:10:30.054054"]]
(1.2ms) commit transaction
=> #<Post id: 3, title: "從board的方式 create post", content: nil, board_id: 3, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 06:10:30", updated_at: "2020-07-30 06:10:30">
兩種方法長得有點不一樣
Has Many
bb.posts.create ...
bb.posts.build / save
Has One
bb.create_post
bb.build_post
Eg.
class Board < ApplicationRecord
validates :title, :intro, presence: true, length: {minimum: 2}
acts_as_paranoid
has_many :posts
has_one :post
end
has_one
的SQL語法只找一筆
2.5.2 :001 > b = Board.find 2
Board Load (0.5ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35">
2.5.2 :002 > b.posts
Post Load (2.0ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 1, title: nil, content: nil, board_id: 2, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 05:58:46", updated_at: "2020-07-30 05:58:46">]>
2.5.2 :003 > b.post
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 1]]
=> #<Post id: 1, title: nil, content: nil, board_id: 2, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 05:58:46", updated_at: "2020-07-30 05:58:46">
不好的網址設計:
Rails.application.routes.draw do
resources :boards do
resources :posts
end
end
進階的寫法:把文章包進看板裡,把不需要的功能抽掉
Rails.application.routes.draw do
resources :boards do
resources :posts, only: [:index, :new, :create]
end
resources :posts, except: [:index, :new, :create]
end
官網
shallow (淺層嵌套)
Rails.application.routes.draw do
resources :boards do
resources :posts, shallow: true
end
<%= form_with(model: @post) do |f| %>
<%= f.label :title, "文章名稱" %>
<%= f.text_field :title %>
<%= f.label :content, "文章內容" %>
<%= f.text_area :content %>
<%= f.submit "送出" %>
<% end %>
form_with
要塞model給它, 然後指定url(不然猜不到)
new 完之後送到create,路徑是board_posts_path
<%= form_with(model: @post, url: board_posts_path) do |form| %>
<%= form.submit %>
rails在建立表單的時候-form-for-跟-form-with-有什麼不同
form_with是用Ajax方式送資料,所以畫面上不會有動作
寫法的差異
<%= form_with(model: @post, url: board_posts_path) do |form| %>
<%= form_for(@post, url: board_posts_path) do |form| %>
補充昨天說明:rails scafford在form_with也是先設定 local: true
(先逃避turbolinks的問題)
回想資料庫的資料的相依性(PK, FK)
資料的association: dependent
Eg. 如果設定 dependent: :destroy
has_many :posts, dependent: :destroy
SQL會先幫我們砍看板,然後跟著砍掉文章
但是因為我們已經裝了paranoid
假刪除功能
刪了等於沒刪 (dependent: :destroy
這句無效)
class Board < ApplicationRecord
acts_as_paranoid
has_many :posts, dependent: :destroy
end
def serial_generator(n)
# a-z / A-Z / 0-9 產生亂數
end
puts serial_generator(8)
先把三組陣列蒐集起來
list = [*'A'..'Z'] + [*'a'..'z'] + [*'1'..'9']
解法一. 把陣列利用sample
取出八個數字,再join
成字串
list.sample(8).join
解法二. 如果不知道sample
這個method,想像洗牌的方式,
先shuffle
洗牌再取出前8張
list.shuffle.first(8).join
list = [*'A'..'Z'] + [*'a'..'z'] + [*'1'..'9']
pool = list - ['i','I', 'l', 'L', '1', '0', 'o', 'O']
pool.shuffle.first(8).join
排列組合的機率,保證序號重複可能性很低!
class Post < ApplicationRecord
validates :title, presence: true, length: {minimum: 2}
validates :serial, presence: true, uniqueness: true
belongs_to :board
end
uniqueness
也可以加上scope
官方手冊的例子
scope: :year
validates :name, uniqueness
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
注意:不要寫在controller裡
rails model的存檔流程
先用鴕鳥方式處理舊文章:allow_nil: true
class Post < ApplicationRecord
validates :title, presence: true, length: {minimum: 2}
# 資料庫已經有很多文章是nil,非uniqueness
validates :serial, presence: true, uniqueness: true, allow_nil: true
belongs_to :board
before_create :create_serial #存檔前先建立序號
private
def create_serial
self.serial = serial_generator(10)
# attr_reader的方法,省略小括弧
# self.serial=(serial_generator(10))
end
def serial_generator(n)
[*'a'..'z', *'A'..'Z', *0..9].sample(n).join
end
end
寫一個rake來更新還沒有序號的舊文章
結構
namespace :db do
desc "更新文章序號"
task :update_post_serial do
# puts "hi"
end
end
Rake -T
指令列出所有的task
~/Documents/projects/PPT master ● rake -T
rake about # List
rake db:structure:dump # Dumps the database structure to db/...
rake db:structure:load # Recreates the databases from the st...
rake db:update_post_serial # 更新文章序號
把剛剛寫的亂數產生序號
方法塞進去
挑出序號為nil的文章,update post
的序號
namespace :db do
desc "更新文章序號"
task :update_post_serial do
Post.where(serial: nil).each do |post|
post.update(serial: serial_generator(10))
end
end
private
def serial_generator(n)
[*'a'..'z', *'A'..'Z', *0..9].sample(n).join
end
end
task :update_post_serial => :environment
指定環境變數,不然會找不到Post
namespace :db do
desc "更新文章序號"
task :update_post_serial => :environment do
puts "-----------------"
puts " updating serial "
puts "-----------------"
Post.where(serial: nil).each do |post|
post.update(serial: serial_generator(10))
print "."
end
puts "done!"
end
private
def serial_generator(n)
[*'a'..'z', *'A'..'Z', *0..9].sample(n).join
end
end
印出提示字元,讓使用者知道已經做完
~/Documents/projects/PPT master ● rails db:update_post_serial
-----------------
updating serial
-----------------
......done!
注意:呼叫方法時要注意receiver是誰
(例如private method的話就不能明確指出receiver)
如果self沒寫出來(隱含的self),跟寫出來self的意義不一樣
task :update_post_serial => :environment do
Post.where(serial: nil).each do |post|
post.update(serial: serial_generator(10))
end
puts "done!"
end
Self
和 JS this
的差別(待補充)
Cat
-> Animal
-> Object
-> BasicObject
檔案查找到Basic還找不到的時候會再回來Cat問有沒有method_missing
如果有的話,印出來
找不到的話,呼叫super
上層來做
module Kernal
def method_missing(...)
# 錯誤訊息印在這裡
end
end
這就是Ruby效能比較不好的原因
# 效能差
Board.find_by_id_title(1, "aaa")
# 直接用hash找,效能好
Board.find_by(id: 1, title: "aaa")
model: User
- accout:string
- password:string
- email:string
- nickname:string
- gender:string
- state:string
- deleted_at:datetime:index
model: Profile
- 簽名檔
- 名片檔
Counter Cache: 用在上站次數。每次登入的時候先存入,增加效能
讓每個資料表只存該存的資料就好
每個欄位應該只有一筆資料 => 拆開來
刪除資料中的重複群組
至少要做到第一正規化,不然就失去了使用資料庫的意義
Eg.三個model
rails g model User account:string:uniq password email:string:uniq nickname gender state deleted_at:datetime:index
然後rails db:migrate
密碼加密
md5 -> 不安全
SHA-1
SHA-256
rails c會載入常用的標準函式庫
require只會載入一次
irb
~/Documents/projects/PPT master ● irb
2.5.2 :001 > require 'digest'
=> true
2.5.2 :002 > require 'digest'
=> false
2.5.2 :003 > require 'digest'
=> false
# keyword argument 關鍵字引數
def self.login(options)
if options[:account] && options[:password]
find_by(account: options[:account], # 帳號: aa
password: add_salt(options[:password])) # 密碼: 已經加密為xyz
# else
# return false
end
end
private
def encrypt_password
# 註冊時加密
self.password = User.add_salt(self.password)
end
def self.add_salt(password)
Digest::SHA1.hexdigest("x#{password}y")
end
console c
2.5.2 :001 > User.login(account: "cc", password: "123")
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."account" = ? AND "users"."password" = ? LIMIT ? [["account", "cc"], ["password", "9eb8e9439adfea0f1615e2dc268117a2a32cb3bf"], ["LIMIT", 1]]
=> nil
單數和複數會長出不一樣的路徑
resources :users
/users/2/edit #=> 不應該給使用者看到id
resource :users
/user/edit #=> 比較好的寫法
resources :users, only: [:new, :create] do # 只有新增時看到自己序號, index user列表是給後台管理用的
member do
get :profile_member
end
collection do # 擴充網址時,collection沒有id
get :profile_collection
end
end
做出來路徑的差別
profile_member_user GET /users/:id/profile_member(.:format) users#profile_member
profile_collection_users GET /users/profile_collection(.:format) users#profile_collection
logout動詞用delete
resources :users, only: [:new, :create] do
collection do
get :profile
get :login
delete :logout
end
end
不想用原本的八條路徑,更進階的寫法
Eg.把原本new的路徑改成sign_up,英文比較符合一般使用者註冊時的流程
resources :users, only: [:create] do
collection do
get :sign_up
get :edit
patch :update
get :sign_in
post :login
delete :sign_out
end
end