首页 > 解决方案 > 将记录加入 Rails 范围的最佳方法是什么?

问题描述

我正在开发一个 Ruby on Rails 应用程序(尽管这实际上更像是一个数据结构问题),我拥有PostsBooksChapters作为模型。假设 a 希望能够引用 a 中的多个章节,Post并且以后能够通过他们引用的章节和书籍来过滤帖子。以以后易于查询的方式将这些记录连接在一起的最佳方法是什么?

我的第一个想法是典型的has_many :through联想。

class Post < ApplicationRecord
  has_many :post_chapters
  has_many :chapters, through: :post_chapters
end

class PostChapter < ApplicationRecord
  belongs_to :post
  belongs_to :chapter
end

class Chapter < ApplicationRecord
  belongs_to :book
  has_many :post_chapters
  has_many :posts, through: :post_chapters
end

class Book < ApplicationRecord
  has_many :chapters
end

如果我只需要存储对几章的引用,这将非常有效。我最终会PostChapter为每章引用一个额外的记录。但是如果有人引用第 1 章到第 1000 章会发生什么?然后应用程序需要创建 1000 条记录才能判断第 X 章是否包含在参考中。

有没有办法将它存储为某种范围连接,它只存储第一章和最后一章,但以后仍然很容易查询?

如果有帮助的话,我正在使用 PostgreSQL。

标签: ruby-on-railsjoinrangedata-modelinghas-many-through

解决方案


正如@beartech 所指出的,您对数据库大小的担忧可能完全没有根据,这很可能只是过早优化的情况。

但要回答实际问题,有几种方法可以在 Postgres 中存储范围。第一种“经典”多语言方式是使用两列,然后使用:

Post.where("? BETWEEN posts.starting_chaper AND posts.ending_chapter", 99)

因为这只是普通的 SQL,它可以在任何关系数据库上工作。

Postgres 还有一系列原生范围类型(​​双关语):

  • int4range - 整数范围
  • int8range - bigint 的范围
  • numrange - 数值范围
  • tsrange - 没有时区的时间戳范围
  • tstzrange - 带有时区的时间戳范围
  • daterange - 日期范围

这些只是内置类型。

在 ActiveRecord 中并没有真正支持原生范围,但您可以使用Rails 5中引入的属性 API来处理类型转换。

class Chapter < ApplicationRecord
  attribute :page_range, range: true
end

这里的一个巨大优势是当涉及到查询时,因为 PG 知道该列实际上是一个范围,并且与以前的解决方案相比可以创建一个非常有效的查询计划。

在这里使用 JSON 或数组类型是非常有问题的,因为您失去了关系模型的所有好处并且没有范围列的好处。如果一个模型有多个范围,我会创建一个单独的连接表。

class Post < ApplicationRecord
  has_many :post_chapters
  has_many :chapter_ranges
  has_many :chapters, through: :post_chapters
end

class ChapterRange
  belongs_to :post
  attribute :chapters, range: true
end

# Check if one chapter is contained in range:
Post.joins(:chapter_ranges)
    .where("? @> chapter_ranges.chapters" 10) 

# range is contained by
Post.joins(:chapter_ranges)
    .where("int4range(?, ?) @> chapter_ranges.chapters" 2, 4) 

# overlap
Post.joins(:chapter_ranges)
    .where("int4range(?, ?) && chapter_ranges.chapters" 2, 4) 

推荐阅读