在Rails 7中添加对跨集群关联的支持

Eileen M. Uchitelle的形象

自从我们在GitHub上迈出了这一步从我们的Rails分支升级努力工作保持最新的版本,我们一直在寻找改进Rails框架上游的方法。我们通过多种方式做到这一点——在Rails main上运行GitHub,报告并修复我们发现的bug,最重要的是将功能推向上游,让整个Ruby社区都能从中受益。

最近,我们提取了内部功能,当一个关联跨多个数据库时禁用连接查询。在我们研究这个领域之前,Rails不支持处理跨集群的关联;团队必须编写SQL来实现这一点。

背景

在GitHub上,我们在Rails中配置了30个数据库——15个初级数据库和15个副本。我们使用“功能分区”来分割数据,这意味着这15个主节点都有不同的模式。相反,“水平分片”方法将拥有15个具有相同模式的分片。

虽然在MySQL中有一些跨集群连接的解决方案,但它们通常不具有性能,或者需要额外的设置。如果没有这些解决方案,尝试从集群a中的一个表连接到集群B中的一个表将会导致错误。为了克服这个限制,团队必须编写SQL,从第一个表中选择id,然后在第二个查询中使用它来查找适当的记录。这是额外的工作,很容易出错。通过在Rails中实现非连接查询,我们有机会使这个过程更加顺利。

让我们看一些代码,看看它是如何工作的:

假设我们有三种模型:人类,治疗

#数据库表狗类动物狗< AnimalsRecord has_many:将通过:人类has_many:人类最终人类在数据库类#表< PeopleRecord has_many:对待has_many:狗#表将在数据库的默认类治疗< ApplicationRecord has_many:狗,通过:人类has_many:人类

如果我们的Rails应用程序代码加载dog.treats关联,通常会自动执行连接查询:

选择治疗。* FROM treat INNER JOIN humans ON treat。human_id =人类。id在人类。dog_id = 2

看看遗传链,我们可以看到治疗,人类它们都继承自不同的基类。这些基类中的每一个都属于不同的数据库连接,这意味着所有三个模型的记录都存储在不同的数据库中。

由于数据存储在多个主服务器上,所以当连接启动时dog.treats,我们将看到一个应用程序错误:

ActiveRecord:: StatementInvalid people_db_cluster(表”。人类不存在)

Rails提供的最佳开箱即用特性之一就是为您生成SQL。但由于GitHub的数据存储在不同的数据库中,我们无法再利用这一点。我们有机会以一种有益于我们的工程师和Rails社区中使用多个数据库的其他人的方式改进Rails。

实现

在我们从事这一领域的工作之前,从事任何跨数据库边界的关联工作的工程师将被迫手动查询id,而不是使用Active Record的关联api。编写SQL很容易出错,这违背了活动记录方便方法的目的,比如dog.treats

两年多以前,我们开始尝试使用一个内部gem来禁用跨数据库关联的连接。我们选择首先在Rails之外实现它,这样我们就可以在合并到Rails之前解决大部分bug。我们想要确保我们能够在生产中成功地使用它,并且它不会在开发中产生任何重大的摩擦或在生产中产生任何性能问题。万博足球竞猜app这就是Rails许多流行特性的开发过程。我们经常从大型生产应用程序中提取实现——如果这是我们需要的,也是许多应用程序可以从中受益万博足球竞猜app的,我们首先使其稳定,然后将其上游到Rails。

整体实现相对较小。为了实现禁用连接,我们添加了一个选项has_many:通过协会称disable_joins.当设置为真正的对于关联,Rails将为每个数据库生成单独的查询,而不是一个连接查询。

这需要成为关联的一个选项,而不是在运行时执行,因为Rails关联是惰性加载的——SQL是在关联对象创建时生成的,这意味着在Rails运行SQL以加载时生成dog.treats连接将已经生成。在Rails中添加这个选项之后,我们实现了一个新的作用域类,它将处理订单、限制、作用域和其他选项。

现在,应用程序可以将以下内容添加到它们的关联中,以确保Rails生成两个或更多查询,而不是连接:

类Dog < AnimalsRecord has_many: treats, through::humans, disable_joins: true has_many: humans end

这就是禁用跨数据库服务器的关联生成连接所需要的全部内容!

现在,调用dog.treats将生成以下SQL:

选择“人类”。"id"来自"humans" WHERE "humans"。“dog_id”= ?[["dog_id", 1]]选择"treats"。* FROM "treats" WHERE "treats"。“human_id”(?),?, ?) [["human_id", 1], ["human_id", 2], ["human_id", 3]]

警告

在使用这个新特性时,有几个重要的注意事项需要记住。需要禁用连接的应用程序可能会发现这些关联具有较慢的数据库性能。无论您是手动编写SQL还是使用Rails的newdisable_joins特性。从根本上说,如果您跨多个数据库执行多个查询,这可能比在一个数据库上执行单个连接查询要慢。在使用此功能之前,确保查询是有效的,并且有适当的索引是非常重要的。和往常一样,在对数据库查询进行重大更改时,重要的是进行基准测试,并了解这些更改将如何影响应用程序。

此外,如果您的查询依赖于连接数据库的订单和限制,您可能会看到请求的性能影响。当两个查询连接时,MySQL可以根据连接的表执行顺序(例如,order by humans.human_id它会根据人类的ID来订购返回的食物)。但是,当您拆分查询时,数据库不能应用该顺序。为了解决这个问题,Rails根据连接时返回的顺序在内存中对记录进行排序。这保留了预期的行为,但由于顺序和限制是在内存中执行的,因此您通常希望避免在数十万条记录上执行这些操作。

结论

四年前,在这个级别上为Rails做贡献只是我们希望有一天能够做到的事情。我们在升级方面远远落后,很难对框架做出改变。这看起来可能是一个小变化,但它清楚地说明了我们为改进应用程序中的技术债务所做的艰苦工作,并确保我们在任何可能的时候都能回馈社区。

通过添加对跨数据库处理关联的支持,我们可以帮助其他应用程序在流量和数据增长时进行扩展。此外,通过将这些代码推入Rails和我们的私有内部gem,我们会发现更多的改进和边缘情况的应用程序不是GitHub。随着我们在GitHub上继续成长和改进Rails,我们将继续为整个社区改进它。这个拉请求只是一个例子,说明我们打算如何在未来几年做到这一点。