Slick:动态创建查询连词/析取

时间:2015-02-02 15:36:23

标签: scala slick play-slick

我试图为Slick表创建一个类型安全的动态DSL,但不知道如何实现这一点。

用户可以通过发送form / json格式的过滤器将过滤器发布到服务器,我需要构建一个Slick查询。

所以基本上这意味着将表示我的过滤器的Scala案例类转换为Slick查询。

似乎"谓词"可以有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]类型的参数

4 个答案:

答案 0 :(得分:15)

我正在用我最终建立的内容回答我自己的问题。

让我们定义一个简单的案例类和行映射器

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很容易应用,但{ {1}}和&&运算符难以应用(据我所知))。但幸运的是,我们似乎可以使用||扩展方法轻松地将Boolean转换为Colunm[Boolean]Column[Option[Boolean]]

所以让我们定义我们的谓词类型:

.?

折叠谓词列表(即使用连词/分离,即组成AND和OR子句)

现在我们只有一种类型,因此我们可以轻松地将谓词列表折叠成一个

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

动态过滤案例类

从这些谓词原语中,我们可以开始基于案例类创建动态,可组合和类型安全的查询DSL。

  // 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)
      }
    }
  }

请注意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) } } 字段.?的使用情况,该字段允许将非可选列符合我们对Slick谓词的定义

使用DSL

companyScopeId

<强>结论

这远非完美,但它是初稿,至少可以给你一些灵感:)我希望Slick能够更轻松地构建在其他查询DSL中非常常见的东西(如Hibernate / JPA Criteria) API)

另请参阅此Gist了解最新解决方案

答案 1 :(得分:1)

“fold”已经是此处的关键字。或者“减少”,因为您不需要播种值。 buildFilter.reduce(_ && _)

答案 2 :(得分:1)

似乎想要更通用的版本:Dynamic OR filtering - Slick。我认为我在这个页面上的最后一个例子正是你想要的 - 这正是cvogt提出的。我希望这有帮助。

答案 3 :(得分:1)

我正在寻找同样的事情,并且遇到了这个问题 - 接受的答案是对我最终登陆的一个非常重的启发。详细信息为here

我对接受的答案的唯一评论 - TablePredicate[Item, T <: Table[Item]]可以简化为TablePredicate[T <: Table[_]],因为项目从未使用过(至少在样本中)。 LiteralColumn(1) === LiteralColumn(1)也可以只是LiteralColumn(Some(true))(使生成的查询稍微不那么尴尬) - 我非常肯定会有更多的工作,这些可以完全消除。