原文:IntermediateRails: Understanding Models, Views and Controllers
非常高兴很多人喜欢StartingRuby on Rails: What I Wish I Knew这篇文章;现在我们来探讨一下曾令我头疼的MVC模式。这不是一篇介绍MVC模式的文章,仅仅是当你初次接触MVC时可能会遇到的问题列表。
这是我理解的MVC模式图:
- l 图中Browser是发送请求的,如http://mysite.com/video/show/15
- l Web server(mongrel,WEBrick等等)是接收请求的。它通过路由(routes)来找到处理请求的控制器;默认的路由模式是“/controller/action/:id”(定义在config/routes.rb)中。在我们的例子中,对应起来指的就是video控制器,控制器的show方法,id为15。Web服务器使用分发器(Dispatcher)来创建新的控制器,调用动作以及传入方法。
- l Controller(控制器)所做的工作就是解析用户的请求、提交的数据、cookies、话以及浏览器相关信息。它们相当于管理者,管理着许多员工。最好的控制器就是像Dilbert-esque:仅仅发布命令而不关注任务是如何完成的。在我们的例子中,video控制器中的show方法知道它必须去查找一个video对象。它通过与model进行交互,获取到id为15的video,最终将查找的结果显示给客户。
- l Model(模型)是Ruby中的类。它们与数据库进行交互,存储和验证数据,执行业务逻辑并且一些情况下做一些繁重的事情。它们是捣弄数字背后的胖小子。在我们的例子中,模型会从数据库中遍历出id为15的video。
- l Views(视图)是我们所看到的:HTML、CSS、XML、Javascript和JSON。它们像是推销员,按照管理者的指令来发布传单和收集数据。视图仅仅是读取控制器已有数据的傀儡。它们不知道在数据黑屋子里会发生什么。在我们例子中,控制器给了video 15数据来显示视图。显示视图生成了HTML:divs、tables、text、description、footers等等。
- l 控制器返回响应信息(HTML、XML等等)和原数据(缓存的headers、redirects)给服务器。服务器将原数据组成HTTP响应并发送给用户。
以故事形式来讲解‘胖模型层,瘦控制器层’比枯燥的‘3层架构’有趣的多。模型层做一些简单的工作,视图层是漂亮的脸蛋,而控制器层是背后的策划者。
许多的MVC讨论者忽略了web服务器的角色。然而,关注控制器是如何魔法般的创建和接收用户信息的是非常重要的。Web服务器是不可见的网关,传输着来来往往的数据:用户从不会直接和控制器进行交互。
- 模型层
模型层在Rails中应该是胖的:它们做一些繁重的事情因此控制器能够保持瘦的、平均的并且忽略掉细节。这里是一些模型层建议:
使用ActiveRecord:
<span style="white-space:pre"> </span>class User < ActiveRecord::Base
<span style="white-space:pre"> </span>end
“<ActiveRecord::Base”这段代码的意思是,你底层的User模型继承于ActiveRecord::Base,并且获取Rails对数据库的魔法般的查询和保存方法。
Ruby能够很容易的捕获到未定义的方法。ActiveRecord允许像“find_by_login”这样的方法,而实际上此类方法并不存在。当你调用“find_by_login”的时候,Rails捕获到未定义的方法被调用,并搜索“login”字段。假设你的数据库中有login字段,那么模型将做基于login字段上的查询。实现此功能不需要什么额外的配置。
定义类方法和实例方法
def self.foo
“Class method” # User.foo
end
def bar
“instance method” # user.bar
end
类方法和实例方法经常容易混淆。
user(小写字母u)是一个对象,你能够像user.save一样调用实例方法。
User(大写字母U)是一个类方法,你不必需要一个对象来调用它(如User.find)。ActiveRecord为你的模型自动添加了实例方法和类方法。
建议:要像User.find_latest一样定义类方法,而不是使用明确参数的User.find进行查找(瘦控制器更好)。
使用属性:
通常的Ruby对象能够像这样定义属性:
# attribute in regular Ruby
attr_accessor :name # like @name
def name=(val) # custom settermethod
@name =val.capitalize # clean it up beforesaving
end
def name # customgetter
“Dearest ” + @name # make it nice
end
这里是一些解释:
- l attr_accessor :name将为模型创建get和set方法(即name=和name)。这就像有了公共实例变量@name。
- l 定义方法name=(val)来改变@name是如何被保存的(例如验证输入数据的合法性)。
- l 定义方法name来控制变量的输出形式(例如改变输出格式)。
- 在Rails中,因为数据库的魔法性属性容易被混淆。这里是一些策略:
- l ActiveRecord获取到数据库的所有字段,并将它们放入attributes数组中。这样可以方便的进行get和set,但是你必须通过调用user.save来保存它们。
- l 若你想覆盖默认的get和set,使用:
# ActiveRecord: override how we access field
def length=(minutes)
self[:length]= minutes * 60
end
def length
self[:length]/ 60
end
ActiveRecord定义了’[]’方法来获取原始的属性(包裹着write_attribute和read_attribute)。这是你如何改变你原始的数据。你能够重定义长度,如下:
def length # this is bad
length / 60
end
由于这是一个无限循环(这一点都搞笑)。因此self[]也是。这也是Rails经常另我头疼的地方,当你不确定的时候可以使用self[:field]。
从不用忘记你正在使用数据库:
Rails是简单、干净的,以至于你会忘记你正在使用数据库。请不要忘记。
保存你的模型。若你做了一些修改,记得保存。忘记保存似乎是件非常容易的事情。你能够使用update_attributes(params)来传入哈希键值对,对数据库中数据进行更新。
当有改变时,重新加载你的模型。假设一个user对象与video对象之间的关系是has_many。你创建了一个信息的video对象,并指定正确的user,然后你希望通过user.video来获取该用户下所有相关的video信息。这样会起作用吗?
可能不会。如果你已经从videos表中查询过数据,user.videos可能会保留旧数据。你需要调用user.reload来刷新获取到最新的查找数据。注意内存中的模型数据会被缓存。
使用新模型:
有两种方式可以创建新对象:
joe = User.new(:name => “SadJoe”) # not save
bob = User.create(:name =>“Happy Bob”) # saved
- l User.new创建了新对象,通过哈希设置了属性。但是该对象并未保存到数据库中:你必须调用user.save来保存。若数据无效,调用save将失败。
- l User.create创建了一个新对象并将其保存到数据库中。若验证失败,user.errors以哈希形式保存了所有的错误信息以及详细提示信息。
注意哈希是如何被传入的。使用Ruby的大括号魔法,{}不必明确写出,因此:
user= User.new( :name => "kalid", :site =>"instacalc.com" )
将变为:
User.new( {:name => "kalid", :site =>"instacalc.com"} )
其中的箭头(=>)表明哈希将被传入(Ruby1.9以后可以写成name: “kalid”)。
使用关联关系:
假设用户有状态字段(即users表中有status字段),有多种状态:活跃、不活跃、沉闷等等。关联关系应该是怎样的呢?
class User < ActiveRecord::Base
belongs_to:status # this?
has_one:status # or this?
end
你很可能认为是belongs_to:status。是的,这个听起来有点奇怪。不要困惑于”has_one”和”belongs_to”的字面意思,要理解其意义:
- l belongs_to:从本表链接向另一个数据表(links_to)。每个用户链接向一个状态。
- l has_one:从另一张表链接向本表(linked_from)。每个状态链接到一个用户。事实上,状态模型(statuses)不知道用户模型(users),在状态表中根本没有用户的信息。在Class Status(即状态模型)中,我们写入has_many :users(has_one和has_many功能相似,但前者返回所链接的一个对象,后者返回所链接的多个对象)。
备注:
- l belongs_to与links_to押韵
- l has_one与linked_from押韵
- 上述是以韵律进行排列的,此方法对我有帮助,也希望可以帮到你。
这些关联关系实际上定义了方法来对其他类进行查找。例如,‘user belongs_to status’意味着user.status将通过status_id来查询status表。同样的,‘status has_many :users’意味着status.users将通过当前的status_id来查询user表。ActiveRecord在我们初始声明这些关系的时候,捕获并处理了这些魔法。
使用自定义的关联关系:
假设我们有两个状态,一个为主状态,另一个为副状态,并使用如下:
belongs_to :primary_status, :model => ‘Status’, :foreign_key=> ‘primary_status_id’
belongs_to:secondary_status, :model=>’Status’, :foreign_key=>’secondary_status_id’
你定义一个新字段,并明确声明了模型的引用以及指向user表的外键。例如,user.primary_status返回一个Status对象且id值与primary_status_id相同。非常棒!、
- 控制器层:
这部分比较简短,因为控制器不应该做太多,除了管理模型和视图外。主要处理如下一些事情:
- l 处理一下像会话、登录验证,过滤,重定向以及错误提示等。
- l 有一些默认的方法(被ActionController添加)。若访问http://localhost:3000/user/show将会调用show方法,若show方法未定义将会自动渲染show.html。
- l 将实例变量(如@user)传入视图。局部变量(不以@符号开头的)将不被传送。
- l 比较难调试。摔死render:text => “Error found” and return来打印调试你的页面。这也是为什么将大多数代码放入到model层,方便通过控制台来调试。
- l 使用会话来存储请求间的数据:session[:variable] = “data”
我将再次提醒(我曾因此抓狂):使用@foo而不是foo来传入数据到视图。
- 使用视图:
视图是简单明了的,基本概念:
- l 控制器动作使用与动作名相同的视图(如show动作默认将加载show.html页面)
- l 控制器实例变量(@foo)在做所有视图和局部视图中都可以调用(译者注:应该注意作用域)。
- 在视图中使用ERB来运行代码:
- l <% … %>:运行代码,但是不输出。常用于if/then/else/end和array.each循环。你能够使用<% if false %> Hi there <% end %>来注释HTML代码。若你在%>后有内容,你将看到两行之间会有空行。(译者注:Rails3.2以上未发现,但不使用Rails框架进行ERB开发时会有)
- l <%- … %>(此方式以废弃):运行代码,不打印尾部新行。生成XML或JSON数据,和当你为了易读性将.html中代码分块,但又不希望输出时有空行时,使用此种方式即可。
- l <%= … %>:运行代码并打印返回值。例如:<%= @foo %>(@开头的变量会被传入到视图中,不是吗?)。不要将if语句放入到<%=中,会导致错误。
- l <%= h … %>:打印代码并对html代码进行字符化处理:>将显示为>。h()实际上是一个Ruby函数,但是不需要括号就可以调用,这是Rubyists经常的做法。
起初,你可能会有所困惑,运行一些代码后,你将理解更清晰。
深呼吸一下:
对于MVC模式很难将其简短的讲明白。当你对它越来越熟悉的时候,任何Rails程序将变得容易被琢磨:更能理解每一部分的意义。MVC将保持你的代码更优秀和模块化,能更好的被调试和维护。