我需要一份关于编译查询执行的详细解释。我无法理解它们如何只需要编译一次以及使用它们的好处。
我需要一份关于编译查询执行的详细解释。我无法理解它们如何只需要编译一次以及使用它们的好处。
Compiled
并将结果存储在下次需要查询时不会重新计算的位置。因此,像def q1 = Compiled(...)
这样的def
没有太多意义,因为它会每次都编译。它应该是一个val
或lazy val
。而且您可能不希望将那个val放入您多次实例化的类中。相反,一个好的位置是顶级Scala单例object
中的val
,它只计算一次并保持JVM的生命周期。Compiled
没有任何神奇之处。它只允许您显式触发Slick的Scala-to-SQL编译,并返回包含SQL的值。重要的是,这允许您从实际执行查询中分开触发编译,从而使您可以编译一次,但运行多次。Slick需要将包含集合操作的AST编译成SQL语句。(实际上,如果没有编译查询,则始终必须先< em >构建 AST,但与编译时间相比,这非常快速。)
数据库服务器必须为查询构建执行计划。这意味着解析查询,将其转换为本地数据库操作,并基于数据布局(例如使用哪个索引)查找优化。即使您不使用Slick中的编译查询,也可以通过使用绑定变量来避免这部分工作,以便在不同参数集的情况下始终获得相同的SQL代码。数据库服务器保留最近使用/编译的执行计划缓存,因此只要SQL语句相同,执行计划只需进行哈希查找,无需重新计算。Slick依赖于这种缓存。没有直接从Slick到数据库服务器的通信来重用旧查询。
至于它们如何实现,在处理流式/非流式以及编译/应用/特定场景查询时有一些额外的复杂性,但有趣的入口点在< code >编译中:
implicit def function1IsCompilable[A , B <: Rep[_], P, U](implicit ashape: Shape[ColumnsShapeLevel, A, P, A], pshape: Shape[ColumnsShapeLevel, P, P, _], bexe: Executable[B, U]): Compilable[A => B, CompiledFunction[A => B, A , P, B, U]] = new Compilable[A => B, CompiledFunction[A => B, A, P, B, U]] {
def compiled(raw: A => B, profile: BasicProfile) =
new CompiledFunction[A => B, A, P, B, U](raw, identity[A => B], pshape.asInstanceOf[Shape[ColumnsShapeLevel, P, P, A]], profile)
}
每个Function
都会隐式地生成一个Compilable
对象。对于2到22参数的函数,类似的方法会自动生成。由于每个参数只需要一个Shape
,因此它们也可以是嵌套的元组、HList或任何自定义类型。(我们仍然为所有函数参数提供抽象,因为在语法上更方便编写,例如,写一个接受Tuple10
作为其参数的Function1
可能不如写一个Function10
方便。)
Shape
中有一个方法,只存在于支持编译函数的情况下:/** Build a packed representation containing QueryParameters that can extract
* data from the unpacked representation later.
* This method is not available for shapes where Mixed and Unpacked are
* different types. */
def buildParams(extract: Any => Unpacked): Packed
这种方法构建的“紧凑”表示可以生成包含QueryParameter
节点的AST,这些节点具有正确的类型。它们在编译过程中与其他字面量一样处理,只是实际值未知。提取器在顶层开始为identity
,并根据需要提取记录元素以精炼其功能。例如,如果您有一个Tuple2
参数,则AST最终将包含两个QueryParameter
节点,它们知道稍后如何提取元组的第一个和第二个参数。
稍后的时间点是在应用编译后的查询时。执行此类AppliedCompiledFunction
会使用预编译的SQL语句(或在首次使用时即时编译)并通过提取器将参数值传递填充到语句参数中。