about 4 years ago

關於MVC

參考文章

什麼東西應該放在model

只要是跟操作資料庫有關的行為基本上都應該儘量放在Model裡面實作,大部份情況是要操做已經叫出來的實例變數(若對ruby的變數不太了解請先參考ruby系列教學)。

Model是什麼?

Model是繼承於ActiveRecord的Class,所以每個Model都可以取用ActiveRecord裡面的Method,例如我們有一個Model叫做Post,這個Model會長這樣:

class Post < ActiveRecord::Base
end

而這個Model將會負責操作Posts的資料庫,由於Post Model繼承ActiveRecord,所以我們可以用ActiveRecord的method例如

# first就是ActiveRecord的Method

Post.first
# => 回傳第一個Post

has_many :through

has_many :through主要是在建立多對多關聯資料庫的時候使用,例如一個球隊可以有很多球員,一個球員可以參加很多球隊,這個就是多對多關係。在這裏球員跟球隊都是一個model,要建立多對多關係時我們會需要第三個model扮演連接的橋樑。第三個Model就是體育協會,負責登記每個球員所屬的球隊以及球隊目前的成員。如此一來我們就可以從體育協會得知每個球隊和球員的狀況(所以叫作through)。
所以球員throuth體育協會has_many球隊,舉例:

model/player.rb
class Player < ActiveRecord::Base
    # 先告訴model我們在體育協會有很多筆資料

    has_many :sports_associations
  # 這些資料是要拿來判斷這個球員有參與多少球隊

    has_many :teams, :through => :sports_associations
end
model/team.rb
class Team < ActiveRecord::Base
    has_many :sports_associations
    has_many :players, :through => :sports_associations
end
model/sports_associations.rb
class SportsAssociations < ActiveRecord::Base
    #體育協會要對球員和球隊負責,所以體育球隊belongs_to球員和球隊

    belongs_to :team
    belongs_to :player
end

validation

在model內驗證資料的方法,用以確保資料符合我們的要求。validation運作的時機是在sql指令執行之前,所以當ActiveRecord認為這筆資料未通過驗證,就不會將ruby轉換為sql語言寫入,而是傳回錯誤訊息。
validation常見的寫法有:

class Topic < ActiveRecord::Base
    # presence: true代表這個資料必須有值

  validates :title, presence: true
  # content最多只能200字

  validates :content, length: { maxmum: 200 }
end

會引發驗證機制的method有createsaveupdate等。正常情況若validate fail時create會把object丟回來,update和save則是會回傳false,倘若想知道錯誤原因,可以在method後加上驚嘆號save!

topic.save
# => false

topic.save!
# => ActiveRecord::RecordInvalid: Validation failed: Title can't be blank

除了上述兩種驗證方式,尚有驗證文字格式、數字、唯一等等,可以查看rails guide得到更多資訊:
http://guides.rubyonrails.org/active_record_validations.html

scope

scope的作用就是將時常使用或是複雜的ORM語法組合成懶人包,這樣下次要用的時候只要把懶人包拿出來就可以了,舉例說明:

model/topic.rb
class Topic < ActiveRecord::Base
    scope :recent, order("created_at DESC")
end

上面這段code我們定義了recent這個scope,以後我們只要下recent這個指令就等於下order("created_at DESC")是一樣的。如此一來就可以讓程式碼更為簡潔。

ids

collection_singular_ids是當我們做has_many時產生的method
假設我們有DrinkMaterial兩個many-to-many的model
並希望使用者在新增Drink的時候可以同時選取Material做關聯
這時候的做法就是讓被選取的materials的id存成array,經由params[:material_ids]傳給drink controller
rails就會根據material_ids幫我們建立關聯

補充:
記得要在drink_controller.rb的strong params內加上material_ids: []
使用simple_form時只要下f.association就可以了

<%=  simple_form_for @drink do |f| %>
  <%= f.association :materials, :as => :check_boxes %><br>
    <%= f.submit "Submit" %>
<% end %>

其他可參考的補充資料:
http://railscasts.com/episodes/17-habtm-checkboxes
http://guides.rubyonrails.org/association_basics.html#has-many-association-reference

collect(&:id)

據說跟map是一樣的東西
http://stackoverflow.com/questions/5254732/difference-between-map-and-collect-in-ruby
http://rubyinrails.com/2014/01/ruby-difference-between-collect-and-map/

collect做的事情就是將array內的資料一一拿出來處理,處理完後再傳回array
所以topics.collect(&:id)就是把topics內所有的id都拿出來存成另一個array
ex:

topics = [{id:1, title:"topic1"}, {id:2, title:"topic2"}]
topics.collect(&:id)
# => [1,2]

# 假設我們要讓id都變成浮點數 (雖然我想不到理由為什麼要變成浮點數,XDrz)

# 我們可以這樣下指令:

t.collect{|t| t.id.to_f }
# 或是更簡單一點:

t.collect(&:id).collect(&:to_f)
# 回傳都會是一樣的

# => [1.0,2.0]

includes

當我們操作關聯性資料時,可以使用includes將關聯資料先抓出來,這樣每次要調用資料時就不是去資料庫搜尋,而是從已經調閱出來的資料搜尋,可以提高效能。舉例說明:

假設Boardhas_manyTopics
原本我們會這樣寫

class Board < ActiveRecord::Base
  def
    @boards = Board.all
  end
end

這種情況下若我們在view裡面寫@board.topics,假設我們共有10個board,系統會這樣幫我們找資料:
1.到資料庫裡找board_id = 1 的topic
2.到資料庫裡找board_id = 2 的topic
.
.
.
10.到資料庫裡找board_id = 10 的topic
這樣的做法有個很弔詭的地方在於我們重複搜尋了資料庫10次,造成效能問題。

若加上includes後會是這樣:

class Board < ActiveRecord::Base
  def
    @boards = Board.includes(:topics).all
  end
end

這種時候我們若在view裡面寫@board.topics,假設我們共有10個board,系統會這樣幫我們找資料:
1.到資料庫裡把topic全部抓出來
2.從抓出來的topic中找到board_id相對應的topic
這樣做我們就只需要搜尋一次,直接減少了資料庫的負擔。

counter_cache

counter_cache是在做關聯性資料庫時計算資料量的一個方法
照樣舉Boardhas_manyTopics的例子來看
正常我們會寫類似下方的code來算出這個Board內有多少個Topics

@board.topics.count

這代表每次我們要算數量的時候都得下一個sql指令去算有多少topics,一樣是會造成效能問題。
所以rails內建了counter_cache的方法
我們只要在topic model內加上counter_cache: true

class Topic < ActiveRecord::Base
    belongs_to :board, counter_cache: true
end

然後在board的資料庫內新增一個叫做topics_count的欄位
這樣以後當這個Board內的topic有增減的時候,rails就會幫我們增減topics_count的欄位,如此一來下次再下@board.topics.count的指令時rails就會預設去找topics_count的欄位,不用重新下sql指令計算。

class Topic < ActiveRecord::Base
    # 我們也可以覆蓋rails的預設規定,自己定義要增減的欄位名稱,此例為count_of_topics

    belongs_to :board, counter_cache: :count_of_topics
end

STI

全名:單一表格繼承 STI(Single-table inheritance)
rails guide說明片段:a way to add inheritance to your models
簡單來講就是讓繼承的submodel可以擁有父類別的表格欄位且繼承父類別的方法
在rails慣例中只要加上type這個欄位在父類別的資料庫中就可以了
例:User有分Native跟Foreigner

class User < ActiveRecord::Base
end
class Native < User
end
class Foreigner < User
end

這樣就可以了,可以新增Native User

native = Native.create(:name => "foobar")
native.type # => "Native"

STI的使用時機是當我們需要一個擁有一樣特性但是不同行為的model時才使用。

待參考資料:
http://thibaultdenizet.com/tutorial/single-table-inheritance-with-rails-4-part-1/

Polymorphic Assoiciaion

請參考此篇:
http://waynechu.logdown.com/posts/201614-polymorphic-associations

不要把该放在 helper 的东西放在 model 里

研究該怎麼寫。

refactor controller code to model

把跟資料庫有關的操作都搬回model,舉例如下:

1.把應該是在model內的邏輯搬回去

舉例:當user發佈post時就可以根據內容多寡得到虛擬幣,最多500元,controller中寫法如下

post_controller.rb
def publish
  @post = Post.find(params[:id])
  @post.update(:is_published => true)
  if @post.content.size < 500
    @post.owner.money += @post.content.size
  else
    @post.owner.money += 500
  end
  redirect_to post_path(@post)
end

refactor後:

post_controller.rb
def publish
  @post = Post.find(params[:id])
  @post.publish
  redirect_to post_path(@post)
end
model/post.rb
def publish
  self.update(:is_published => true)
  if self.content.size < 500
    self.owner.money += self.content.size
  else
    self.owner.money += 500
  end
end
2.多利用scope,把ORM的邏輯都放回model(見上方scope介紹)

(見上方scope介紹)

3.讓ActiveRecord建立關聯而不是自己土法煉鋼,直接看範例:
class PostsController < ApplicationController
  def create
    @post = Post.new(params[:post])
    @post.user_id = current_user
    @post.save
  end
end

Refactor後:

class PostsController < ApplicationController
  def create
    @post = current_user.posts.build(params[:post])
    @post.save
  end
end

class User < ActiveRecord::Base
  has_many :posts
end

參考資料

4.Use scope access

scope access可以讓我們避免掉許多不必要的判斷,請直接看例子,此例使用current_user就可以省去判斷user的情況。

Refactor前:

class PostsController < ApplicationController
  def edit
    @post = Post.find(params[:id])
    if @post.user != current_user
      flash[:warning] = 'Access denied'
      redirect_to posts_url
    end
  end
end

Refactor後:

class PostsController < ApplicationController
  def edit
    @post = current_user.posts.find(params[:id])
  end
end

參考資料

5.Add model virtual attribute (尚未實作過)

有時候由於表單的設計方式與model內的attribute不同,我們會需要將表單的資料拆解後再送給資料庫,例如資料庫內有'first_name'和'last_name'但是表單只有'full_name'

不好的寫法:
在controller裡面將params的資料切開,並存進資料庫

<% form_for @user do |f| %>
  <%= text_field_tag :full_name %>
<% end %>

class UsersController < ApplicationController
  def create
    @user = User.new(params[:user])
    @user.first_name = params([:full_name]).split(' ', 2).first
    @user.last_name = params([:full_name]).split(' ', 2).last
    @user.save
  end
end

比較好的寫法:

class User < ActiveRecord::Base

    # 這個method可以讓表單以後要讀取full_name的attribute時可以拿到資料

  def full_name
    [first_name, last_name].join(' ')
  end
    # 這個method讓post回來的full_name拆解成first_name和last_name儲存

  def full_name=(name)
    split = name.split(' ', 2)
    self.first_name = split.first
    self.last_name = split.last
  end
end

<% form_for @user do |f| %>
  <%= f.text_field :full_name %>
<% end %>

class UsersController < ApplicationController
  def create
    @user = User.create(params[:user])
  end
end

如此一來controller變得超乾淨!!
參考資料

待研究:
6.Use model callback
參考資料

7.Replace Complex Creation with Factory Method
參考資料

8.Nested Model Forms

http://waynechu.logdown.com/posts/201843-nested-model-form

參考資料

← [Rails 高級新手系列] 關於MVC-什麼東西應該放在Controller [Rails 高級新手系列] 關於Ruby →
 
comments powered by Disqus