【问题标题】:Structuring using rails ActiveRecord使用 Rails ActiveRecord 进行结构化
【发布时间】:2013-03-19 10:37:48
【问题描述】:

我实际上是 Ruby 和 Rails 框架的初学者,这就是为什么我决定在做一些违反框架约定的事情之前寻求帮助的原因。

我有相当扎实的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 已更新并且从原始标签中删除了一些主题标签/提及,这些更改将反映在 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. 架构本身是否正确(即它是否非常低效且设计不佳,无法与 Rails 一起使用?(如果是,请提供有关如何纠正它的建议)
  2. Around Save 是解析和更新关联的正确位置吗?
  3. 我的 ActiveRecords 是否根据当前架构结构正确设置? (特别是我不确定我是否正确使用了polymorphic 属性)
  4. 如何向Poll 实例添加选项/答案,而不重新保存投票的全部内容(从而触发对内容的另一次冗余解析),同时仍然保留OOP 方法? (即选项/答案是通过 Poll 模型的公共 API 创建的)

如果有人对RailsRubyActiveRecord 感到非常满意,可以让我快速了解他们将如何实现这一点,那就太好了。正如我所说,我以前从未使用过 ActiveRecord 类,所以我什至不确定这个简单的代码会在单个 save() 调用上触发多少原始 SQL 查询。

【问题讨论】:

  • 我认为这个问题有点太大了——它几乎要求就您的应用程序进行咨询。我想说的是,如果你是从 Rails 开始的,那就继续尝试吧,当你犯错和学习时,你可以回去并逐步改进。但是,一个提示是使用after_save 而不是around_saveyield
  • 我知道这是一个相当大的问题,但我要问的只是少量代码,其中大部分只是关于在嵌套连接表中正确使用 ActiveRecords 的一些建议,避免需要冗余更新记录。应用程序的标签/提及/评论方面与问题无关(除了关于构建“抽象”表的一般问题),所以本质上它只是询问如何使用 ActiveRecords 实现Poll,而不是真正的“大”。至于after_save这个方法,我没找到明确的文档说明你是否可以在里面回滚。
  • 你可能是对的,它只是少量的代码,但正是对上下文的理解才需要花费大量时间投入才能让某人能够提供这些琐碎的代码。最好尽可能将其分解为更小的问题。
  • 理解是 Rails 的基础,任何熟悉 Rails 的人都应该熟悉 Active Records 的上下文和应用程序,因此这对他们来说非常简单。它本质上只是 3 张桌子……如果你知道自己在做什么,那就没什么工作了。我可以在大约 10-20 分钟内手动完成 SQL,但我宁愿正确使用 ActiveRecords。
  • 你为什么还要使用 Rails?

标签: ruby-on-rails ruby activerecord


【解决方案1】:

这是一个由两部分组成的 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:create rake 任务来创建表

  • Rails 不支持外键。您可以通过验证器强制执行它。

  • 不要使用分号来终止行。 ruby 的乐趣之一是你没有终止行。

  • 不要对 DSL API 参数使用显式哈希。

    使用这个成语

    belongs_to :content, :polymorphic => true
    

    代替:

    belongs_to :content, { :polymorphic => true };
    
  • 使用模块而不是继承来重用代码。

  • 使用each 而不是for

  • 了解数组上的mapreduce(即inject)函数。

【讨论】:

  • 非常感谢您的回答,我回家后会去看视频。但是,我对您后来提出的一些观点有一些疑问。这可能是由于我的语言背景有点“严格”。 Do not use explicit hashes for DSL API parameters. 为什么?我讨厌 ruby​​ 的一件事是它不会强制在方法参数周围加上括号,它让你可以懒惰地编写没有大括号的哈希值。至少对我来说,以这种方式写论据会使一目了然地破译变得更加困难。可能是因为我是 ruby​​ 新手,但我喜欢冗长。
  • 至于显式行终止符;在 C/C++、PHP、C#、Javascript 等期间,我理所当然地花了很长时间编写它们,以至于它根深蒂固,以至于需要付出体力才能记住将它们排除在外 :)
  • 我同情需要用分号终止行并用大括号明确地包围散列。事实上,这是我开始用 Ruby 编码时使用的。约定是不要使用它们。我敢肯定,从现在开始,你会对此有不同的感受。 Dave Thomas vimeo.com/61255738#t=2246 有一个很好的演讲。在一节中,他谈到了由非本地 Ruby 开发人员编写的代码的进展。我觉得很合适。
  • 非常感谢您的回答,我花了一段时间才找到时间重新评论它,但截屏视频非常有用(尽管跳过了很多准系统信息)。享受赏金:)。 @Harish Shetty
猜你喜欢
  • 2018-08-19
  • 1970-01-01
  • 2021-09-03
  • 1970-01-01
  • 2011-12-22
  • 2021-07-03
  • 1970-01-01
  • 2020-07-01
  • 2015-03-25
相关资源
最近更新 更多