Slick: динамическое создание конъюнкций / дизъюнкций запросов

Я пытаюсь создать типизированный динамический DSL для таблицы Slick, но не знаю, как этого добиться.

Пользователи могут отправлять фильтры на сервер, отправляя фильтры в формате form / json, и мне нужно создать Slick-запрос со всем этим.

По сути, это означает преобразование класса case Scala, представляющего мои фильтры, в Slick-запрос.

Кажется, «предикаты» могут иметь 3 разных вида. Я видел черту CanBeQueryCondition. Могу ли я сложить эти разные возможные формы?

Я видел методы расширения && и || и знаю, что с этим что-то связано, но я просто не знаю, как это сделать.

По сути, у меня есть список предикатов, который принимает следующие типы:

(PatientTable) => Column[Option[Boolean]]

or

(PatientTable) => Column[Boolean]

Проблема для меня в том, что не существует единого супертипа для всех трех разных типов, имеющих CanBeQueryCondition, поэтому я действительно не знаю, как сложить предикаты с &&, поскольку после добавления в список эти предикаты различной формы требуют очень общий тип List[(PatientTable) => Column[_ >: Boolean with Option[Boolean]]].

Кроме того, я не уверен, что можно считать предикатом в Slick. Составной предикат выглядит Column[Boolean], но на самом деле метод filter принимает только параметры типа (PatientTable) => Column[Boolean]


person Sebastien Lorber    schedule 02.02.2015    source источник


Ответы (4)


Я отвечаю на свой вопрос тем, что я наконец построил.

Давайте определим простой класс case и средство отображения строк

case class User(
                    id: String = java.util.UUID.randomUUID().toString,
                    companyScopeId: String,
                    firstName: Option[String] = None,
                    lastName: Option[String] = None
                    ) 


class UserTable(tag: Tag) extends Table[User](tag,"USER") {
  override def id = column[String]("id", O.PrimaryKey)
  def companyScopeId = column[String]("company_scope_id", O.NotNull)
  def firstName = column[Option[String]]("first_name", O.Nullable)
  def lastName = column[Option[String]]("last_name", O.Nullable)

  def * = (id, companyScopeId, firstName, lastName) <>
    (User.tupled,User.unapply)
}

Понятие предиката в Slick

Я предполагаю, что понятие «предикат» - это то, что можно поместить внутрь TableQuery.filter. Но этот тип довольно сложен, так как это функция, которая принимает Table и возвращает тип с неявным CanBeQueryCondition

К сожалению, для меня существует 3 разных типа, у которых есть CanBeQueryCondition, и поместить их в список для сворачивания в один предикат кажется непростым (т.е. filter легко применить, но операторы && и || трудно применить (насколько Я пробовал)). Но, к счастью, кажется, что мы можем легко преобразовать Boolean в Colunm[Boolean] в Column[Option[Boolean]] с помощью метода расширения .?.

Итак, давайте определим наш тип предиката:

type TablePredicate[Item, T <: Table[Item]] = T => Column[Option[Boolean]]

Сворачивание списка предикатов (т. е. использование союзов / дизъюнкций, т. е. составление предложений AND и OR)

Теперь у нас есть только один тип, поэтому мы можем легко свернуть список предикатов в один

  // A predicate that never filter the result
  def matchAll[Item, T <: Table[Item]]: TablePredicate[Item,T] = { table: T => LiteralColumn(1) === LiteralColumn(1) }

  // A predicate that always filter the result
  def matchNone[Item, T <: Table[Item]]: TablePredicate[Item,T] = { table: T => LiteralColumn(1) =!= LiteralColumn(1) }

  def conjunction[Item, T <: Table[Item]](predicates: TraversableOnce[TablePredicate[Item, T]]): TablePredicate[Item,T]  = {
    if ( predicates.isEmpty ) matchAll[Item,T]
    else {
      predicates.reduce { (predicate1, predicate2) => table: T =>
        predicate1(table) && predicate2(table)
      }
    }
  }

  def disjunction[Item, T <: Table[Item]](predicates: TraversableOnce[TablePredicate[Item, T]]): TablePredicate[Item,T] = {
    if ( predicates.isEmpty ) matchNone[Item,T]
    else {
      predicates.reduce { (predicate1, predicate2) => table: T =>
        predicate1(table) || predicate2(table)
      }
    }
  }

Класс случая динамической фильтрации

Из этих примитивов предикатов мы можем начать создавать наш динамический, составной и типизированный запрос DSL на основе класса case.

case class UserFilters(
                           companyScopeIds: Option[Set[String]] = None,
                           firstNames: Option[Set[String]] = None,
                           lastNames: Option[Set[String]] = None
                           ) {

  type UserPredicate = TablePredicate[User,UserTable]


  def withFirstNames(firstNames: Set[String]): UserFilters = this.copy(firstNames = Some(firstNames))
  def withFirstNames(firstNames: String*): UserFilters = withFirstNames(firstNames.toSet)

  def withLastNames(lastNames: Set[String]): UserFilters = this.copy(lastNames = Some(lastNames))
  def withLastNames(lastNames: String*): UserFilters = withLastNames(lastNames.toSet)

  def withCompanyScopeIds(companyScopeIds: Set[String]): UserFilters = this.copy(companyScopeIds = Some(companyScopeIds))
  def withCompanyScopeIds(companyScopeIds: String*): UserFilters = withCompanyScopeIds(companyScopeIds.toSet)


  private def filterByFirstNames(firstNames: Set[String]): UserPredicate = { table: UserTable => table.firstName inSet firstNames }
  private def filterByLastNames(lastNames: Set[String]): UserPredicate = { table: UserTable => table.lastName inSet lastNames }
  private def filterByCompanyScopeIds(companyScopeIds: Set[String]): UserPredicate = { table: UserTable => (table.companyScopeId.? inSet companyScopeIds) }


  def predicate: UserPredicate = {
    // Build the list of predicate options (because filters are actually optional)
    val optionalPredicates: List[Option[UserPredicate]] = List(
      firstNames.map(filterByFirstNames(_)),
      lastNames.map(filterByLastNames(_)),
      companyScopeIds.map(filterByCompanyScopeIds(_))
    )
    // Filter the list to remove None's
    val predicates: List[UserPredicate] = optionalPredicates.flatten
    // By default, create a conjunction (AND) of the predicates of the represented by this case class
    conjunction[User,UserTable](predicates)
  }

}

Обратите внимание на использование .? для поля companyScopeId, которое позволяет подогнать необязательный столбец к нашему определению предиката Slick.

Использование DSL

val Users = TableQuery(new UserTable(_))

val filter1 = UserFilters().withLastNames("lorber","silhol").withFirstName("robert")
val filter2 = UserFilters().withFirstName("sebastien")

val filter = disjunction[User,UserTable](Set(filter1.predicate,filter2.predicate))

val users = Users.filter(filter.predicate).list

// results in 
// ( last_name in ("lorber","silhol") AND first_name in ("robert") ) 
// OR 
// ( first_name in ("sebastien") )

Заключение

Это далеко не идеально, но это первый черновик и, по крайней мере, может дать вам некоторое вдохновение :) Я хотел бы, чтобы Slick упростил создание таких вещей, которые очень распространены в других DSL запросов (например, Hibernate / JPA Criteria API)

См. Также этот Gist для получения обновленных решений.

person Sebastien Lorber    schedule 03.02.2015
comment
Я уже вижу, как вы создаете новый репозиторий на github с именем slick-criterion;) Хорошая работа, Себастьен! - person tfh; 04.02.2015
comment
спасибо :) да, это могло бы быть хорошей идеей, даже если бы в проекте не было много вещей - person Sebastien Lorber; 04.02.2015
comment
Вау, это золото! Спасибо, что поделился! - person alex88; 02.03.2016

"fold" уже является ключевым словом здесь. Или «уменьшить», поскольку вам не нужно значение заполнения. buildFilter.reduce(_ && _)

person cvogt    schedule 02.02.2015
comment
Я пару раз читал о динамической фильтрации в stackoverflow за последние дни. Может, стоит это задокументировать. Что вы думаете? Я мог бы помочь. - person tfh; 03.02.2015
comment
Да, это функция, которая нужна всем, кто создает пользовательский интерфейс с фильтрами форм. Я немного удивлен, что это не основная функция, предоставляемая Slick для запросов, поскольку она настолько общедоступна. Hibernate и JPA уже много лет используют API критериев - person Sebastien Lorber; 03.02.2015
comment
Я не совсем понимаю. .filter в сочетании с .reduce или .fold - отличный способ сделать это. Что еще тебе нужно? Компонентность Slick делает это возможным, что является главной особенностью Slick. - person cvogt; 04.02.2015
comment
Задокументировать это - хорошая идея. Было бы здорово, если бы кто-то мог подать PR. slick.typesafe.com/doc/2.1.0 (см. отредактировать эту страницу на github вверху) - person cvogt; 04.02.2015

Похоже, вам нужна более общая версия этого: Динамическая фильтрация OR - Slick. Я думаю, что мой последний пример на этой странице - это именно то, что вам нужно - это именно то, что предлагает cvogt. Надеюсь, это поможет.

person tfh    schedule 03.02.2015
comment
Спасибо, Тофер, это хорошая идея, я изучу ее. Однако в вашем примере все предикаты, кажется, имеют одинаковые shape Я имею в виду, что ваш список предикатов либо Column[Boolean], либо все Colunm[Option[Boolean]]. Также я бы хотел, чтобы свернутый фильтр оставался компонуемым, чтобы иметь возможность составлять очень сложные союзы и дизъюнкции. Что-то вроде filter1.build && filter2.build || filter3.build, где каждая из трех частей уже представляет собой свернутый набор предикатов. - person Sebastien Lorber; 03.02.2015
comment
Рад, что помог! Для меня это звучит как что-то, что должно быть поверх пятна, я не думаю, что это возможно из коробки с пятном. Надеюсь, вы поделитесь возможным решением, звучит интересно. - person tfh; 03.02.2015
comment
На самом деле начинаю преуспевать. Кажется, я могу преобразовать предикат Colunm[Boolean] в Column[Option[Boolean]], используя .?, таким образом, я могу сделать мой список однородным по общему типу, для которого я могу вызывать && или ||. Я написал функцию сворачивания, которая объединяет функции предиката (с параметром таблицы). Это немного сложнее, но не настолько. Постараюсь посмотреть, куда он идет, и опубликуйте ответ здесь - person Sebastien Lorber; 03.02.2015

Я искал то же самое и наткнулся на этот вопрос - принятый ответ был очень сильным вдохновением для того, на что я в конечном итоге остановился. Подробности см. здесь.

Единственные комментарии, которые я бы сделал по поводу принятого ответа - TablePredicate[Item, T <: Table[Item]] можно просто упростить до TablePredicate[T <: Table[_]], потому что Item никогда не используется (по крайней мере, в примере). LiteralColumn(1) === LiteralColumn(1) также может быть просто LiteralColumn(Some(true)) (делает сгенерированные запросы немного менее неудобными) - я почти уверен, что приложив немного больше усилий, они могут быть полностью устранены.

person badlander    schedule 28.08.2017