使用Scala的“丰富我的图书馆模式”将黑匣子变成彩虹

我们的大多数服务都是用Java 8编写的,但是我们的一些微服务是在Scala中构建的。 在Java中实现问题的解决方案时,即使Scala和Java可以互操作,将其直接导入Scala可能也不是最佳解决方案,当然也不是最优雅的解决方案。

在本文中,我们将研究以下示例:面向用户的应用程序必须向每个用户提供允许其查看的数据。 对于每个用户,这将是(更大)表中记录的(可能很大)记录子集。 决定谁来看看可能是什么复杂的逻辑,即,数据中没有简单的“用户ID”列可用于过滤),并且不能仅仅包含在每个查询中。 假设我们有一个“帐户ID”列表,可以按照以下jOOQ查询中的方法进行过滤:

accountsIds列表足够小时,此查询将起作用。 但是,如果accountsIds的大小对于IN子句太大(数千个),则是个大问题。 我们需要在条件( WHERE )中使用IN子句或JOIN应用过滤器。

在Kenshoo中,我们决定使用以下方法解决此问题:如果“允许的ID”的数量足够小,我们可以简单地添加一个SQL’IN’子句(例如… AND id IN (X, Y, Z) ) ,就像我们在上面的示例中所做的那样。 但是,如果“允许的” ID列表太大,那该怎么办? 我们决定,当我们超出预定义的元素数量时(例如10个),我们将使用这些ID创建一个临时表,并将其与原始大表连接。 使用jOOQ可以轻松实现这种“动态”查询生成,因此该解决方案似乎很简单,但是我们希望找到一种通用且可重用的解决方案,以便该实现可用于多个查询中,而不仅限于Int类型键,也适用于LongString

我们开始在用Java 8编写的主服务中实现它。我们创建了一个通用类,该类将能够检测是否需要IN子句或临时表。

  • 如果我们需要IN子句,则解决方案很简单—在jOOQ select查询中添加WHERE x IN y子句。
  • 如果我们需要一个临时表,请使用所需的元素创建并填充该表,然后返回一个jOOQ select查询,该查询具有对该临时表的内部联接。 运行结束后放下桌子。

根据上述情况,该方法将返回jOOQ选择查询步骤。

实现此抽象解决方案后,我们将如何使用它? 我们将定义一个处理字符串ID的子类,如以下示例所示:

然后,使用StringIdsList的示例:

这个解决方案在Java中是优雅的,但是在实现过程中遇到了问题,这使得代码没有我们期望的那么优雅:

  • 转换 -如您所见,上面的代码大量使用了转换和类型检查(使用反射),这是jOOQ相当奇怪的接口层次结构的结果(不同的步骤产生不同的类型,没有通用的超类型)。 Scala的类型系统太强大了,无法轻易放弃—在Java中使用Scala进行反射和强制转换在Java中甚至不那么普遍,而且通常比Java更容易避免。
  • 可读性 -如果我们可以简单地假设ID列表足够小,可以使用与使用IN子句相同的方式使用,那么我们的查询将简单而流畅:

现在,通过上面的`IdsListImpl`实现,它将变成这个奇怪的“过程”代码,打破了jOOQ的“流畅” API:

如上面的示例所示,我们可以在Scala中使用完全相同的解决方案。 它确实完成了我们想要的操作,但是由于上述原因,API并不是最佳选择。 因此,让我们退后一步–我们如何想象最初问题的解决方案? 我们想要jOOQ API的扩展

在我们最美丽的梦中,我们期望它是:

我们想要的是通过我们自己的方法whereIdsIn来“神奇地”扩展jOOQs API,该方法将相关子句(IN或JOIN)添加到我们的查询中。

你怎么看?

十多年来,马丁·奥德斯基(Martin Odersky)问:“ 如果您对现有的库和API感到困惑,该怎么办? ”。 答案是— “丰富我的图书馆” (也称为“皮条客我的图书馆”,但我们不喜欢皮条客;请在此处阅读有关此模式的更多信息:思想,语言和程序,皮条客我的图书馆,作者:Martin Odersky)。

这种解决方案似乎像手套一样适合我们的问题,方法如下:

说明:

创建一个仅包含一个参数(我们要扩展的API的输入)的隐式类(从Scala 2.10开始,我们可以对类使用隐式,这可以防止我们在运行时创建类的实例)。 在我们的例子中,jOOQ选择查询步骤对象。 现在,我们创建了闪亮的新方法,我们将其称为whereIdsIn 。 它需要我们要为其添加条件的字段和条目列表。

我们添加了一个隐式参数,它将“注入” jOOQ上下文,以便我们可以使用它。 whereIdsIn的实现与我们以前在Java实现中的实现非常相似,因为我们明确定义了我们希望获得的查询(这是隐式类SelectJoinStep的输入),所以不需要强制转换/反射。 whereIdsIn方法的输出( SelectConditionStep

我们如何使用它? 就像这样简单:

就是这样!

让我们谈谈临时表解决方案-我们将创建包含所有ID的临时表。 列出的ID是用户可以看到的ID,因此它基本上是大表中所有元素的子集。 我们使用jOOQ DSLContext和一个代表临时表的类创建临时表:

该类保存与临时表有关的数据,其类型,名称以及我们需要的一列-ID列。 使用DSLContext ,我们可以轻松地使用上述参数创建表。

通过使用jOOQ批处理绑定步骤插入ID,可以很容易地填充临时表。

那么,我们获得了什么? 我们获得了更多可读,干净且易于维护的实施。 我们仅在一个隐式类中支持所有类型,而无需创建复杂的结构(通用代码,拆分为不同的数据类型等)。

免责声明:

  • 我们在与临时表的内部联接的末尾添加了一个空的WHERE子句 ,以为两种情况创建一致的返回类型— SelectConditionStep[R]
  • 我们必须在select语句或join语句之后直接调用“ whereIdsIn ”方法。 我们不能在WHERE子句之后添加它,因为它们不会产生正确的类型(它们会产生SelectConditionStep[R]而不是SelectJoinStep[R] )。

实施技巧:

不要忘记将此类放在您的主代码可见的地方。 您可以在以下位置定义它:

  • 包对象
  • 一个对象(别忘了导入它)
  • 伴随对象

当心MySQL排序规则配置。 当使用不同的字符集运行时,避免排序规则错误。

添加日志和指标。 以上示例缺少这些部分。 添加日志和指标可以在部署到生产后节省大量时间来调查问题。 例如,了解连接选项的使用频率以及查询速度会降低多少很有用。