6

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

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

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

似乎“谓词”可以有 3 种不同的形状。我已经看到了特质CanBeQueryCondition。我可以折叠这些不同的可能形状吗?

我已经看过扩展方法&&||并且知道与此有关,但我只是不知道该怎么做。

基本上,我有一个谓词列表,它采用以下类型:

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

或者

(PatientTable) => Column[Boolean]

对我来说的问题是,对于所有 3 种具有 a 的不同类型,没有一个超类型CanBeQueryCondition,所以我真的不知道如何折叠谓词,&&因为一旦添加到列表中,这些不同形状的谓词采用非常通用的类型List[(PatientTable) => Column[_ >: Boolean with Option[Boolean]]].

另外,我不确定什么可以被视为 Slick 中的谓词。一个可组合的谓词似乎是Column[Boolean],但实际上该filter方法只接受类型的参数(PatientTable) => Column[Boolean]

4

4 回答 4

17

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

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

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的。但是这种类型相当复杂,因为它是一个接受 aTable并返回具有隐式类型的函数CanBeQueryCondition

对我来说不幸的是,有 3 种不同的类型具有 aCanBeQueryCondition并将它们放在一个列表中以折叠成一个谓词似乎并不容易(即filter易于应用,但&&and||运算符很难应用(据我尝试过) ))。但幸运的是,我们似乎可以使用扩展方法轻松地Boolean将 a转换Colunm[Boolean]为 a 。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 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)
  }

}

.?请注意for字段的用法,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以获得最新的解决方案

于 2015-02-03T16:15:50.277 回答
1

似乎想要一个更通用的版本:动态或过滤 - 光滑。我认为我在此页面上的最后一个示例正是您想要的 - 这正是 cvogt 提出的。我希望这有帮助。

于 2015-02-03T10:38:34.523 回答
1

“折叠”已经是这里的关键字。或“减少”,因为您不需要种子值。buildFilter.reduce(_ && _)

于 2015-02-02T16:06:59.810 回答
1

我一直在寻找同样的东西,并遇到了这个问题 - 接受的答案对我最终得到的启发非常大。详情在这里

我对已接受答案的唯一评论 -TablePredicate[Item, T <: Table[Item]]可以简化为,TablePredicate[T <: Table[_]]因为 Item 从未使用过(至少在示例中)。LiteralColumn(1) === LiteralColumn(1)也可以只是LiteralColumn(Some(true))(使生成的查询稍微不那么尴尬)——我很确定只要做更多的工作,这些就可以完全消除。

于 2017-08-28T15:14:45.363 回答