1

私は事実上、Ruby と Rails フレームワークの両方の初心者です。そのため、フレームワークの慣習を破るようなことをする前に、助けを求めることにしました。

私はかなり堅実なOOプログラミングのバックグラウンドを持っており、初級から中級レベルの SQL クエリにかなり慣れています。ActiveRecordしかし、 Rails が提供するクラスについて頭を悩ませていました。私の当面の本能は、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;

スキーマが示すように、これは本当に基本的な Web 投票アプリケーションです。各投票には複数のオプションがあり、各オプションには異なるユーザーによる複数の回答が含まれる場合があります。さて、おそらくそれを見ている奇妙な部分はcontent_*テーブルです。これを説明する最善の方法は、おそらくabstract表として説明することです。通常、関係は 2 つ以上の明示的なテーブル間にあり、必要に応じて外部キーを追加します。ただし、この場合、contentハッシュタグ/メンション/コメントが必要な複数の異なるタイプの . どのテーブルが参照されているかは事前にわかりませんcontent_id(コードは受け取ったデータを適切に処理します) indexedcontent_*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、関連付けに反映されるようにするにはどうすればよいですか?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. スキーマ自体は正しいですか (つまり、非常に非効率的であり、レールで使用するように設計されていませんか? (そうであれば、それを修正する方法についての提案は素晴らしいでしょう)
  2. Around Saveアソシエーションを解析および更新する適切な場所はありますか?
  3. ActiveRecords は現在のスキーマ構造に基づいて正しく設定されていますか? (具体的には、polymorphic属性を正​​しく使用しているかどうかわかりません)
  4. これに対するアプローチをPoll維持しながら、投票のコンテンツ全体を再保存することなく (したがって、コンテンツの別の冗長な解析をトリガーすることなく) 、インスタンスにオプション/回答を追加するにはどうすればよいですか? (つまり、オプション/回答は、モデルOOPからパブリック API を介して作成されます)Poll

に本当に慣れている人がいて、これの最低限の機能を実装する方法の簡単なコピーを私に実行できれば、それは本当に素晴らしいRailsことですRuby. ActiveRecord私が言ったように、私は以前にこのクラスを使用したことがないので、この単純なコードが 1 回の呼び出しActiveRecordでトリガーされる未加工の SQL クエリの数さえわかりません。save()

4

1 に答える 1

2

これは、投票/調査アプリケーションの実装の側面をカバーする2部構成のレールキャストです。モデルに関連する疑問のほとんどをカバーしています。

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:createrakeタスクを使用します

  • Railsはフォアギンキーをサポートしていません。バリデーターを介して強制することができます。

  • 行を終了するためにセミコロンを使用しないでください。ルビーの喜びの1つは、終端行がないことです。

  • DSLAPIパラメーターに明示的なハッシュを使用しないでください。

    このイディオムを使用する

    belongs_to :content, :polymorphic => true
    

    それ以外の:

    belongs_to :content, { :polymorphic => true };
    
  • コードの再利用には、継承の代わりにモジュールを使用してください。

  • eachの代わりに使用for

  • 配列の関数をmap学びますreduceinject

于 2013-03-21T19:10:10.313 に答える