Slick:动态创建查询 conjunctions/disjunctions
Slick: create query conjunctions/disjunctions dynamically
我正在尝试为 Slick 创建类型安全的动态 DSL table,但不确定如何实现。
用户可以通过以 form/json 格式发送过滤器来 post 过滤器到服务器,我需要用所有这些构建一个 Slick 查询。
所以基本上这意味着将代表我的过滤器的 Scala 案例 class 转换为 Slick 查询。
看来"predicates"可以有3种不同的形状。我已经看到了特征 CanBeQueryCondition
。我可以折叠这些不同的可能形状吗?
我看过扩展方法 &&
和 ||
并且知道这与此有关,但我不知道该怎么做。
基本上,我有一个谓词列表,它采用以下类型:
(PatientTable) => Column[Option[Boolean]]
或
(PatientTable) => Column[Boolean]
我的问题是,对于所有具有 CanBeQueryCondition
的 3 种不同类型,没有一个超类型,所以我真的不知道如何使用 &&
折叠谓词一旦添加到列表中,这些不同形状的谓词采用非常通用的类型 List[(PatientTable) => Column[_ >: Boolean with Option[Boolean]]]
.
此外,我不确定什么可以被视为 Slick 中的谓词。可组合谓词似乎是 Column[Boolean]
,但实际上 filter
方法只接受 (PatientTable) => Column[Boolean]
类型的参数
"fold" 已经是这里的关键字了。或者 "reduce" 因为您不需要种子值。 buildFilter.reduce(_ && _)
似乎想要一个更通用的版本:。我认为我在此页面上的最后一个示例正是您想要的 - 这正是 cvogt 所建议的。我希望这有帮助。
我正在用我最终构建的内容回答我自己的问题。
让我们定义一个简单的案例class和行映射器
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 中谓词的概念
我假设"predicate"的概念可以放在TableQuery.filter
里面。但是这种类型相当复杂,因为它是一个函数,它接受一个 Table
和 returns 一个具有隐式 CanBeQueryCondition
的类型
对我来说不幸的是有 3 种不同的类型有一个 CanBeQueryCondition
并且将它们放在一个列表中以折叠成一个谓词似乎并不容易(即 filter
很容易应用,但是&&
和 ||
运算符很难应用(据我所试)。但幸运的是,我们似乎可以使用 .?
扩展方法轻松地将 Boolean
转换为 Colunm[Boolean]
再转换为 Column[Option[Boolean]]
。
那么让我们定义谓词类型:
type TablePredicate[Item, T <: Table[Item]] = T => Column[Option[Boolean]]
折叠谓词列表(即使用conjunctions/disjunctions,即组成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)
}
}
}
动态过滤案例class
从这些谓词原语中,我们可以开始创建基于案例的动态、可组合和类型安全的查询 DSL class。
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 条件 API)
另请参阅此 Gist 以获得最新的解决方案
我在寻找同样的东西,并遇到了这个问题 - 接受的答案对我最终登陆的内容来说是一个非常重要的灵感。详情为 here.
我对已接受的答案的唯一评论 - TablePredicate[Item, T <: Table[Item]]
可以简化为 TablePredicate[T <: Table[_]]
,因为 Item 从未使用过(至少在样本中)。 LiteralColumn(1) === LiteralColumn(1)
也可以只是 LiteralColumn(Some(true))
(使生成的查询稍微不那么尴尬)-我很确定再做一些工作,这些可以完全消除。
我正在尝试为 Slick 创建类型安全的动态 DSL table,但不确定如何实现。
用户可以通过以 form/json 格式发送过滤器来 post 过滤器到服务器,我需要用所有这些构建一个 Slick 查询。
所以基本上这意味着将代表我的过滤器的 Scala 案例 class 转换为 Slick 查询。
看来"predicates"可以有3种不同的形状。我已经看到了特征 CanBeQueryCondition
。我可以折叠这些不同的可能形状吗?
我看过扩展方法 &&
和 ||
并且知道这与此有关,但我不知道该怎么做。
基本上,我有一个谓词列表,它采用以下类型:
(PatientTable) => Column[Option[Boolean]]
或
(PatientTable) => Column[Boolean]
我的问题是,对于所有具有 CanBeQueryCondition
的 3 种不同类型,没有一个超类型,所以我真的不知道如何使用 &&
折叠谓词一旦添加到列表中,这些不同形状的谓词采用非常通用的类型 List[(PatientTable) => Column[_ >: Boolean with Option[Boolean]]]
.
此外,我不确定什么可以被视为 Slick 中的谓词。可组合谓词似乎是 Column[Boolean]
,但实际上 filter
方法只接受 (PatientTable) => Column[Boolean]
"fold" 已经是这里的关键字了。或者 "reduce" 因为您不需要种子值。 buildFilter.reduce(_ && _)
似乎想要一个更通用的版本:
我正在用我最终构建的内容回答我自己的问题。
让我们定义一个简单的案例class和行映射器
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 中谓词的概念
我假设"predicate"的概念可以放在TableQuery.filter
里面。但是这种类型相当复杂,因为它是一个函数,它接受一个 Table
和 returns 一个具有隐式 CanBeQueryCondition
对我来说不幸的是有 3 种不同的类型有一个 CanBeQueryCondition
并且将它们放在一个列表中以折叠成一个谓词似乎并不容易(即 filter
很容易应用,但是&&
和 ||
运算符很难应用(据我所试)。但幸运的是,我们似乎可以使用 .?
扩展方法轻松地将 Boolean
转换为 Colunm[Boolean]
再转换为 Column[Option[Boolean]]
。
那么让我们定义谓词类型:
type TablePredicate[Item, T <: Table[Item]] = T => Column[Option[Boolean]]
折叠谓词列表(即使用conjunctions/disjunctions,即组成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)
}
}
}
动态过滤案例class
从这些谓词原语中,我们可以开始创建基于案例的动态、可组合和类型安全的查询 DSL class。
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 条件 API)
另请参阅此 Gist 以获得最新的解决方案
我在寻找同样的东西,并遇到了这个问题 - 接受的答案对我最终登陆的内容来说是一个非常重要的灵感。详情为 here.
我对已接受的答案的唯一评论 - TablePredicate[Item, T <: Table[Item]]
可以简化为 TablePredicate[T <: Table[_]]
,因为 Item 从未使用过(至少在样本中)。 LiteralColumn(1) === LiteralColumn(1)
也可以只是 LiteralColumn(Some(true))
(使生成的查询稍微不那么尴尬)-我很确定再做一些工作,这些可以完全消除。