[Rails學習心得] ActiveRecord:定義Model之間的關係

上一篇文章裡稍微提到了ActiveRecord能讓你不需寫SQL語法便能快速地存取資料,(事實上我只提到了讀取的部份,寫入其實也相當簡單,依樣畫葫蘆,只是使用的method不同罷了)這篇便來談談在ActiveRecord裡,是如何處理Model與Model之間的關係。

定義Model之間的關係

平常我們在描述兩個資料表之間的關係,通常可能會說兩張table是一對一、一對多、或是多對多,而在ActiveRecord裡,兩個Model之間的關係主要以上述的三種方式來定義。

延續上一篇文章中我所建立的三個model: User, Video, and Comment,這三個model之間的關係如下:

  • User has_many videos:User可以擁有多個Videos
  • User has_many comments:User可以擁有多個Comments
  • Video belongs_to user:Video必然會屬於某個User
  • Comment belongs_to user:Comment也會屬於某個User
  • Video has_many comments:每個影片都可以有多筆Comments
  • Comment belongs_to video:每個 comment也會屬於某個Video

看這樣的描述,如果是尚未接觸過Ruby on Rails或是ActiveRecord的讀者一定會感到疑惑,根本聽不懂我在說什麼嘛!請看下列三段程式碼,分別是上述三個Model的程式:

#Model: rails_app/app/models/user.rb
class User < ActiveRecord::Base
  has_many :videos
  has_many :comments
end
#Model: rails_app/app/models/video.rb
class Video < ActiveRecord::Base
  has_many :comments
  belongs_to :user
end
#Model: rails_app/app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :user
  belongs_to :video
end

看出什麼了嗎?只要使用很直觀的has_many, belongs_to這樣的method便可以輕鬆定義Model之間的關係。

定義資料表欄位:使用Migration

接著,我們還必須仰賴migration替我們新增對應的資料表或欄位,在上一篇文章中已經完成了User migration的程式:

#Migration: rails_app/app/db/migrate/001_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :name, :string, :null => false
      t.column :email, :string, :null => false
      t.column :password, :string, :null => false
      t.column :nickname, :string, :null => false
      t.column :memo, :text
      t.column :created_at, :datetime
      t.column :updated_at, :datetime
    end
  end

  def self.down
    drop_table :users
  end
end

在上一篇文章中,除了self.up、self.down兩個method之外,我並沒有交代其他程式碼所代表的意義,這邊稍微補充一下

  • create_table代表建立一個新的資料表。透過對資料表進行管理的method尚有:deop_table, add_column, remove_column, rename_column等等。
  • create_table :users代表建立一個名為users的資料表,其後開始是一個block
  • t.column :name, :string, :null => false分別代表"table中的欄位"、"欄位名稱"、"資料型態"、"不可為null",這樣的寫法在Ruby是很常見的寫法,假設你不懂Ruby、但卻對Rails有興趣,我建議你可以透過在學習Rails的過程中慢慢熟悉Ruby語言的寫法,我相信很快就上手。
  • 有關於建立欄位時的相關選項,可以參考Rails的文件,或是往後的文章或許會用到,我會再一一介紹。
  • 別忘了created_at, updated_at是ActiveRecord中的Magic Column Name,它可以幫你在對資料表寫入資料的時候,自動產生資料建立或修改的時間。(雖然說寫入時間這種事情只要一行就可以解決)

接下來我們要繼續完成與Video、Comment這兩個model相關的migration。migration檔案通常是由generator在建立model的同時,就會自動新增一個migration的檔案,而generator也可以單獨產生一個migration file讓你對資料庫的schema進行改變。

#Migration: rails_app/app/db/migrate/002_create_videos.rb
class CreateVideos < ActiveRecord::Migration
  def self.up
    create_table :videos do |t|
      t.column :title, :string, :null => false
      t.column :description, :text
      t.column :user_id, :integer
      t.column :created_at, :datetime
    end
  end

  def self.down
    drop_table :videos
  end
end
#Migration: rails_app/app/db/migrate/003_create_comments.rb
class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.column :content, :text
      t.column :user_id, :integer
      t.column :video_id, :integer
      t.column :created_at, :datetime
    end
  end

  def self.down
    drop_table :comments
  end
end

從上面的Migration可以發現下列兩點:

  • Migrate目錄裡面的檔案,檔名會依001, 002, 003...的次序為開頭,這代表migration的版本。例如你開發到第10版時,發現第十版新增的table或欄位是有錯的,你可以馬上將資料庫的schema還原到第九版。
  • 上一篇文中我有提到,Magic Column Name中包含了「xxxx_id」這樣的格式,因此在video、comment的table中可以看到出現「user_id」、「video_id」這樣的欄位,往後ActiveRecord便是用這個欄位來替你找到對應的資料(自動幫你join table)

將Migration定義的內容寫入資料庫

從上一篇文章透過generate指令來產生model、migration files,到本篇文章我們實際去修改上述的檔案內容,如果熟練大概三分鐘就可以完成,接下來便可以開始感受到ActiveRecord所帶來的高生產力與享受:D

做完上述動作之後,我們便要將定義好的table schema寫入資料庫裡面,在Rails的目錄結構裡面,config/database.yml便是在定義資料庫的相關設定值。設定資料庫的動作一般是在建立好Rails Project便該完成的,這算是開發Rails project的前置動作之一,那為甚麼我到現在才說咧?其實只是我忘了先講怎麼建立專案、怎麼設定資料庫罷了.....以後如果要把這個系列彙整起來給Rails新鮮人看,再寫一篇好了XD

關於Rails Project的前置動作可以參考午夜盧比人Winson寫的「My Rails Way」,裡面提到的技巧都相當實用,像是第一點提到的

為了節省時間,我通常會維護一個叫做sample的Rails project,裡面把簡易的帳號、密碼、登入做起來,以及設計一個model,也把他的tag、comment功能作起來,並把相關的測試案例寫好、常用的外掛裝起來,然後整包壓成一個sample.zip檔。

倘若資料庫已經設定好,接下來會使用到「rake」指令來將我們規劃好的schema寫入資料庫裡面,指令如下:
rake db:migrate
通常會得到下面的結果:

deduce@nccu:~/test$ rake db:migrate
(in /home/deduce/test)
== CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0170s
== CreateUsers: migrated (0.0174s) =============================

== CreateVideos: migrating =====================================
-- create_table(:videos)
   -> 0.0090s
== CreateVideos: migrated (0.0094s) ============================

== CreateComments: migrating ===================================
-- create_table(:comments)
   -> 0.0089s
== CreateComments: migrated (0.0093s) ==========================

如果沒有出現錯誤訊息就代表資料表皆成功建立,接下來我們便可以開始使用ActiveRecord來對資料庫進行各種關聯式的存取動作!

註:實在是有點拖稿,不過我寫這些文章是希望可以盡量淺白,所以盡量詳細、盡量簡單,一樣,有任何錯誤或建議請多指教。

[Rails學習心得] ActiveRecord:存取資料庫不用寫SQL語法?

ActiveRecord:存取資料庫不用寫SQL語法?

首先,Ruby on Rails對我而言,最大的衝擊便是Rails中的ActiveRecord實作了ORM (Object-relational mapping),讓程式對資料庫進行存取的動作時,程式設計師不需要寫任何一行SQL語法,就可以存取到特定的資料表並找到自己想要的資料,沒接觸過Rails或是ActiveRecord的人,會認為:「真的有這麼神奇嗎?」

我可以告訴你,是的,真的就這麼神奇!,90%的一般性需求不需要寫SQL語法,而剩下的10%特殊需求,Rails又提供給你充足的彈性讓你自訂SQL語法對資料庫進行存取。

如此的作法正符合了Convention Over Configuration的精神,RoR裡面使用大量的慣例來方便程式設計師進行開發,當慣例無法滿足需求時,她同時間會提供足夠的空間讓你自訂想要的功能。

舉例來說,假設要開發一個類似Youtube的網站,資料庫中起碼會有這三個資料表:

  • users:儲存使用者資料
  • videos:儲存影片的基本資料、發表時間(此例不考慮影像壓縮、儲存技術XD)
  • comments:儲存使用者發表的影片評論

在Rails中的作法,通常會使用下列的語法來建立與資料表相對應的三個Model

ruby script/generate model user
ruby script/generate model video
ruby script/generate model comment

"generate"指令除了會產生上述的三個Model,同時還會產生三個對應的migration,migration中文通常翻作遷移,概念是資料庫的版本演進由migration來控制,包含新增、刪除資料表,或是新增欄位、刪除欄位、欄位類型變更、欄位名稱變更、欄位屬性變更等等,都可以在migration裡面完成。

由migration來進行資料表的管理,優點是無須在意後端使用的資料庫系統為何,migration會自動幫你產生對應到不同資料庫的資料型態及語法,因此無論後端是MySQL、MSSQL Server、DB2、Oracle,ActiveRecord的Migration都可以替你完成資料表的建置與管理。(事實上可能不是每個資料庫系統都有100%的支援。)

由generate指令所產生的migration檔案通常如下:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
    end
  end

  def self.down
    drop_table :users
  end
end

上述的程式碼會對資料庫進行新增table的動作,可以發現其中定義了self.up及self.down兩個method,這代表當程式設計者對於目前對資料庫進行的修改感到不滿意時,可以馬上回到前一個版本,此時migration便會自動執行self.down的程式;換句話說,migration裡面定義的是兩個反向的動作,例如:新增table、移除table;新增欄位、移除欄位。

接下來我將這個CreateUsers的migration修改如下:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column :name, :string, :null => false
      t.column :email, :string, :null => false
      t.column :password, :string, :null => false
      t.column :nickname, :string, :null => false
      t.column :memo, :text
      t.column :created_at, :datetime
      t.column :updated_at, :datetime
    end
  end

  def self.down
    drop_table :users
  end
end

我定義了七個欄位給users這張table,如此的定義方式,在接下來的開發過程將會有幾個重要的效果:

  • ActiveRecord的慣例是自動為每個Table建立「id」這個欄位,作為Primary Key以供存取;換句話說,表格內實際上會有我定義的七個欄位加上id共八個欄位。當然,這是慣例,你不想要id這個欄位也是可以的:D
  • 接下來你可以使用User.find(1)來找到在users資料表中id=1的資料,背後其實是自動幫你完成一段SQL語法:"SELECT * FROM users WHERE users.id=1"
  • 當然,你可以使用User.find_by_name("deduce")或是User.find_by_email("[email protected]")這樣的方式來找到特定的資料,背後實際上是幫你轉換成類似"SELECT * FROM users WHERE users.email = '[email protected]'"這樣的語法。這在Ruby裡面是Dynamic Methods的概念,她會根據class definition去產生對應的class method來方便使用。
  • 另外Find method也可以用User.find(:all, :conditions => "搜尋條件", :o rder => "排序條件")來找資料,絕大多數的資料存取都可以靠這樣的方式完成,其他的可以靠User.find_by_sql("SQL語法")來完成:)
  • 接著,下面這段程式可以簡單地在網頁上show出某個user的基本資料
    <% user = User.find(1) %>
    帳號:<%= user.name %>
    電子郵件:<%= user.email %>
    暱稱:<%= user.nickname %>
    何時註冊?<%= user.created_at %>
    

    如何?是不是非常簡單而且非常直觀:)

  • 此外,注意到兩個欄位:created_at及updated_at,這樣的欄位名稱在ActiveRecord中稱為「Magic Column Names」,欄位只要是以Magic Column Names來命名,在資料寫入Table的同時,ActiveRecord會自動幫你產生相關的資料。以created_at來說就是資料的建立時間,而updated_at則是資料的修改時間。常見的Magic Column Names還有created_on, updated_on(_at與_on的差別在於一個有記錄詳細時間、一個只記錄日期),xxxx_id則是預設的Foreign Key名稱,例如我們稍後會在video及comment兩個table建立user_id當作Foreign Key來reference到user table,如此便可以得知某個video或comment是屬於哪個user。
  • 想統計目前站上有多少註冊會員?簡單,網頁上輸出一行<%= User.count %>便是目前的會員數了,或是想一口氣輸出全站User的資料,可以用如下寫法:
    <% users = User.find(:all) %>
      <% users.each do |user| %>
      User序號:<= user.id %>
      帳號:<%= user.name %>
      電子郵件:<%= user.email %>
      暱稱:<%= user.nickname %>
      何時註冊?<%= user.created_at %>
    <% end %>
    

    (Ruby也可以用 for user in users的寫法,不過我比較習慣用.each)

事實上,要深入談ActiveRecord的話,是可以寫成一本書的,最近才剛出版的一本新書「Pro ActiveRecord: Database with Ruby on Rails」內容便是在探討ActiveRecord的深入技巧,Amazon上的Book Description中有一段話是這麼說的:

Because ActiveRecord is configured with default assumptions that mesh perfectly with the Rails framework, Rails developers often find they hardly need think about it at all. However, if you are developing in Ruby without Rails, or are deploying against legacy databases designed without Rails in mind, or you just want to take advantage of database-specific features such as large objects and stored procedures, you need the in-depth knowledge of ActiveRecord found in this book.
大致上是說,身為一個Rails developer,由於Rails跟ActiveRecord有著許多的慣例(configured with defaul assumptions),因此我們通常不太需要想太多,反正用就對了。然而如果身為一個Rails developer,或是你想單獨拿ActiveRecord來開發其他與資料庫有關的project,就需要有更多深入的瞭解,這也是這本書的主要目的。

我想這或許會是一本值得購入的好書吧?:D

這篇文章簡單地介紹了ActiveRecord如何讓Rails developer輕鬆地存取資料,下一篇再來談Model與Model之間要如何運作,文中有任何錯誤或對本文有任何指教請留言或來信告知(deduce_at_gmail_dot_com),感激不盡!

10分鐘內開發出Twitter網站:使用Ruby on Rails

剛剛在訂便當被買走了這篇文章提到,用Ruby on Rails可以很快地開發出一個網站,到底有多快呢?;事實上如果已經聽過或是稍微接觸過Ruby on Rails的朋友,應該都有聽說關於RoR的高生產力傳說,例如thegiive去年曾經po過的幾篇文章,都讓我學習Rails的心更加堅定XD像是他曾經提到

根據 Beyond JAVA 裡面,Justin Gehtland 用 Rails 重寫一個用 JAVA Spring/Hibernate 寫好的 Project,他發現程式碼比例 JAVA :Rails 大概是 3.48 : 1 。附帶一提,他重寫的開發時間開發時間比是 16 : 1,更噁心的數字。

image當然我後來也去弄了一本Beyond Java來看(有出中文翻譯本,某些學校圖書館或許能借到),對當時的我而言,聽到這麼多RoR的高生產力傳說、又好像可以少寫很多程式碼來達到同樣的目的,我就開始進入了Rails的世界,雖然一年來進步緩慢,不過畢竟是當興趣嘛XD

 

左邊這張圖片大概可以也可以稍微表達為何我熱愛Ruby & Rails,而不乖乖聽老師的話學Java XD

 

anyway,這篇文章的重點應該是標題中提到的「用RoR在十分鐘內開發出Twitter網站」,這是我在日本ZDNet網站上看到的一個短片「Ruby on Railsで10分で作るTwitterもどき」,有興趣的請直接連結到該網站觀看影片,網站上有提供影片中原始碼的下載!

作者用十分鐘左右的時間開發出一個簡單的Twitter雛型,包含了三個Model: User, friendship, status,分別是使用者的個人資料、好友名單的relation以及留下的訊息,光看這樣好像滿簡單的,但要在十分鐘內飆出來也不是那麼簡單。(如果是我就先把程式碼都先放在旁邊準備複製貼上XD)

我想作者可以在十分鐘內就完成這支簡單的程式,除了對Ruby on Rails要夠熟悉之外,還有兩個很重要的關鍵:

    1. TextMate在編輯過程中節省大量的coding時間。
    2. Rails的兩大精神幫了許多忙Don’t Repeat Yourselft, Convention over Configuration

有關TextMate這套編輯器,應該是很多Rails developer的最愛,關於TextMate應該是另外一個故事了,我最近也正式成為Mac User,尚在摸索這套工具所能帶來的高生產力與享受:p

從下篇文章開始,我想分享一些最近開發Rails的感想以及Rails到底在網站的開發過程中幫了些什麼忙。簡單分享個人心得與體會,如果誤會了Rails的精神還請不吝指教:)

「訂便當」被買走了

image

剛剛在Neo’s Blog看到這篇「訂便當被買走了?」,因此馬上連過去訂便當看看,果然網頁的最下方出現了ap-mobile的字樣及客服專線。

關於訂便當這個網站應該還算小有名氣,去年底Mr6還特地寫了一篇訂便當,如何擄獲「她」的心來談訂便當網站,從文中可以看出訂便當對於某些上班族來說是多麼具有吸引力及便利性。

最早知道訂便當這個網站是去年暑假剛接觸到Ruby on Rails時,在網路上到處逛與Ruby & Rails相關的文章,偶然發現有個人為了練習Ruby on Rails而寫了一個簡單的訂便當系統,當時看到作者對於Ruby on Rails的簡要感想就讓我更加想學好這個Framework(儘管作者說他Dirty orz):

Ruby for rails 這本書剛剛入手,看了幾章之後,很想實際寫寫看。我找了個超迷你的 web app 來嘗試看看 – 一個訂便當的系統,含四個 table,四個網頁。

花了兩天,已經開發到堪用的程度了。對 Ruby on Rails 最大的感想是 — Quick and Dirty

Rails 一開始就把整個開發的環境都設好給你了,而且有很多 script 可以省功。在 Java 界裡只有 Appfuse 有做這樣的整合。Rails 預設是用 MySQL,是我最討厭的資料庫之一。二話不說換成 postgreSQL,這當中也花了不少時間摸索… 如果撇開資料庫的設定不談,Rails 一開始馬上就能上工。

其實說起來還挺膚淺的,我是個很懶得coding的人,因此看到這種「似乎」可以讓我在短時間速成一個簡單網站的玩意兒,我是挺心動的。言下之意是後來發現並非如此:p,最近每天跟Ruby on Rails搏鬥,我認為要把這個架構寫得漂亮還是需要一點經驗與技巧,我還太淺了orz

anyway,當時這位用Ruby on Rails開發訂便當系統的Java狂,隨後用spring, hibernate等Java Framework開發了訂便當網站,有興趣的可以看他當初發表的這篇「日常小工具 – 訂便當管理系統~」,剛好這篇文章底下有一段由william發表的comment提到:

你對 Ruby + Rails 的熟悉度,當然比不上對 Java + wicket/spring/hibernate/etc 的熟悉;但換一個角度想想:同樣都會花你兩三天完成一個應用系統(當然啦,在改寫成後者時你多加了 AJAX 及 test),前者的技術要花多少時間來學,後者的技術組合又要花多少時間來學呢(learning curve 很重要)?前者的技術要寫多少程式及設定、同步,後者的技術組合又是如何?

說真的,現在看到這樣的說法實在是心有戚戚焉,事實上我從來沒有去認真寫過什麼Java的程式,我一直都是個.NET developer,我的工作、專題都是以.NET Framework為主,玩Ruby on Rails則是閒暇時的娛樂之一,我認為Ruby on Rails的確是個非常容易入門、容易上手的Framework,比起微軟總是把軟體搞得好像很容易入門、但接下來就不知道該怎麼辦,Ruby on Rails對於開發一般的web網站來說,我認為是相當足夠的,對MVC pattern有概念、對Rails所設定的Controller, Action, Model, View, Helper, Scaffold, Migration等概念有所瞭解之後,很快就可以開發出一個簡單的網站;然而,Ruby on Rails現在有個問題是有可能當你遇到問題的時候,未必找得到解決的方法 :p。

至於ASP.NET,雖然入門容易、提供的元件乍看之下都很簡易,但如果本身對於.NET Framework的熟悉度不夠,我認為學習過程中是非常容易遇到學習瓶頸的,我想這大概就是學習曲線的差異吧:p

References