我几乎都是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;
如模式所示,这是一个非常基本的Web轮询应用程序。每个民意调查都有多个选项,每个选项可以有不同用户的多个答案。现在,奇怪的部分可能是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
所以我想我的问题如下:
Around Save
是解析和更新关联的正确位置吗?polymorphic
属性)Poll
实例中添加选项/答案,而不重新保存轮询的整个内容(从而触发内容的另一个冗余解析),同时仍保留OOP
方法这个? (即选项/答案是通过Poll
模型的公共API)如果对Rails
,Ruby
和ActiveRecord
非常满意的人能够快速了解他们如何实现这个问题,那真的很棒。正如我所说,我以前从未使用过ActiveRecord
类,因此我甚至不确定这个简单代码将在单个save()
调用中触发多少原始SQL查询。
答案 0 :(得分: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:create rake tasks创建表
Rails不支持foregin密钥。您可以通过验证器强制执行。
请勿使用分号终止该行。红宝石的一个乐趣就是你没有终止线。
不要对DSL API参数使用显式哈希。
使用此习语
belongs_to :content, :polymorphic => true
而不是:
belongs_to :content, { :polymorphic => true };
使用模块而不是继承来重复使用代码。
使用each
代替for
了解数组中的map
,reduce
(即inject
)函数。