Ruby on Rails學習筆記

11
Nov

前陣子寫了兩篇關於建立好友名單的文章: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 :o 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 :o wnerships
  end
end

Model的程式如下,簡單來說就是透過第三個Model來記錄兩個Model之間的關係:

# app/models/item.rb
class Item < ActiveRecord::Base
  has_many :o wnerships
  has_many :users, :through => :o wnerships
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :o wnerships
  has_many :items, :through => :o 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

Category : Ruby on Rails學習筆記 | Blog
6
Oct

前言

這篇文章你可以視為「如何在多對多關係中,記錄額外的資訊」。其實一般想要在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,一樣是拿上一篇的程式,多了三行程式:

  • attr_accessor :description,由於ActiveRecord的Class在mapping到資料庫時,是直接對應到table的column,但目前users table沒有description這個column,因此我們在此給User這個class一個屬性,等一下在建立好友關係時就可以一併連好友描述都塞到friendships裡面。
  • :insert_sql,因為除了ActiveRecord會幫我們找到foreign_key所屬的欄位之外,我們還需要塞入額外的資訊,因此自訂語法。
  • after_find的定義,由於ActiveRecord撈出資料後,User這個model本身沒有description欄位,所以我們在撈出好友關係的時候,再把description寫到剛剛建立好的屬性。
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之類的。

References

Category : Ruby on Rails學習筆記 | Blog
6
Oct

網站想要開發「好友名單」功能時,會新增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取得好友人數

Category : Ruby on Rails學習筆記 | Blog
6
Oct

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
Category : Ruby on Rails學習筆記 | Blog
18
Sep

利用ActiveRecord提供的dependent選項處理資料相依性

最近在ptt的Ruby板,有板友在討論刪除資料時如何進行驗證,原po的問題大致上是說倘若某一筆資料跟其他的資料有所關聯,該如何顯示錯誤訊息給使用者、告知因為有資料的關聯參照存在所以無法刪除。

看到這個問題,我心中第一個想到的是「就用CASCADE的方式把資料一口氣刪光啊!」XD不過隨即想想,大多數的資料可不是說想刪就能隨便刪的,例如在Ruby板上的討論串就有人提到要刪除某個分類名稱,但在刪除前要先將該分類底下的文章都指定為未分類。

總之,好奇心驅使之下,我猜想ActiveRecord裡面一定有針對關聯性特別設計一套機制來處理,果不其然其實在has_one, has_many底下就有:dependent來處理互相參照的資料。

:dependent有三個選項:(在此假設User has_many Posts, Post has_many Comments)

class User < ActiveRecord::Base
  has_many :posts, :dependent => :destroy
end

  • :destroy:呼叫User.destroy時,ActiveRecord會呼叫Post.destroy來達到刪除Posts的目的,也就是與該Posts相關聯的comments也會被一併刪除。(除非Post與Comments之間的關係是nullify)
  • :delete_all:呼叫User.destroy時,僅刪除User本身及該user的posts。
  • :nullify:不刪除關聯的物件,僅將User的posts之Foreign Key改為NULL

至於要在物件刪除之前,若要做驗證動作,就如同ptt Ruby的板主godfat分享的一樣,使用"before_destroy"可以在呼叫destroy之前先做其他動作。例如在這個網頁裡面看到的sample code(before_destroy是寫在Model裡,destroy的動作則是由controller中的action來呼叫):

class Group < ActiveRecord::Base
  acts_as_tree :o rder => "name"

  has_and_belongs_to_many => :users

  before_destroy :validates_no_dependents

  def validates_no_dependents
    if children.size > 0 || users.size > 0
      errors.add :base, "Cannot delete this group, as it has sub-groups and/or users"
    end
  end
end

Category : Ruby on Rails學習筆記 | Blog