Posted by (0) Comment
我曾寫過一篇「Rails: 建立Permalink,避免流水號洩漏網站資料」針對有人擔心(或質疑)Ruby on Rails中以資料表主鍵流水號作為URL中的參數,會導致網站洩漏某些不希望被外界一眼看穿的資訊,例如使用者總數或文章總數之類的數據,因此簡單地透過Model在存取資料庫時的before_create方法建立每一筆資料的unique key來避免這個問題。
之前使用Rails 1.2.x版本時根據我當時的作法並無不妥,因為我自己的coding習慣是會完整地指定controller, action以及id,並在對應的action中使用find_by_key的方法來找到資料。
然而,升級到Rails 2.0後便會出現許多麻煩,例如預設產生的scaffold必須修改許多地方,才能讓Model.find_by_key、redirect_to post_url(@post)之類的方法正確運作。
於是我一直到前陣子才找到應該比較正規的作法:在Model中使用to_param方法。
to_param(): Enables Active Record objects to be used as URL parameters in Action Pack automatically.
這麼好用的方法我居然不是一開始就發現,只能說相見恨晚,好險我開發Rails 2.0的時間還不算長,沒走到太多冤枉路。要實踐本標題所說的在Rails 2.0中如何避免在URL使用資料流水號便相當簡單!
1. 建立Scaffold:(通常會對key這個欄位建立index並設定字串長度limit,別忘了:p)
script/generate post title:string content:text key:string
2. 將Post的migration寫進資料庫後修改Post model如下:
class Post < ActiveRecord::Base
before_create :generate_key
def self.find(*args)
if args.first.is_a?(String)
find_by_key(args.shift, *args) or raise ActiveRecord::RecordNotFound
else
super
end
end
def to_param
key
end
protected
# 我前一篇文章產生key的方式不太一樣,這應該是個人習慣。
# 另外,其實亂碼的網址沒有SEO的效益,建議還是弄個slug吧!
def generate_key
require 'digest/sha1'
self.key = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )[15..24]
end
end
就這樣!概念很簡單,就是不再用ID當查詢的參數,全面改用key啦!關鍵就在於使用to_param()方法以及對find做進一步的判斷,因為我除了Model.find(:all)之類的Symbol,其餘的CRUD都是傳入Key作為參數,因此在此僅簡單地判斷是否為字串。
如此一來,不管你是什麼user_posts_url, new_user_post_url都可以輕鬆地直接以ActiveRecord Object傳遞,起碼我之前曾經嘗試用edit_post_url(@post.key)這種方式傳值,非常麻煩、要改Controller又要改View,現在用了to_param(),什麼都不用改了!又優雅又簡單,寫起來更加快樂、效率也提昇了:p
P.S. 其實在Rails 1.2.x應該也是這麼做比較優雅,不限於2.0
Posted by (0) Comment
前陣子寫了兩篇關於建立好友名單的文章:Rails: 建立好友名單、Rails: 建立好友名單(續)加上好友描述,文中有提到,除非有特殊需求,否則建議建立多對多關係請盡量使用has_and_belongs_to_many(habtm)或has_many :through的方式來建立。
我個人比較常用到的是has_many :through,也就是本篇的主題。
本範例將建立物品清單管理,每個使用者擁有多個物品,例如A擁有電腦、手機、相機;使用者設定物品清單的時候可以建立描述,例如紀錄購入時間、價格或是其他文字描述等等。
首先建立兩個Model分別為User、Item,分別對應到Users以及Items資料表,另外建立記錄兩者relationship的Model及Table,使用migration來建立內容如下:
# 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 :created_at, :datetime
end
end
def self.down
drop_table :users
end
end
# db/migrate/002_create_items.rb
class CreateItems < ActiveRecord::Migration
def self.up
create_table :items do |t|
t.column :name, :string, :null => false
t.column :description, :text
t.column :created_at, :datetime
end
end
def self.down
drop_table :items
end
end
# db/migrate/003_create_ownerships.rb
class CreateOwnerships < ActiveRecord::Migration
def self.up
create_table
wnerships do |t|
t.column :user_id, :integer, :null => false
t.column :item_id, :integer, :null => false
t.column :description, :text
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
def self.down
drop_table
wnerships
end
end
Model的程式如下,簡單來說就是透過第三個Model來記錄兩個Model之間的關係:
# app/models/item.rb class Item < ActiveRecord::Base has_manywnerships has_many :users, :through =>
wnerships end # app/models/user.rb class User < ActiveRecord::Base has_many
wnerships has_many :items, :through =>
wnerships end # app/models/ownership.rb class Ownership < ActiveRecord::Base belongs_to :item belongs_to :user end
如此一來,就可以用下列指令來增加User, Item,並且可以查詢到某User所有的items,或是
擁有某item的users。
peter = User.create(:name => "Peter") deduce = User.create(:name => "Deduce") phone = Item.create(:name => "phone") camera = Item.create(:name => "camera") peter.items << phone peter.items.count phone.users.count
Posted by (4) Comment
這篇文章你可以視為「如何在多對多關係中,記錄額外的資訊」。其實一般想要在M:N之間的關係記錄額外的資訊,應該是會透過has_many :through比較恰當,不過因為當初我在寫好友名單時,先用了上一篇文章的寫法,所以才找到本篇文章要談的作法。
簡單來說,如果要記錄的資訊只有一個欄位,用本篇作法是快又有效;否則,我認為還是用has_many :through應該比較方便。後者的作法下次再談。
常見的社群網站,建立好友之間的關係不外乎是加入好友、並通知對方,似乎比較少有可以替好友分群組、加上描述的功能,其中丁丁大站就是可以替好友們加上描述以及分組的。這篇文章就是教你如何像丁丁大站一樣,用簡單的程式建立好友關係並加上簡單的描述。
首先一樣是你必須有一張記錄Friendship的Table,偷用上一篇的Migration(這樣上一篇瞬間變得毫無意義了-.-,不過我的想法是,一般人應該只會用到上一篇的作法。)。注意,與上一篇不同的是,這裡多了description欄位
class AddFriendship < ActiveRecord::Migration
def self.up
create_table :friendships, :id => false do |t|
t.column :user_id, :integer, :null => false
t.column :friend_id, :integer, :null => false
t.column :description :string
end
end
def self.down
drop_table :friendships
end
end
接下來一樣是定義model,一樣是拿上一篇的程式,多了三行程式:
class User < ActiveRecord::Base
attr_accessor :description
has_and_belongs_to_many :friends,
:class_name => "User",
:join_table => "friendships",
:association_foreign_key => "friend_id",
:foreign_key => "user_id",
:insert_sql => 'INSERT into friendships (user_id, friend_id, description) VALUES (#{id}, #{record.id}, \'#{record.description}\')'
# 要特別注意引號的使用,尤其description通常都是string,所以用單引號框起來
def after_find
self.description = self["description"]
# 說實在這邊我也不是很確定寫法是否正確XD應該說可以work,但不知道有沒有更好的寫法:p
end
end
如此一來,你就可以用下列的程式來建立、取得好友及描述
u = User.create(:name => "deduce") k = User.create(:name => "punk") u.description = "Rails愛好者:deduce" k.friends << u unless k.friends.include?(u) # 輸出好友描述 k.friends.each do |friend| puts friend.description end
註:description的字串丟進去之前要記得先處理,避免發生不必要的問題,例如SQL injection之類的。
Posted by (4) Comment
網站想要開發「好友名單」功能時,會新增Table來記錄好友之間的關係,在Rails應該怎麼做呢?假設目前系統已經有User這個Model,我們將會建立 User has_many friends的關係來記錄每個User擁有的好友們。
首先新增一張Table來記錄Friendship(請善用Migration):
class AddFriendship < ActiveRecord::Migration
def self.up
create_table :friendships, :id => false do |t|
t.column :user_id, :integer, :null => false
t.column :friend_id, :integer, :null => false
end
end
def self.down
drop_table :friendships
end
end
接下來在User Model中定義好友關係:
class User < ActiveRecord::Base
has_and_belongs_to_many :friends,
:class_name => "User",
:join_table => "friendships",
:association_foreign_key => "friend_id",
:foreign_key => "user_id"
end
如此一來,你就可以用下列語法來建立使用者之間的好友關係:
u = User.create(:name => "deduce") k = User.create(:name => "punk") u.friends << k if not u.friends.include?(k) # 如果 u 的好友不包含 k 則加入好友,不需要另外進行儲存的動作
u.friends取得好友名單、u.friends.count取得好友人數
Posted by (2) Comment
XDite曾經在「以 ROR 打造網站,設計盲點所引發的惡搞危機」這篇文章中提到Rails的scaffold所建立出來的程式(或說開發者很習慣直接以id操作Model),由於在URL上是直接以流水號的方式呈現,我們便可以利用一些簡單的程式爬完某個網站的特定頁面,來取得所有的文章、所有的使用者頁面。
事實上XDite應該是有在另外一場活動提過因應的對策,雖然我沒抓下來聽,不過我猜應該是改變Controller的設計方式,以亂碼或是其他方式來產生URL。
首先在Table裡面加上「permalink」這個column,如果你是要在既有的table上新增,可以使用migration來新增欄位(當然你也可以砍掉重練XD)
class AddPermalink < ActiveRecord::Migration
def self.up
add_column :posts, :permalink, :string
add_index :posts, :permalink, :unique => true
end
def self.down
remove_index :posts, :permalink
remove_column :posts, :permalink
end
end
接下來在Model裡面定義permalink的產生方式
class Post < ActiveRecord::Base
before_create :generate_permalink
protected
def generate_permalink
# 定義permalink的產生方式
self.permalink = Time.now.strftime("%Y-%m-%d_") + rand(100000).to_s
end
end
這邊我是用時間加亂數的方式產生permalink,事實上這樣的寫法還是有可能重複,不如用SHA或MD5的方式來產生Permalink也不錯(雖然長了點)。
class Post < ActiveRecord::Base
before_create :generate_permalink
protected
def generate_permalink
# 用標題+時間進行md5的編碼
require 'digest/md5'
self.permalink = Digest::MD5.hexdigest(self.title + Time.now.to_s)
end
end
往後在連結可以使用:
link_to post.title,
:controller => "post",
:action => "show",
:id => post.permalink
在Controller裡面Action的寫法則是:
class PostController < ApplicationController
def show
@post = Post.find_by_permalink(params[:id])
end
end