Я отвечаю на свой вопрос тем, что я наконец построил.
Давайте определим простой класс 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