Buggy Rails 代码:Rails 开发人员最常犯的 10 个错误
已发表: 2022-03-11Ruby on Rails(“Rails”)是一种流行的开源框架,基于 Ruby 编程语言,致力于简化和简化 Web 应用程序开发过程。
Rails 建立在约定优于配置的原则之上。 简而言之,这意味着,默认情况下,Rails 假定其专家开发人员将遵循“标准”最佳实践约定(例如命名、代码结构等),如果您这样做,一切都会为您“自动-magically”,而您无需指定这些详细信息。 虽然这种范式有其优点,但也并非没有缺陷。 最值得注意的是,框架中发生在幕后的“魔法”有时会导致假装、混乱和“到底发生了什么?” 类型的问题。 它还可能对安全性和性能产生不良影响。
因此,尽管 Rails 易于使用,但它也不难误用。 本教程着眼于 10 个常见的 Rails 问题,包括如何避免它们以及它们导致的问题。
常见错误 #1:在控制器中放置太多逻辑
Rails 基于 MVC 架构。 在 Rails 社区中,我们一直在谈论胖模型、瘦控制器,但我继承的几个最近的 Rails 应用程序违反了这个原则。 将视图逻辑(最好放在帮助程序中)或域/模型逻辑移动到控制器中太容易了。
问题是控制器对象将开始违反单一职责原则,这使得未来对代码库的更改变得困难且容易出错。 通常,您的控制器中应该有的唯一逻辑类型是:
- 会话和 cookie 处理。 这可能还包括身份验证/授权或您需要执行的任何其他 cookie 处理。
- 型号选择。 给定从请求传入的参数,查找正确模型对象的逻辑。 理想情况下,这应该是对单个 find 方法的调用,该方法设置一个实例变量,以便稍后用于呈现响应。
- 请求参数管理。 收集请求参数并调用适当的模型方法来持久化它们。
- 渲染/重定向。 呈现结果(html、xml、json 等)或重定向,视情况而定。
虽然这仍然突破了单一职责原则的限制,但它是 Rails 框架要求我们在控制器中拥有的最低限度。
常见错误 #2:在视图中放置太多逻辑
开箱即用的 Rails 模板引擎 ERB 是构建具有可变内容的页面的好方法。 但是,如果您不小心,您很快就会得到一个混合了 HTML 和 Ruby 代码的大文件,可能难以管理和维护。 这也是一个可能导致大量重复的领域,从而导致违反 DRY(不要重复自己)原则。
这可以通过多种方式表现出来。 一是在视图中过度使用条件逻辑。 作为一个简单的例子,考虑一个可用的current_user
方法返回当前登录用户的情况。 通常,视图文件中最终会出现这样的条件逻辑结构:
<h3> Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %> </h3>
处理此类问题的更好方法是确保current_user
返回的对象始终设置,无论是否有人登录,并且它以合理的方式回答视图中使用的方法(有时称为 null目的)。 例如,您可以像这样在app/controllers/application_controller
中定义current_user
助手:
require 'ostruct' helper_method :current_user def current_user @current_user ||= User.find session[:user_id] if session[:user_id] if @current_user @current_user else OpenStruct.new(name: 'Guest') end end
然后,这将使您能够用以下简单的代码行替换之前的视图代码示例:
<h3>Welcome, <%= current_user.name -%></h3>
几个额外推荐的 Rails 最佳实践:
- 适当地使用视图布局和局部视图来封装页面上重复的内容。
- 使用 Draper gem 之类的演示者/装饰器将视图构建逻辑封装在 Ruby 对象中。 然后,您可以将方法添加到此对象中以执行您可能已放入视图代码中的逻辑操作。
常见错误#3:在模型中加入太多逻辑
鉴于最小化视图和控制器中的逻辑的指导,MVC 架构中唯一可以放置所有逻辑的地方就是模型,对吧?
嗯,不完全是。
许多 Rails 开发人员实际上犯了这个错误,最终将所有内容都粘在他们的ActiveRecord
模型类中,从而导致 mongo 文件不仅违反了单一职责原则,而且是维护的噩梦。
诸如生成电子邮件通知、与外部服务接口、转换为其他数据格式等功能与 ActiveRecord 模型的核心职责没有太大关系, ActiveRecord
模型应该只是在数据库中查找和保存数据。
所以如果逻辑不应该放在视图中,也不应该放在控制器中,也不应该放在模型中,那么,它应该放在哪里呢?
输入普通的旧 Ruby 对象 (PORO)。 对于像 Rails 这样的综合框架,新开发人员通常不愿意在框架之外创建自己的类。 然而,将逻辑从模型中移到 PORO 中通常正是医生为了避免过于复杂的模型而要求的。 使用 PORO,您可以将电子邮件通知或 API 交互等内容封装到它们自己的类中,而不是将它们粘贴到ActiveRecord
模型中。
因此,考虑到这一点,一般来说,应该保留在模型中的唯一逻辑是:
-
ActiveRecord
配置(即关系和验证) - 简单的变异方法来封装更新少数属性并将它们保存在数据库中
- 访问包装器以隐藏内部模型信息(例如,结合数据库中的
first_name
和last_name
字段的full_name
方法) - 复杂的查询(即比简单的
find
更复杂); 一般来说,您永远不应该在模型类本身之外使用where
方法或任何其他类似的查询构建方法
常见错误 #4:使用通用辅助类作为垃圾场
这个错误实际上是上面错误#3的必然结果。 如前所述,Rails 框架强调 MVC 框架的命名组件(即模型、视图和控制器)。 对于属于这些组件的类中的事物种类有相当好的定义,但有时我们可能需要似乎不适合这三者中的任何一个的方法。
Rails 生成器方便地构建一个帮助器目录和一个新的帮助器类,以配合我们创建的每个新资源。 然而,开始将任何不适合模型、视图或控制器的功能填充到这些帮助类中变得非常诱人。
尽管 Rails 肯定是以 MVC 为中心的,但没有什么能阻止您创建自己的类类型并添加适当的目录来保存这些类的代码。 当您有其他功能时,请考虑将哪些方法组合在一起,并为包含这些方法的类找到合适的名称。 使用像 Rails 这样的综合框架并不是放弃良好的面向对象设计最佳实践的借口。
常见错误 #5:使用太多宝石
Ruby 和 Rails 由丰富的 gem 生态系统支持,这些 gem 共同提供了开发人员能想到的任何功能。 这对于快速构建复杂的应用程序非常有用,但我也看到许多臃肿的应用程序,与提供的功能相比,应用程序的Gemfile
中的 gem 数量大得不成比例。
这会导致几个 Rails 问题。 过度使用 gem 会使 Rails 进程的大小超出其需要的大小。 这会降低生产性能。 除了让用户感到沮丧之外,这还可能导致需要更大的服务器内存配置并增加运营成本。 启动更大的 Rails 应用程序也需要更长的时间,这会使开发速度变慢,并使自动化测试花费更长的时间(通常,慢速测试根本不会经常运行)。
请记住,您带入应用程序的每个 gem 可能又依赖于其他 gem,而这些 gem 又可能依赖于其他 gem,依此类推。 因此,添加其他宝石可以产生复合效果。 例如,添加rails_admin
gem 总共会增加 11 个 gem,比基础 Rails 安装增加了 10% 以上。
在撰写本文时,全新的 Rails 4.1.0 安装在Gemfile.lock
文件中包含 43 个 gem。 这显然比Gemfile
中包含的要多,它代表了少数标准 Rails gem 作为依赖项引入的所有 gem。
在添加每个 gem 时,请仔细考虑额外的开销是否值得。 例如,开发人员通常会随意添加rails_admin
gem,因为它本质上为模型结构提供了一个不错的 Web 前端,但它实际上只不过是一个花哨的数据库浏览工具。 即使您的应用程序需要具有额外权限的管理员用户,您也可能不希望给他们原始数据库访问权限,与添加此 gem 相比,开发您自己的更简化的管理功能会更好地为您服务。
常见错误 #6:忽略您的日志文件
虽然大多数 Rails 开发人员都知道在开发和生产过程中可用的默认日志文件,但他们通常没有足够注意这些文件中的信息。 虽然许多应用程序在生产中依赖于 Honeybadger 或 New Relic 等日志监控工具,但在整个开发和测试应用程序的过程中密切关注您的日志文件也很重要。
正如本教程前面提到的,Rails 框架为您做了很多“魔法”,尤其是在模型中。 在模型中定义关联可以很容易地拉入关系并使所有内容都可用于您的视图。 为您生成填充模型对象所需的所有 SQL。 那太棒了。 但是你怎么知道生成的 SQL 是有效的呢?
您经常遇到的一个示例称为 N+1 查询问题。 虽然这个问题很好理解,但观察它发生的唯一真正方法是查看日志文件中的 SQL 查询。
例如,您在一个典型的博客应用程序中有以下查询,您将在其中显示一组选定帖子的所有评论:

def comments_for_top_three_posts posts = Post.limit(3) posts.flat_map do |post| post.comments.to_a end end
当我们查看调用此方法的请求的日志文件时,我们会看到类似以下内容,其中进行了一次查询以获取三个 post 对象,然后再进行三个查询以获取每个对象的评论:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:13 -0700 Processing by PostsController#some_comments as HTML Post Load (0.4ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (5.6ms) ELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]] Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]] Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 3]] Rendered posts/some_comments.html.erb within layouts/application (12.5ms) Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)
Rails 中ActiveRecord
的快速加载功能可以通过让您提前指定将要加载的所有关联来显着减少查询的数量。 这是通过在正在构建的 Arel ( ActiveRecord::Relation
) 对象上调用includes
(或preload
) 方法来完成的。 使用includes
, ActiveRecord
可确保使用尽可能少的查询来加载所有指定的关联; 例如:
def comments_for_top_three_posts posts = Post.includes(:comments).limit(3) posts.flat_map do |post| post.comments.to_a end end
执行上述修改后的代码时,我们在日志文件中看到所有评论都收集在一个查询中,而不是三个:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:18 -0700 Processing by PostsController#some_comments as HTML Post Load (0.5ms) SELECT "posts".* FROM "posts" LIMIT 3 Comment Load (4.4ms) SELECT "comments".* FROM "comments" WHERE"comments "."post_id" IN (1, 2, 3) Rendered posts/some_comments.html.erb within layouts/application (12.2ms) Completed 200 OK in 560ms (Views: 219.3ms | ActiveRecord: 5.0ms)
效率更高。
这个 N+1 问题的解决方案实际上只是作为示例,如果您没有给予足够的关注,那么您的应用程序中可能存在“幕后”的低效率。 这里的要点是,您应该在开发期间检查您的开发和测试日志文件,以检查(并解决!)构建响应的代码中的低效率。
查看日志文件是发现代码效率低下并在应用程序投入生产之前纠正它们的好方法。 否则,在您的系统上线之前,您可能不会意识到由此产生的 Rails 性能问题,因为您在开发和测试中使用的数据集可能比在生产中小得多。 如果您正在开发一个新应用程序,即使您的生产数据集一开始也可能很小,并且您的应用程序看起来运行良好。 但是,随着您的生产数据集的增长,像这样的 Rails 问题会导致您的应用程序运行得越来越慢。
如果你发现你的日志文件被一堆你不需要的信息堵塞了,你可以做一些事情来清理它们(那里的技术适用于开发和生产日志)。
常见错误 #7:缺乏自动化测试
Ruby 和 Rails 默认提供强大的自动化测试功能。 许多 Rails 开发人员使用 TDD 和 BDD 样式编写非常复杂的测试,并使用更强大的测试框架以及 rspec 和 cucumber 之类的 gem。
尽管向您的 Rails 应用程序添加自动化测试是多么容易,但我对我继承或加入的项目中实际上没有编写测试(或充其量很少)的情况感到非常不愉快开发团队。 虽然有很多关于你的测试应该有多全面的争论,但很明显,每个应用程序至少应该存在一些自动化测试。
作为一般经验法则,应该为控制器中的每个操作编写至少一个高级集成测试。 在未来的某个时候,其他 Rails 开发人员很可能想要扩展或修改代码,或者升级 Ruby 或 Rails 版本,而这个测试框架将为他们提供一种清晰的方式来验证应用程序的基本功能是否正确在职的。 这种方法的另一个好处是它为未来的开发人员提供了对应用程序提供的完整功能集合的清晰描述。
常见错误 #8:阻止对外部服务的调用
Rails 服务的第 3 方提供商通常可以通过封装其 API 的 gem 轻松地将他们的服务集成到您的应用程序中。 但是,如果您的外部服务中断或开始运行非常缓慢,会发生什么情况?
为了避免阻塞这些调用,而不是在正常处理请求期间直接在 Rails 应用程序中调用这些服务,您应该在可行的情况下将它们移至某种后台作业队列服务。 Rails 应用程序中用于此目的的一些流行 gem 包括:
- 延误工作
- 雷斯克
- 西德基克
如果将处理委派给后台作业队列是不切实际或不可行的,那么您需要确保您的应用程序具有足够的错误处理和故障转移规定,以应对外部服务出现故障或遇到问题时不可避免的情况. 您还应该在没有外部服务的情况下测试您的应用程序(可能通过从网络中删除您的应用程序所在的服务器),以验证它不会导致任何意外后果。
常见错误 #9:与现有的数据库迁移结婚
Rails 的数据库迁移机制允许您创建指令来自动添加和删除数据库表和行。 由于包含这些迁移的文件是按顺序命名的,因此您可以从头开始回放它们,以将空数据库带入与生产相同的模式。 因此,这是管理应用程序数据库模式的精细更改并避免 Rails 问题的好方法。
虽然这在您的项目开始时确实很有效,但随着时间的推移,数据库创建过程可能会花费相当长的时间,有时迁移会放错位置、乱序插入或从使用同一数据库服务器的其他 Rails 应用程序引入。
Rails 在名为db/schema.rb
的文件中创建当前模式的表示(默认情况下),该文件通常在运行数据库迁移时更新。 通过运行rake db:schema:dump
任务,甚至可以在没有迁移的情况下生成schema.rb
文件。 一个常见的 Rails 错误是检查一个新的迁移到你的源代码库,而不是相应更新的schema.rb
文件。
当迁移失控并且运行时间过长,或者不再正确创建数据库时,开发人员不应该害怕清除旧的迁移目录,转储新的模式,然后从那里继续。 建立一个新的开发环境将需要rake db:schema:load
而不是大多数开发人员所依赖的rake db:migrate
。
Rails 指南中也讨论了其中一些问题。
常见错误 #10:将敏感信息检查到源代码存储库中
Rails 框架使创建不受多种攻击的安全应用程序变得容易。 其中一些是通过使用秘密令牌来保护与浏览器的会话来实现的。 尽管这个令牌现在存储在config/secrets.yml
中,并且该文件从生产服务器的环境变量中读取令牌,但过去的 Rails 版本将令牌包含在config/initializers/secret_token.rb
中。 该文件经常被错误地与您的应用程序的其余部分一起签入源代码存储库,当这种情况发生时,任何有权访问该存储库的人现在都可以轻松地危害您的应用程序的所有用户。
因此,您应该确保您的存储库配置文件(例如,用于 git 用户的.gitignore
)排除带有您的令牌的文件。 然后,您的生产服务器可以从环境变量或类似 dotenv gem 提供的机制中获取它们的令牌。
教程总结
Rails 是一个强大的框架,它隐藏了构建强大的 Web 应用程序所需的许多丑陋的细节。 虽然这使 Rails Web 应用程序开发速度更快,但开发人员应注意潜在的设计和编码错误,以确保他们的应用程序在发展过程中易于扩展和维护。
开发人员还需要注意可能使他们的应用程序更慢、更不可靠和更不安全的问题。 研究框架并确保您完全了解您在整个开发过程中所做的架构、设计和编码权衡非常重要,以帮助确保高质量和高性能的应用程序。