Rails和Django的深度技术对比 难以取舍的公正(1)


我想以一个免责声明来开始下面的内容。我使用Django开发网站已经有三年了,众所周知,我喜欢Django。我已经写了一个开源的应用程序app),并且我已经将补丁发送到了Django.然而,我以尽可能以公正的态度写了这篇文章,这篇文章对这个框架有称赞,也有批评。

6个月以前我在大学用Ruby on Rails做了一个项目而且一直做到现在。我做地第一件事就是仔细地学习了这两个框架并对它们进行了比较,但是我记得当时我很泄气的。当我寻找这些问题比如说:”对于这两者来说,数据库的迁移是如何操作的?“、”它们的语法有什么区别?“、”用户认证是如何做的“)的答案时,我站在一个较高的角度比较了两者,发现它们大部分是非常肤浅的。下面的评论将会回答这些问题并且比较每个web框架是如何操作模型、控制器、视图、测试的。

简要介绍

两个框架都是为了更快的开发web应用程序和更好的组织代码这两个需求应运而生的. 它们都遵循 MVC 原则, 这意味着域模型层)的建模,应用程序的展现视图层)以及用户交互控制层)三者之间都是相互分开的. 附带说明一下, Django 实际上只考虑了让框架做控制层的工作,因此Django 自己声称它是一个模型-模板-视图model-template-view)框架. Django 的模板可以被理解为视图,而视图则可以看做是MVC典型场景中的控制层. 本文中我将都是用标准的MVC术语.

Ruby on Rails

 

Ruby on Rails (RoR) 是一个用 Ruby 写就的web开发框架,并且Ruby“famous”也经常被认为是归功于它的. Rails 着重强调了约定大于配置和测试这两个方面. Rails 的约定大于配置CoC)意味着几乎没有配置文件, 只有实现约定好的目录结构和命名规则. 它的每一处都藏着很多小魔法: 自动引入, 自动向视图层传递控制器实体,一大堆诸如模板名称这样的东西都是框架能自动推断出来的. 这也就意味着开发者只需要去指定应用程序中没有约定的部分, 结果就是干净简短的代码了.

Django

Django 是一个用 Python 写成的web开发框架,并以吉他手 Django Reinhardt 命名. Django 出现的动机在于 "产品部密集的最后期限和开发了它的有经验的Web开发者他们的严格要求". Django 遵循的规则是 明确的说明要比深晦的隐喻要好 (这是一条核心的 Python 原则), 结果就是即使对框架不熟悉的人,代码都是非常具有可读性的. 项目中的Django是围绕app组织的.  每一个app都有其自己的模型,控制器,视图以及测试设置,从而像一个小项目一样. Django 项目基本上就是一个小app的集合, 每一个app都负责一个特定的子系统.

模型Model)
 

让我们先从看看每个框架怎样处理MVC原则开始. 模型描述了数据看起来是什么样子的,并且还包含了业务逻辑.

创建模型

Rails 通过在终端中运行一个命令来创建模型.

  1. rails generate model Product name:string quantity_in_stock:integer   
  2.                              category:references 

该命令会自动生成一次迁移和一个空的模型文件,看起来像下面这样:

  1. class Product < ActiveRecord::Base  
  2.    
  3. end 

由于我有Django的技术背景, 令我很生气的一个事实就是我不能只通过模型文件就了解到一个模型有哪些字段. 我了解到Rails基本上只是将模型文件用于业务逻辑,而把模型长什么样存到了一个叫做 schemas.rb 的文件中. 这个文件会在每次有迁移运行时被自动更新. 如果我们看看该文件,我们可以看到我们的 Product 模型长什么样子.

  1. create_table "products", :force => true do |t|  
  2.   t.string   "name",  
  3.   t.integer  "quantity_in_stock",  
  4.   t.integer  "category_id",  
  5.   t.datetime "created_at", :null => false 
  6.   t.datetime "updated_at", :null => false 
  7. end 

在这个模型中你可以看到两个额外的属性. created_at 和 updated_at 是两个会被自动添加到Rails中的每个模型中的属性.

在 Django 中,模型被定义到了一个叫做models.py的文件中. 同样的 Product 模型看起来也许会像下面这样

  1. class Product(models.Model):  
  2.     name = models.CharField()  
  3.     quantity_in_stock = models.IntegerField()  
  4.     category = models.ForeignKey('Category')  
  5.     created_at = models.DateTimeField(auto_now_add=True) # set when it's created  
  6.     updated_at = models.DateTimeField(auto_now=True) # set every time it's updated 

注意,我们必须在Django中明确的也就是自己手动的)添加 created_at 和 updated_at 属性. 我们也要通过auto_now_add 和 auto_now 两个参数告诉 Django 这些属性的行为是如何定义的.

模型Model)字段默认值和外键

Rails将默认允许字段为空。你可以在上面的例子中看到,我们创建的三个字段都被允许为空。引用字段类别也将既不创建索引,也不创建一个外键约束。这意味着引用完整性是无法保证的。Django的字段默认值是完全相反的。没有字段是被允许为空的,除非明确地设置。Django的ForeignKey将自动创建一个外键约束和索引。尽管Rails这里的制定可能是出于性能的担忧,但我会站在Django这边,我相信这个制定可以避免意外)糟糕的设计和意想不到的情况。举例来说,在我们的项目中以前有一个学生没有意识到他创建的所有字段都被允许空null)作为默认值。一段时间后,我们发现我们的一些表包含的数据是毫无意义的,如一个使用null作为标题的轮询。由于Rails不添加外键,在我们的例子中,我们可以删除一个持续引用其他产品的类别,这些产品将会有无效引用。一种选择是使用一个第三方应用程序,增加对自动创建外键的支持。

迁移Migrations)

迁移允许数据库的模式(schema)在创建之后可以再次更改(实际上,在Rails中所有的内容都使用迁移,即使是创建)。我不得不敬佩Rails长期以来支持这个特性。通过使用Rails的生成器(generator)即可完成工作。

  1. $ rails generate migration AddPartNumberToProducts part_number:string 

这会向Product模型(model)添加一个名为part_number的新字段(field)。

然而Django只能通过名为South的第三方库来支持迁移。我感觉South的方式更加简洁和实用。上面对应的迁移工作可以直接编辑Product模型的定义并添加新的字段

  1. class Product(models.Model):  
  2.     ... # 旧字段  
  3.     part_number = models.CharField() 

然后调用

  1. $ python manage.py schemamigration products --auto 

South会自动识别到一个新增字段添加到Product模型并创建迁移文件。随后会调用下面命令完成同步(synced)

  1. $ python manage.py migrate products 

Django最终在它的最新版本(1.7) 将South整合进来并支持迁移。

执行查询
 

感谢对象关系映射(object-relation mapping),你不需要在任何框架中写一行SQL语句。感谢Ruby表达式,你能够很优雅的写出范围搜索查询(range query)。.

  1. Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight) 

这会查询到昨天创建的Clients。Python不支持像1.day这种极其可读和简洁的语法,也不支持..范围操作符。但是,有时在写Rails时我感觉像是又在写预声明(prepared statement)一样。比如为了选择所有的某一字段大于某个值的行,你不得不像下面这样

  1. Model.where('field >= ?', value) 

Django完成的方式不是太好,但以我的观点,却更加简介。在Django对应的代码如下:

  1. Model.objects.filter(field__gt=value) 

控制器(Controller)
 

控制器的工作就是利用请求返回准确的应答。网络应用程序典型工作是支持添加,编辑,删除和显示具体的资源,而RoR的便捷表现在使开发控制器的工作简单而贴心。控制器被拆分为若干个方法(method),每个方法代表指定的动作(action)(show代表请求某个资源,new代表显示创建资源的表单,create代表从new接收POST的数据并真正的创建资源)。控制器的实例变量(以@为前缀)会自动被传递给视图(view),Rails从方法名称就会识别应该把哪个模板(template)作为视图。

  1. class ProductsController < ApplicationController  
  2.   # 自动渲染views/products/show.html.erb  
  3.   def show  
  4.     # params是包含请求变量的ruby hash  
  5.     # 实例变量会自动被传递给视图  
  6.     @product = Product.find(params[:id])  
  7.   end  
  8.    
  9.   # 返回空的product,渲染views/products/new.html.erb  
  10.   def new 
  11.     @product = Product.new 
  12.   end  
  13.    
  14.   # 接收用户提交的POST数据。多数来至于在'new'视图中表单  
  15.   def create  
  16.     @product = Product.new(params[:product])  
  17.     if @product.save  
  18.       redirect_to @product 
  19.     else 
  20.       # 重写渲染create.html.erb的默认行为  
  21.       render "new" 
  22.     end  
  23.   end  
  24. end 

Django使用两种不同的方式实现控制器。你可以使用一个方法来实现每个动作,与Rails做法非常相似,或者你可以为每个控制器动作创建一个类。 Django没有区分new和create方法,资源的创建和空资源的创建发生在同一个控制器中。也没有便捷的方法命名你的视图。视图变量需要从控制器显式的传递,而使用的模板文件也需要显式的设置。

  1. # django通常称 'show' 方法为'detail' 
  2. # product_id 参数由route传递过来  
  3. def detail(request, product_id):  
  4.     p = Product.objects.get(pk=product_id) # pk 表示主键  
  5.    
  6.     # 使用传递的第三个参数作为内容渲染detail.html  
  7.     return render(request, 'products/detail.html', {'product': p})  
  8.    
  9. def create(request):  
  10.     # 检查表单是否提交   
  11.     if request.method == 'POST':  
  12.         # 类似于RoR的 'create' 动作  
  13.         form = ProductForm(request.POST) # 绑定于POST数据的表单  
  14.         if form.is_valid(): # 所有的验证通过  
  15.             new_product = form.save()  
  16.             return HttpResponseRedirect(new_product.get_absolute_url())  
  17.     else:  
  18.         # 类似于RoR的 'new' 动作  
  19.         form = ProductForm() # 空的表单  
  20.    
  21.     return render(request, 'products/create.html', { 'form': form }) 

在以上Django的例子中代码数量与RoR相比很明显。Django似乎也注意到这个问题,于是利用继承和mixin开发出了第二种实现控制器的方法。第二种方法称为基于类的视图(class-based views) (注意, Django称这个控制器为view),并且在Django 1.5中引入以提高代码重用。很多常用的动作都存在可被用来继承的类,比如对资源的显示,列表,创建和更新等,这大大简化了代码开发。重复的工作比如指定将被使用的视图文件名称,获取对象并向view传递该对象等工作也会被自动完成。上面相同的例子使用这种方式只有四行代码。

  1. # 假设route传递了名为 'pk' 的参数,包含对象的 id 并使用该id获得对象。  
  2. # 自动渲染视图 /products/product_detail.html  
  3. # 并将product作为上下文(context)变量传递给该视图  
  4. class ProductDetail(DetailView):  
  5.     model = Product  
  6.    
  7. # 为给定的模型生成表单。如果得到POST数据  
  8. # 自动验证表单并创建资源。  
  9. # 自动渲染视图 /products/product_create.html  
  10. # 并将表单作为上下文变量传递给视图  
  11. class ProductCreate(CreateView):  
  12.     model = Product 

当控制器比较简单时,使用基于类的视图(class-based views)通常是最好的选择,因为代码会变得紧密,具有可读性。但是,取决于你的控制器的不标准(non-standard)程度,可能会需要重写很多函数来得到想要的功能。常遇到的情况就是程序员想向视图传递更多的变量,这时可以重写get_context_data函数来完成。你是不是想按照当前对象(模型实例)的特定的字段来渲染不同的模板?你只好重写render_to_response函数。你想不想改变获得对象的方式(默认是使用主键字段pk)?你只好重写get_object。例如,如果我们想要通过产品名称选择产品而不是id,也要把类似的产品传递给我们的视图,代码就有可能像这样:

  1. class ProductDetail(DetailView):  
  2.     model = Product  
  3.    
  4.     def get_object(self, queryset=None):  
  5.         return get_object_or_404(Product, key=self.kwargs.get('name'))  
  6.    
  7.     def get_context_data(self, **kwargs):  
  8.         # 先调用基类函数获取上下文  
  9.         context = super(ProductDetail, self).get_context_data(**kwargs)  
  10.    
  11.         # 在相关产品(product)中添加  
  12.         context['related_products'] = self.get_object().related_products  
  13.         return context 

视图
 

Rails 视图使用 内置的Ruby 模板系统,它可以让你在你的模板里面编写任意的Ruby代码. 这就意味着它非常强大和快速, 而非常强大的同时就意味着非常大的责任. 你不得不非常小心的不去把表现层同任何其它类型的逻辑混在一起. 这里我需要再次提到涉及一位学生的例子. 一位新同学加入了我们的RoR项目,并且在学习一项新特性. 代码审查的时间到了. 我们首先从控制器开始,第一件令我吃惊的事情是他写的控制器里面代码非常少. 我转而很快去看看他写的视图,看到了大块混着HTML的ruby代码. 诚然,Rails并不会嫌弃缺乏经验的程序员,但我的观点是框架可以帮助开发者避免一些坏的实践. 例如 Django 就有一个非常简洁的 模板语言. 你可以进行if判断以及通过for循环进行迭代,但是没有方法选择没有从控制器传入的对象,因为它并不会执行任意的Python表达式. 这是一个我认为可以敦促开发者方向正确的设计决定. 这能让我们项目中的新手找到组织他们代码的正确方式.

资源: CSS, Javascript 以及 图片

Rails 有一个很不错的内置 资源管道. Rails 的资源管道具有对JavaScript和CSS文件进行串联、最小化和压缩的能力. 不仅仅如此,它也还支持诸如 Coffeescript, Sass 和 ERB 等其它语言. Django 对资源的支持同Rails相比就显得相形见绌了,它把要麻烦都丢给了开发者去处理. Django 唯一提供的就是所谓的 静态文件, 这基本上就只是从每个应用程序那里将所有的静态文件集合到一个位置. 有一个叫做 django_compressor 的第三方app提供了一种类似于Rails的资源管道的解决方案.

单(Forms)

网络应用中的表单是用户输入(input)的界面。在Rails中的表单包含在视图中直接使用的帮助方法(method)。

  1. <%= form_tag("/contact", method: "post") do %>  
  2.   <%= label_tag(:subject, "Subject:") %>  
  3.   <%= text_field_tag(:subject) %>  
  4.   <%= label_tag(:message, "Message:") %>  
  5.   <%= text_field_tag(:message) %>  
  6.   <%= label_tag(:subject, "Sender:") %>  
  7.   <%= text_field_tag(:sender) %>  
  8.   <%= label_tag(:subject, "CC myself:") %>  
  9.   <%= check_box_tag(:sender) %>  
  10.   <%= submit_tag("Search") %>   
  11. <% end %> 

像subject,message这样的输入字段可以在控制器中通过ruby哈希 (类似字典的结构)params来读取,比如params[:subject]和params[:message]。Django通过另一种方式抽象了表单概念。表单封装了字段并包含验证规则。它们看起来像是模型。

  1. class ContactForm(forms.Form):  
  2.     subject = forms.CharField(max_length=100)  
  3.     message = forms.CharField()  
  4.     sender = forms.EmailField()  
  5.     cc_myself = forms.BooleanField(required=False) 

Django会将CharField解析为对应HTML元素的文本输入框,将BooleanField解析为单选框。你可以按照自己的意愿使用 widget 字段更换为其他输入元素。Django的表单会在控制器中实例化。

  1. def contact(request):  
  2.     if request.method == 'POST':  
  3.         form = ContactForm(request.POST)  
  4.         if form.is_valid():  
  5.             return HttpResponseRedirect('/thanks/'# POST之后重定向  
  6.     else:  
  7.         form = ContactForm() # An unbound form  
  8.    
  9.     return render(request, 'contact.html', { 'form': form }) 

Django会自动添加验证信息。默认情况下所有的字段都是必须的,除非特意定义(比如cc_myself)。使用上面的代码片段,如果表单验证失败,带有错误信息的表单会自动重新显示,已经输入的内容也会显示。下面的代码在视图中显示显示了一个表单。

  1. <form action="/contact/" method="post">   
  2. {{ form.as_p }} <!-- 生成类似于rails的表单 -->  
  3. <input type="submit" value="Submit" />  
  4. </form> 

URL 和 Route

Route 是将特定的URL匹配到指定控制器的工作。Rails建立REST网络服务非常轻松,而route以HTTP的行为(verbs)来表达。

  1. get '/products/:id', to: 'products#show' 

以上的例子显示,向/products/any_id发起的GET请求会自动route到products控制器和show动作(action)。感谢惯例优先原则(convention-over-configuration),对于建立包含所有动作(create,show,index等等)的控制器的这种常见任务,RoR建立了一种快速声明所有常用route的方法,叫做resources。如果你依照Rails的惯例(convention)命名了控制器的方法时这会很方便。

  1. # automatically maps GET /products/:id to products#show  
  2. #                    GET /products to products#index  
  3. #                    POST /products to products#create  
  4. #                    DELETE /products/:id to products#destroy  
  5. #                    etc.  
  6. resources :products 

Django不是通过HTTP的行为来决定route。而是使用更复杂的使用正则表达式来匹配URL和对应的控制器。

  1. urlpatterns = patterns('',  
  2.     # 在products控制器中匹配具体方法  
  3.     url(r'^products/(?P\d+)/$', products.views.DetailView.as_view(), name='detail'),  
  4.     # 匹配index方法就获得了主页  
  5.     url(r'^products/$', products.views.IndexView.as_view(), name='index'),  
  6.     url(r'^products/create/$', products.views.CreateView.as_view(), name='create'),  
  7.     url(r'^products/(?P\d+)/delete/$', products.views.DeleteView.as_view(), name='delete'),  

由于使用了正则表达式,框架会自动使用单纯的验证。请求/products/test/会因匹配不到任何route而返回404,因为test不是正确的数字。不同的哲学思想在这里又一次出现。Django在命名控制器动作方面确实方便一些,以至于Django就没有像Rails的resource那样方便的助手,而且每个route必须显式的定义。这将导致每个控制器需要若干个route规则。

测试

在Rails中测试很轻松,与Django比较起来更需要着重强调。

Fixture

两个框架以相似的方式都支持fixture示例数据)。我却给Rails更高的评价,因为它更实用,能从文件的名称得知你在使用哪个模板。Rails使用YAML格式的fixture,这是人类可读的数据序列化格式。

  1. # users.yml (Rails当前知道我们在使用user的fixtures)  
  2. john:  
  3.   name: John Smith  
  4.   birthday: 1989-04-17 
  5.   profession: Blacksmith  
  6.    
  7. bob:  
  8.   name: Bob Costa  
  9.   birthday: 1973-08-10 
  10.   profession: Surfer 

所有的fixture会自动加载而且在测试中可以作为本地变量来访问。

  1. users(:john).name # John Smith 

Django也支持YAML格式的fixture但是开发人员更倾向于使用JSON格式。

  1. [  
  2.   {  
  3.     "model""auth.user",  
  4.     "fields": {  
  5.       "name""John Smith",  
  6.       "birthday""1989-04-17",  
  7.       "profession""Blacksmith",  
  8.     }  
  9.   },  
  10.   {  
  11.     "model""auth.user",  
  12.     "fields": {  
  13.       "name""Bob Costa",  
  14.       "birthday""1973-08-10",  
  15.       "profession""Surfer",  
  16.     }  
  17.   }  

这没什么吸引力,注意它有多啰嗦,因为你必须显式的定义它属于哪个模板,然后在 fields下面列出每个字段。




相关内容

    暂无相关文章

评论关闭