将记录加入 rails 中的范围的最佳方法是什么?

What is the best way to join a record to a range in rails?

我正在 Rails 应用程序上开发 Ruby(尽管这实际上更像是一个数据结构问题),其中我有 PostsBooksChapters作为模特。假设一个人希望能够引用 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。

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

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

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

因为这只是香草 SQL 它适用于任何关系数据库。

Postgres 也有一个范围 native range types(双关语):

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

这些只是内置类型。

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

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

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

在这里使用 JSON 或数组类型是非常值得怀疑的,因为您失去了关系模型的所有好处,却拥有范围列的 none 好处。如果一个模型有多个范围,我会创建一个单独的连接 table.

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)