2013-03-19 55 views
1

我幾乎是Ruby和Rails框架的初學者,這就是爲什麼我決定在做一些打破框架約定的事情之前尋求幫助。使用rails構造ActiveRecord

我有一個相當穩定的OO編程背景,對於初學者 - >中級SQL查詢我非常滿意。然而,我一直在圍繞着Rails提供的ActiveRecord類包裝我的頭。我的直覺是完全拋棄ActiveRecord類,並手工寫出我自己的SQL查詢並將它們包裝在模型中。不過,我知道ActiveRecords是Rails框架的一個相當不可或缺的部分,避免它們將在未來引起我的痛苦。

以下是我的MySQL架構(我將在稍後編寫一個Rails Migration)。我會盡量讓這個問題儘可能簡潔,但是我可能不得不進入一個小背景來解釋爲什麼我已經模擬了架構。我並不過分依賴它,所以如果人們對這個結構有更好的想法,那會很棒。

-- Users table is a minimalized version of what it probably will be, but contains all pertinent information 
CREATE TABLE IF NOT EXISTS users (
    id   INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    name  VARCHAR(20) UNIQUE NOT NULL 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS hashtags (
    id   INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    tag   VARCHAR(30) UNIQUE NOT NULL 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS content_mentions (
    content_id INT UNSIGNED NOT NULL, 
    user_id  INT UNSIGNED NOT NULL, 
    INDEX(content_id), 
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS content_hashtags (
    content_id INT UNSIGNED NOT NULL, 
    hashtag_id INT UNSIGNED NOT NULL, 
    INDEX(content_id), 
    FOREIGN KEY(hashtag_id) REFERENCES hashtags(id) ON DELETE CASCADE 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS content_comments (
    id   INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    user_id  INT UNSIGNED NOT NULL, 
    content_id INT UNSIGNED NOT NULL, 
    text_body VARCHAR(1000) NOT NULL, 
    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 
    INDEX(content_id) 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS polls (
    id   INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    user_id  INT UNSIGNED NOT NULL, 
    question VARCHAR(100) NOT NULL, 
    text_body VARCHAR(1000) NOT NULL, 
    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS poll_options (
    id   INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    poll_id  INT UNSIGNED NOT NULL, 
    content  VARCHAR(150) NOT NULL, 
    active  VARCHAR(1) NOT NULL DEFAULT 'Y', 
    FOREIGN KEY(poll_id) REFERENCES polls(id) ON DELETE CASCADE 
) Engine=InnoDB; 

CREATE TABLE IF NOT EXISTS poll_answers (
    poll_option_id INT UNSIGNED NOT NULL, 
    user_id  INT UNSIGNED NOT NULL, 
    FOREIGN KEY(poll_option_id) REFERENCES poll_options(id) ON DELETE CASCADE, 
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 
    PRIMARY KEY(poll_option_id,user_id) 
) Engine=InnoDB; 

由於架構將表明,這是一個非常基本的網絡民意調查中的應用。每個投票都有多個選項,每個選項可以由不同的用戶提供多個答案。現在,看起來很奇怪的部分是content_*表。我可以解釋這一點的最好方法可能是將其描述爲abstract表格。我從來沒有真正做過這樣的事情,通常這些關係是在兩個或多個顯式表之間,我會根​​據需要添加外鍵。然而,在這種情況下,我可能會得到多種不同類型的content,所有這些都需要哈希標籤/提及/評論。我不知道content_id是指什麼表(代碼將處理它接收的數據),所以我現在只是indexed列。 我需要調整content_*表在一些階段添加type列,因爲一旦存在多個content表,如果兩個表都使用自動遞增主鍵,則可能有重複的content_id條目,我認爲這有點超出問題的範圍雖然。

關於構造ActiveRecord類。第一部分是處理提及/主題標籤的解析。我寫了一個摘要Content類來處理表格的「抽象」一面。它是這樣的(爲簡潔起見,一些解析已被刪除)。

class Content < ActiveRecord::Base 
    self.abstract_class = true; 

    # relationships 
    belongs_to :user 

    has_many :content_mentions; 
    has_many :content_hashtags; 
    has_many :mentions, { :through => :content_mentions, :source => :user, :as => :content }; 
    has_many :hashtags, { :through => :content_hashtags, :as => :content }; 

    # available columns (in the abstract side of things) 
    attr_accessible :text_body, :date_created; 

    # database hooks 
    around_save :around_save_hook 

    # parsing 
    ENTITY_PATTERN = /removed_for_brevity/iox; 

    def render_html() 
     # parsing of the text_body field for hashtags and mentions and replacing them with HTML 
     # goes in here, but unrelated to the data so removed. 
    end 

protected 

    # Is this the best way to do this? 
    def around_save_hook() 
     # save the main record first (so we have a content_id to pass to the join tables) 
     yield 

     # parse the content and build associations, raise a rollback if anything fails 
     text_body.scan(ENTITY_PATTERN) do |boundary,token,value| 
      m = $~; 

      if m[:token] == '@' 
       # mention 
       unless mentions.where(:name => m[:value]).first 
        mention = User::where(:name => m[:value]).first; 
        next unless mention; 

        raise ActiveRecord::Rollback unless content_mentions.create({ :content_id => id, :user_id => mention.id }); 
       end 
      else 
       # hashtag 
       unless hashtags.where(:tag => m[:value]).first 
        hashtag = Hashtag.where(:tag => m[:value]).first; 

        unless hashtag 
         hashtag = Hashtag.new({ :tag => m[:value] }); 
         raise ActiveRecord::Rollback unless hashtag.save(); 
        end 

        raise ActiveRecord::Rollback unless content_hashtags.create({ :content_id => id, :hashtag_id => hashtag.id }); 
       end 
      end 
     end 
    end 
end 

我這裏的主要問題是與around_save_hook,這是分析和保存協會的最佳地點?我該怎麼做,以便如果更新了text_body,並且從原始中刪除了一些hashtags /提及,這些更改將反映在content_*關聯中,而不僅僅是未檢查刪除而添加的新標籤/提及?

ActiveRecord類的其他定義如下:

class Poll < Content 
    has_many :poll_options; 
    has_many :poll_answers, { :through => :poll_options } 

    attr_accessible :user_id, :question; 
    validates :text_body, :presence => true, :length => { :maximum => 1000 }; 
end 

class PollOption < ActiveRecord::Base 
    belongs_to :poll; 
    has_many :poll_answers; 

    attr_accessible :content, :active, :poll_id; 
end 

class PollAnswer < ActiveRecord::Base 
    belongs_to :poll_option; 
    belongs_to :user; 

    attr_accessible :user_id, :poll_option_id; 
end 

class User < ActiveRecord::Base 
    attr_accessible :name; 

    validates :name, :presence => true, :length => { :maximum => 20 }; 
end 

class Hashtag < ActiveRecord::Base 
    attr_accessible :tag; 

     validates :tag, :presence => true, :length => { :maximum => 30 }; 
end 

# Join table for content->users 
class ContentMention < ActiveRecord::Base 
    belongs_to :user; 
    belongs_to :content, { :polymorphic => true }; 

    attr_accessible :content_id, :user_id; 
end 

# Join table for content->hashtags 
class ContentHashtag < ActiveRecord::Base 
    belongs_to :hashtag; 
    belongs_to :content, { :polymorphic => true }; 

    attr_accessible :content_id, :hashtag_id; 
end 

所以我想我的問題如下:

  1. 是模式本身corrent(即是非常低效和不良設計用於軌道?(如果是這樣,關於如何糾正它的建議將是太棒了)
  2. Around Save解析和更新關聯的正確位置?
  3. 我的ActiveRecords是否基於當前模式結構正確設置? (具體而言,我不確定我是否正確使用polymorphic屬性)
  4. 如何在不重新保存輪詢的全部內容的情況下將選項/答案添加到Poll實例中(從而觸發另一個冗餘分析內容),同時仍然保留OOP的方法呢? (即選項/答案通過公共API從Poll模型創建)

它會非常巨大的,如果有人誰與RailsRubyActiveRecord真的很舒服可以運行了我的快速複製他們將如何執行這個的基本框架。正如我所說,我從來沒有使用過ActiveRecord類,所以我甚至不知道這個簡單的代碼將在單個save()調用上觸發多少個原始SQL查詢。

+1

我覺得這個問題有點太大 - 它幾乎要求您的應用程序諮詢。我會說,如果你從Rails入手,那就試試吧,當你犯錯並學習時,你可以回過頭來逐步改進。但是,一個技巧是使用'after_save'而不是'around_save'和'yield'。 – 2013-03-19 10:58:29

+0

我明白這是一個相當大的問題,但我所要求的僅僅是一小撮代碼,其中大部分僅僅是關於在嵌套連接表中正確使用ActiveRecords的一些建議,避免了冗餘更新記錄的需要。應用程序的hashtag /提及/註釋方面與問題無關(除了關於構造「抽象」表的一般問題之外),所以本質上它只是詢問如何使用ActiveRecords實現「投票」,而不是「大」。至於'after_save'方法,我無法找到一個明確的文檔,以確定是否可以回滾它。 – 2013-03-19 11:24:25

+0

你可能是對的,它只是一小撮代碼,但它是對上下文的理解,需要大量的時間投入才能夠提供那些微不足道的代碼。在可能的情況下將其分解成更小的問題會更好。 – 2013-03-19 11:29:34

回答

2

這是一個涉及實施投票/調查應用程序的兩部分railscast。它涵蓋了大部分與模型有關的疑惑。

http://railscasts.com/episodes/196-nested-model-form-part-1

http://railscasts.com/episodes/197-nested-model-form-part-2

我會被壓倒一切的制定者text_body創建任務過程中依賴的對象。

如:

def text_body=(val) 
    write_attribute(:text_body, val).tap do |v| 
    append_new_tags_and_mentions 
    end 
end 

def append_new_tags_and_mentions 
    tag_list, mention_list = extract_tags_and_mentions 
    new_mentions = mention_list - mentions.where(name => mention_list).pluck(:name)  
    mentions.concat(*new_mentions) if new_mentions.present? 
    new_tags = tag_list - hashtags.where(tag => tag_list).pluck(:tag) 
    hashtags.concat(*new_tags) if new_tags.present? 
end 

def extract_tags_and_mentions 
    tag_list = [] 
    mention_list = [] 
    text_body.scan(ENTITY_PATTERN) do |boundary, token, value| 
    if token == "@" 
     mention_list << value 
    else 
     tag_list << value 
    end 
    end 
    [tag_list, mention_list] 
end 

添加驗證檢查的依賴關係。

一般指導原則我希望在使用Java/C++/SQL很長一段時間後在rails中開始編程之前我就已經知道了。

  • 不要用手碼錶生成SQL

  • 使用DB:創建表

  • Rails不支持foregin鍵創建rake任務。你可以通過驗證器強制執行它。

  • 不要使用分號來終止該行。紅寶石的樂趣之一是你沒有終止線。

  • 不要對DSL API參數使用顯式散列。

    使用這個成語

    belongs_to :content, :polymorphic => true 
    

    代替:代替

    belongs_to :content, { :polymorphic => true }; 
    
  • 使用模塊繼承代碼重新使用。

  • 使用each代替for

  • 瞭解陣列上mapreduce(即inject)函數。

+0

非常感謝你的回答,當我回家時我會去看屏幕。不過,我對你提出的一些後來的觀點有一些問題。這可能是由於我對語言有些「更嚴格」的背景。 '不要爲DSL API參數使用明確的散列值。「爲什麼?我討厭ruby的一件事是,它不會在方法參數周圍強制包裝括號,並且它可以讓你在沒有大括號的情況下懶散地編寫散列。至少對於我來說,以這種方式來寫論證使得一眼難以破譯。可能是因爲我對Ruby很陌生,但我喜歡冗長。 – 2013-03-22 05:53:19

+0

至於顯式行結束符;在C/C++,PHP,C#,Javascript等等中,我已經花了很長時間寫這些東西,因爲它是如此根深蒂固,以至於需要努力去記住把它們留下:) – 2013-03-22 05:54:33

+1

我同情需要用分號結束這些行,並用括號明確地包圍這些哈希。實際上,這是我在Ruby中開始編碼時所使用的。公約不是要使用它們。我很肯定從今年起,你對此會有不同的感覺。戴夫托馬斯http://vimeo.com/61255738#t=2246有一個非常好的演講。在一節中,他討論了由非本地Ruby開發人員編寫的代碼的進展情況。我發現它適合。 – 2013-03-22 16:30:26