Rails 数据库最佳实践

简介: Rails 数据库最佳实践

在维护一些老旧的 Rails 项目的时候,我偶尔会碰到一些不好的 ActiveRecord 代码。我也花费过一些时间来加速一些相对较慢的、或者多次调用数据库的数据库操作。有了这些经历,启发了我写一些“回归到基础”的 Rails 数据库最佳实践。

规则 1:让数据库完成它应该完成的工作

当我们正确使用数据库的时候,数据库会表现出非常丰富的特性以及令人难以置信的速度。他们在数据过滤和排序方面是非常厉害的,当然其他方面也不错。假如数据库可以做的,我们就让数据库来完成,这方面肯定速度会比 Ruby,或其他语言快。

你可能必须学习一点关于数据库方面的知识,但是老实说,为了发挥数据库的最佳好处你不必深入学习太多相关知识。

通常我们用的都是 Postgres。选择哪种数据库和认识它、使用它,让它发挥它的长处相比来说,后者更重要。假如你对 Postgres 好奇,在这篇博客后面有挺多相关资源的链接。我们喜爱他们。

我们的首要原则就是:让数据库来做它擅长的事情,而不是 Ruby。

规则 2:编写高效的和可以级联调用的 Scope

Scope 很好用。它们允许你根据具体的要求创建直观的 helper 来获取数据的子集,但是反模式的 scope 反而会显著的扼杀它带给我们的优势。我们先看看.active scope:

class Client < ActiveRecord::Base
  has_many :projects
end
class Project < ActiveRecord::Base
  belongs_to :client
  # Please don't do this...
  scope :active, -> {
    includes(:client)
      .where(active: true)
      .select { |project| project.client.active? }
      .sort_by { |project| project.name.downcase }
  }
end

有几点需要我们注意的是:

  1. 这个 scope 返回的不是 ActiveRecord Relation,所以它不可以链式调用,也不能用.merge方法。
  2. 这个 scope 用 Ruby 来实现数据选择(过滤)
  3. 用 Ruby 实现排序
  4. 周期性的用 Ruby 实现排序

#1 返回 ActiveRecord::Relation(例如不触发查询)

为什么返回 Relation 更好?因为 Relation 可以链式调用。可链式调用的 scope 更容易被复用。当 scope 返回的是 Relation 时,你可以把多个 scope 组合进一条查询语句,例如Athlete.active.men.older_than(40)。Relation 也可以和.merge()一起使用,安利一下.merge()非常好用。

#2 用 DB 选择数据(而不是 Ruby)

为什么用 Ruby 过滤数据是个坏主意?因为和 DB 相比,Ruby 这方面确实很慢。对于小的数据集来说关系大,但是对大量数据集来说,差别就太大了。

首要原则:Ruby 做的越少越好

为什么 Ruby 在这方面很慢,有三方面原因:

  • 花费在把数据从数据库里传到应用服务器上的时间
  • ActiveRecord 必须解析查询结果,然后为每条记录创建 AR 模型对象
  • 你的数据库有索引(对吧?!),它会让数据过滤非常快,Ruby 不会。

#3 让数据库排序(而不是 Ruby)

那么排序呢?在数据库里排序会更快,除非你处理很大的数据集,否则很难注意到这点。最大的问题是.sort_by会触发执行查询,而且我们丢失了 Relation 数据结构。光这个原因就足够说服我们了。

#4 不要把排序放进 scope 里面(或放到一个专用的 scope 里)

因为我们尽量创建一个可复用的 scope,不可能每个 scope 的调用都有相同的排序要求。因此,我推荐把微不足道的排序一起单独拿出来。或者,把混合的或比较重要的排序移到它自己的 scope,如下:

scope :ordered, => { order(:status).order('LOWER(name) DESC') }

更好的 scope 看起来如下:

class Client < ActiveRecord::Base
  has_many :projects
  scope :active, -> { where(active: true) }
end
class Project < ActiveRecord::Model
  belongs_to :client
  scope :active, -> {
    where(active: true)
      .joins(:client)
      .merge(Client.active)
  }
  scope :ordered, -> {
    order('LOWER(name)')
  }
end

在重构过的 scope 版本里看看.merge()api的用法。.merge()让从别的已经 join 进查询的 model 中使用 scope 变得更加容易,减少了潜在的可能存在的重复。

这个版本在效率上等价,但是消除了原来的 scope 的一些缺点。我觉得也更容易阅读。

规则 3:减少数据库调用

ActiveRecord 为操作数据库提供了容易使用的 API。问题主要是在开发期间我们的通常使用本地数据库,数据也很少。一旦你把代码 push 到生产环境,等待时间瞬间增加了 10+倍。数据量显著上升。请求变得非常非常慢。

最佳实践:假如经常被访问的页面需要多次访问 DB,那么就需要多花点时间来减少查询数据库的次数。

在很多情况下,区别就在于用.includes() 还是.joins()。有时你还必须使用.group(), .having()和一些其他函数。在很稀少的情况下,你可能需要直接写 SQL 语言。

对于非不重要的查询,从 CLI 开始。**一旦你实现了 SQL,然后在弄清楚怎么把它转化为 ActiveRecord。这样的话,你每次只需弄明白一件事,先是纯 SQL,然后是 ActiveRecord。DB 是 Postgres?使用pgcli而不是 psql。

这方面有很多详细的介绍。下面是一些链接:

那么用缓存会怎么样呢?当然,缓存是另外一个加速这些加载速度慢的页面,但是最好先消除低效的查询。当没有缓存时可以提高表现,通常也会减少 DB 的压力,这对 scale 会很有帮助。

#4:使用索引

DB 仅在查询有索引的列的时候会很快,否则就会做全表查询(坏消息)。

首要原则:在每个 id 列和其他在 where 语句里出现的列上都添加索引。

为表添加索引很容易。在 Rails 的迁移里:

class SomeMigration < ActiveRecord::Migration
  def change
    # Specify that an index is desired when initially defining the table.
    create_table :memberships do |t|
      t.timestamps             null: false
      t.string :status,        null: false, default: 'active', index: true
      t.references :account,   null: false, index: true, foreign_key: true
      t.references :club,      null: false, index: true, foreign_key: true
      # ...
    end
    # Add an index to an existing table.
    add_index :payments, :billing_period
    # An index on multiple columns.
    # This is useful when we always use multiple items in the where clause.
    add_index :accounts, [:provider, :uid]
  end
end

现实略微有点差别,而且总是。过度索引和在 insert/update 时会增加一些开销是可能的,但是作为首要原则,有胜于无。

想要理解当你触发查询或更新时 DB 正做什么吗?你可以在 ActiveRecord Relation 末尾添加.explain,它会返回 DB 的查询计划。见running explain

#规则 5:对复杂的查询使用 Query 对象

我发现 scope 当他们很简单而且做的不多的时候最好用。我把它们当中可复用的构建块。假如我需要做一些复杂的事情,我会使用 Query 类来包装复杂的查询。示例如下:

# A query that returns all of the adults who have signed up as volunteers this year,
# but have not yet become a pta member.
class VolunteersNotMembersQuery
  def initialize(year:)
    @year = year
  end
  def relation
    volunteer_ids  = GroupMembership.select(:person_id).school_year(@year)
    pta_member_ids = PtaMembership.select(:person_id).school_year(@year)
    Person
      .active
      .adults
      .where(id: volunteer_ids)
      .where.not(id: pta_member_ids)
      .order(:last_name)
  end
end

粗看起来好像查询了多次数据库,然而并没有。9-10行只是定义 Relation。在15-16 行里的两个子查询用到它们。这是生成的 SQL(一个单独的查询):

SELECT people.*
FROM people
WHERE people.status = 0
  AND people.kind != "student"
  AND (people.id IN (SELECT group_memberships.person_id FROM group_memberships WHERE group_memberships.school_year_id = 1))
  AND (people.id NOT IN (SELECT pta_memberships.person_id FROM pta_memberships WHERE pta_memberships.school_year_id = 1))
ORDER BY people.last_name ASC

注意,这个查询返回的是一个 ActiveRecord::Relation,它可以被复用。

不过有时候要返回一个 Relation 实在太难,或者因为我只是在做原型设计,不值得那么努力。在这些情况下,我会写一个返回数据的查询类(例如触发查询,然后以模型、hash、或者其他的形式返回数据)。我使用命名惯例:加入返回的是已经查询的数据,用.data,否则用.relation。如上。

查询模式的主要好处是代码组织;这个也是把一些潜在的复杂的代码从 Model/Controller 里提取到自己的文件的一个比较容易的方式。单独的查询也容易测试。它们也遵循单负责原则。

#规则 6:避免 Scope 和查询对象外的 ad-hoc 查询(及时查询)

我不记得我第一次从哪里听到的,但是首要原则是和我站在一条线:

限制 Scope 和查询对象对 ActiveRecord 的构建查询方法(如.where, .group, joins, .not, 等)的访问。

即,把数据读写写进 scope 和查询对象,而不是在 service、controller、task 里构建 ad-hoc 查询。

为什么?嵌入控制器(或视图、任务、等)里的 ad-hoc 查询更难测试,不能复用。对于推理代码遵从什么原则更容易,让它更容易理解和维护。

#规则 7:使用正确的类型

每个数据库提供的数据类型都比你以为的要多。这些不常用的 Postgres 类型我认为适合绝大多数应用:

  • 想要保留状态(preserve case),但是希望所有的比较都大小写不敏感吗?citextdoc正是你需要的。在 migration 里和 String 用法差不多。
  • 想要保存一个集合(例如地址,标签,关键词)但是用一个独立的表觉得太重了?使用array类型。 (PG docs)/(Rails doc)
  • 模型化 date, int, float Range?使用 range 类型(PG doc)/(Rails doc)
  • 需要全局独立的 ID(主键或其他)使用UUID类型(PG doc)/(Rails doc)
  • 需要保存 JSON 数据,或者在考虑 NoSQL DB?使用 JSON 类型之一(PG doc)/(Rails doc)

这些只是特殊的数据类型中的一小部分。感兴趣可以看理解数据类型的能量--PG 的秘密武器了解更多。

#规则 8:考虑你的数据库的全文检索

PG 高级查询链接 (1)(2)

和(PG 文档)

#规则 9:存储过程作为最后的选择(翻译略)

Wait what?! I’m saying use your database, but not to use stored procedures?!

Yup. There’s a time and place for them, but I think it’s better to avoid them while a product is rapidly evolving. They’re harder to work with and change, awkward to test, and almost definitely unnecessary early on. Keep it simple by leaving business logic out of your database, at least until something drives you to re-evaluate.

总结

我相信当使用数据库的潜力时产品会性能会表现更好,更容易。建议

减少查询的数量,使用索引,或任何别的建议都不是初级优化 IMHO。它是正确的使用你的数据库。当然,有一个收益递减的点:例如写一个七七八八的原始 SQL 查询,从 3 个一般的查询减少到 1 个。利用你最好的判断力。

相关文章
|
存储 缓存 数据库
数据库数据删除策略:硬删除vs软删除的最佳实践指南
在项目开发中,“删除”操作常见但方式多样,主要分为硬删除与软删除。硬删除直接从数据库移除数据,操作简单、高效,但不可恢复;适用于临时或敏感数据。软删除通过标记字段保留数据,支持恢复和审计,但增加查询复杂度与数据量;适合需追踪历史或可恢复的场景。两者各有优劣,实际开发中常结合使用以满足不同需求。
1660 4
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:详细步骤与最佳实践指南ali01n.xinmi1009fan.com
随着Web开发技术的不断进步,ASP.NET已成为一种非常流行的Web应用程序开发框架。在ASP.NET项目中,我们经常需要与数据库进行交互,特别是SQL数据库。本文将详细介绍如何在ASP.NET项目中连接SQL数据库,并提供最佳实践指南以确保开发过程的稳定性和效率。一、准备工作在开始之前,请确保您
1082 3
|
消息中间件 缓存 监控
优化微服务架构中的数据库访问:策略与最佳实践
在微服务架构中,数据库访问的效率直接影响到系统的性能和可扩展性。本文探讨了优化微服务架构中数据库访问的策略与最佳实践,包括数据分片、缓存策略、异步处理和服务间通信优化。通过具体的技术方案和实例分析,提供了一系列实用的建议,以帮助开发团队提升微服务系统的响应速度和稳定性。
|
存储 运维 监控
数据库服务器运维最佳实践
【8月更文挑战第22天】
667 2
数据库服务器运维最佳实践
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
435 2
|
关系型数据库 MySQL 数据库
MySQL数据库:基础概念、应用与最佳实践
一、引言随着互联网技术的快速发展,数据库管理系统在现代信息系统中扮演着核心角色。在众多数据库管理系统中,MySQL以其开源、稳定、可靠以及跨平台的特性受到了广泛的关注和应用。本文将详细介绍MySQL数据库的基本概念、特性、应用领域以及最佳实践,帮助读者更好地理解和应用MySQL数据库。二、MySQL
1299 5
|
SQL 数据管理 数据库
文章初学者指南:SQL新建数据库详细步骤与最佳实践
引言:在当今数字化的世界,数据库管理已经成为信息技术领域中不可或缺的一部分。作为广泛使用的数据库管理系统,SQL已经成为数据管理和信息检索的标准语言。本文将详细介绍如何使用SQL新建数据库,包括准备工作、具体步骤和最佳实践,帮助初学者快速上手。一、准备工作在开始新建数据库之前,你需要做好以下准备工作
1500 3
|
SQL NoSQL 关系型数据库
Grafana 与数据库连接:最佳实践
【8月更文第29天】Grafana 是一个开源的度量分析和可视化套件,被广泛应用于展示来自各种数据源的时间序列数据。它可以与多种数据库类型连接,从传统的 SQL 数据库到现代的 NoSQL 解决方案。本文将介绍如何通过 Grafana 连接到不同的数据源,并提供一些最佳实践。
2038 5
|
开发者 UED Java
Play Framework惊天秘密:如何让异常处理优雅得像芭蕾舞?
【8月更文挑战第31天】在Web应用开发中,异常处理至关重要,直接影响应用稳定性和用户体验。Play Framework作为轻量级Java Web框架,提供了基于Scala偏函数的灵活异常处理机制。通过实现`HttpErrorHandler`接口可定义全局异常逻辑,而在控制器中使用try-catch块则能捕获特定异常。定义自定义异常类也有助于表示特定错误情况。最佳实践包括保持处理一致性、提供有用错误信息、记录日志及分类处理异常。掌握这些技巧,能使Play应用更健壮可靠。
444 1
|
SQL 数据库 开发者
全面提速你的数据访问:Entity Framework Core性能优化指南,从预加载到批量操作的最佳实践揭秘,打造高性能数据库交互体验
【8月更文挑战第31天】本文详细介绍如何在Entity Framework Core(EF Core)中优化数据访问性能,涵盖从创建项目到定义领域模型、配置数据库上下文的最佳实践。文章通过具体代码示例讲解了预加载、惰性加载、显式加载、投影及批量操作等技术的应用,并介绍了如何使用SQL查询和调整查询性能来进一步提升效率。通过合理运用这些技术,开发者可以构建出高效且响应迅速的数据访问层,提升应用程序的整体性能和用户体验。
792 1

热门文章

最新文章