Какой лучший способ присоединить запись к диапазону в рельсах? - PullRequest
0 голосов
/ 29 апреля 2020

Я работаю над приложением Ruby на Rails (хотя на самом деле это больше вопрос структурирования данных), где у меня есть Posts, Books и Chapters в качестве моделей. Предположим, вы хотите иметь возможность ссылаться на несколько глав в 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 в ссылку.

Есть ли способ сохранить это как некое соединение типа Range, в котором он будет хранить только первую и последнюю главу, но к нему все равно будет легко обращаться позже?

Я использую PostgreSQL если это вообще поможет.

1 Ответ

1 голос
/ 29 апреля 2020

Как отмечает @beartech, ваши опасения по поводу размера баз данных могут быть совершенно необоснованными, и это, скорее всего, всего лишь случай преждевременной оптимизации.

Но для ответа на реальный вопрос есть несколько способов хранения колеблется в Postgres. Первый «классический» способ полиглота заключается в использовании двух столбцов, а затем между:

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

Так как это просто ваниль SQL, он будет работать с любой реляционной базой данных.

Postgres также имеет диапазон собственных типов диапазонов (каламбур предназначен):

  • int4range - диапазон целых чисел
  • int8range - диапазон bigint
  • numrange - диапазон чисел c
  • tsrange - диапазон отметки времени без часового пояса
  • tstzrange - диапазон отметки времени с часовым поясом
  • daterange - диапазон даты

И это только встроенные типы.

Собственные диапазоны на самом деле не поддерживаются в ActiveRecord, но вы можете использовать API атрибутов, представленный в Rails 5 для обработки типов.

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) 
...