将公用表表达式 (CTE) 与 Rails ActiveRecord 结合使用

mec*_*cov 4 ruby-on-rails common-table-expression rails-activerecord

通用表表达式是不同 RDBMS(PostgreSQL、MySQL、Oracle、SQLite3 等)中相当常见的做法,用于跨多个查询组件多次执行相同的计算或用于某些其他目的

我发现旧的 gem postgres_ext具有这样的功能。但它没有得到维护。这是 Postgres 特有的

有一些关于它的老问题,但它们是关于特定的 Rails 版本或特定的 RDBMS 或关于Arel

是否可以使用WITHAR 某种常见方式在 Rails 中使用子句?

mec*_*cov 10

在这个拉取请求之后,Rails 7.1 引入了with可以带几个参数的方法

假设我们有books一个带有整数reviews_count列的表。要定义和使用 CTE,您可以应用ActiveRecord::QueryMethods#with以下方式:

Book.with(books_with_reviews: Book.where("reviews_count > ?", 0))

# WITH books_with_reviews AS (
#   SELECT * FROM books WHERE (reviews_count > 0)
# )
# SELECT * FROM books
Run Code Online (Sandbox Code Playgroud)

它返回ActiveRecord::Relation对象,这使得它的使用非常方便和灵活

例如,定义公共表表达式后,可以将辅助语句的名称与指定的FROM子句或JOIN语句一起使用:

Book
  .with(books_with_reviews: Book.where("reviews_count > ?", 0))
  .from("books_with_reviews AS books")

# WITH books_with_reviews AS (
#  SELECT * FROM books WHERE (reviews_count > 0)
# )
# SELECT * FROM books_with_reviews AS books
Run Code Online (Sandbox Code Playgroud)
Book
  .with(books_with_reviews: Book.where("reviews_count > ?", 0))
  .joins("JOIN books_with_reviews ON books_with_reviews.id = books.id")

# WITH books_with_reviews AS (
#   SELECT * FROM books WHERE (reviews_count > 0)
# )
# SELECT * FROM books JOIN books_with_reviews ON books_with_reviews.id = books.id
Run Code Online (Sandbox Code Playgroud)

也可以使用Arel.sql以下方法传递 SQL 查询:

Book.with(popular_books: Arel.sql("some SQL literals here"))
Run Code Online (Sandbox Code Playgroud)

重要提示:仔细检查此类参数以防止 SQL 注入漏洞,此方法不得与不安全值一起使用,尤其是那些包含未经消毒的输入的值

要定义多个 CTE,只需传递一些哈希值作为参数:

Book.with(
  books_with_reviews: Book.where("reviews_count > ?", 0),
  books_with_ratings: Book.where("ratings_count > ?", 0)
)

# WITH books_with_reviews AS (
#   SELECT * FROM books WHERE (reviews_count > 0)
# ), books_with_ratings AS (
#   SELECT * FROM books WHERE (ratings_count > 0)
# )
# SELECT * FROM books
Run Code Online (Sandbox Code Playgroud)

由于with返回关系,您可以简单地将其链接多次:

Book
  .with(books_with_reviews: Book.where("reviews_count > ?", 0))
  .with(books_with_ratings: Book.where("ratings_count > ?", 0))

# WITH books_with_reviews AS (
#   SELECT * FROM books WHERE (reviews_count > 0)
# ), books_with_ratings AS (
#   SELECT * FROM books WHERE (ratings_count > 0)
# )
# SELECT * FROM books
Run Code Online (Sandbox Code Playgroud)