Как написать вложенные запросы в предложении select

Я пытаюсь создать этот SQL с SLICK 1.0.0:

    select
    cat.categoryId,
    cat.title,
    (
      select
        count(product.productId)
      from
        products product
        right join products_categories productCategory on productCategory.productId = product.productId
        right join categories c on c.categoryId = productCategory.categoryId
      where
        c.leftValue >= cat.leftValue and
        c.rightValue <= cat.rightValue
    ) as productCount
from
    categories cat
where
    cat.parentCategoryId = 2;

Моя самая успешная попытка (я убрал часть "соединения", чтобы она была более читабельной):

def subQuery(c: CategoriesTable.type) = (for {
        p <- ProductsTable

      } yield(p.id.count))
      for {
        c <- CategoriesTable
        if (c.parentId === 2)
      } yield(c.id, c.title, (subQuery(c).asColumn))

который производит SQL без скобок в подзапросе:

   select 
    x2.categoryId, 
    x2.title, 
    select count(x3.productId) from products x3 
   from 
    categories x2 
   where x2.parentCategoryId = 2

что, очевидно, является недействительным SQL Есть какие-нибудь мысли, как сделать так, чтобы SLICK поместил эти скобки в нужное место? Или, может быть, есть другой способ добиться этого?

1 ответ

Решение

Я никогда не использовал Slick или ScalaQuery, поэтому было довольно интересно узнать, как этого добиться. Slick очень расширяемый, но документация по расширению немного хитрая. Возможно, он уже существует, но это то, что я придумал. Если я сделал что-то неправильно, пожалуйста, поправьте меня.

Для начала нам нужно создать собственный драйвер. Я продлил H2Driver чтобы иметь возможность легко тестировать.

trait CustomDriver extends H2Driver {

  // make sure we create our query builder
  override def createQueryBuilder(input: QueryBuilderInput): QueryBuilder = 
    new QueryBuilder(input)

  // extend the H2 query builder
  class QueryBuilder(input: QueryBuilderInput) extends super.QueryBuilder(input) {

    // we override the expr method in order to support the 'As' function
    override def expr(n: Node, skipParens: Boolean = false) = n match {

      // if we match our function we simply build the appropriate query
      case CustomDriver.As(column, LiteralNode(name: String)) =>
        b"("
        super.expr(column, skipParens)
        b") as ${name}"

      // we don't know how to handle this, so let super hanle it
      case _ => super.expr(n, skipParens)
    }
  }
}

object CustomDriver extends CustomDriver {
  // simply define 'As' as a function symbol
  val As = new FunctionSymbol("As")

  // we override SimpleSql to add an extra implicit
  trait SimpleQL extends super.SimpleQL {

    // This is the part that makes it easy to use on queries. It's an enrichment class.
    implicit class RichQuery[T: TypeMapper](q: Query[Column[T], T]) {

      // here we redirect our as call to the As method we defined in our custom driver
      def as(name: String) = 
        CustomDriver.As.column[T](Node(q.unpackable.value), name)
    }
  }

  // we need to override simple to use our version
  override val simple: SimpleQL = new SimpleQL {}
}

Чтобы его использовать, нам нужно импортировать определенные вещи:

import CustomDriver.simple._
import Database.threadLocalSession

Затем, чтобы использовать его, вы можете сделать следующее (в моем примере я использовал таблицы из официальной документации Slick).

// first create a function to create a count query
def countCoffees(supID: Column[Int]) =
  for {
    c <- Coffees
    if (c.supID === supID)
  } yield (c.length)

// create the query to combine name and count
val coffeesPerSupplier = 
  for {
    s <- Suppliers
  } yield (s.name, countCoffees(s.id) as "test")

// print out the name and count
coffeesPerSupplier foreach { case (name, count) =>
  println(s"$name has $count type(s) of coffee")
}

Результат таков:

Acme, Inc. has 2 type(s) of coffee
Superior Coffee has 2 type(s) of coffee
The High Ground has 1 type(s) of coffee
Другие вопросы по тегам