使用 GORM 动态查找器查询数据库
本指南将演示如何使用 GORM 动态查找器高效地查询数据库。
作者:Matthew Moss
Grails 版本 3.3.1
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和交付!。
2 开始入门
2.1 需要具备的条件
要完成本指南,您需要具备以下条件
-
占点时间
-
一个不错的文本编辑器或 IDE
-
安装了 JDK 1.7 或更高版本,并适当地配置
JAVA_HOME
2.2 如何完成本指南
若要开始,请执行以下操作
-
下载并解压源代码
或
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个带有若干附加代码以助您先行开始的简单 Grails 应用。 -
complete
一个已完成的示例。它是按照指南提供的步骤进行操作,并对initial
文件夹应用这些更改的结果。
要完成指南,请转到 initial
文件夹
-
cd
进入grails-guides/querying-gorm-dynamic-finders/initial
并按照下一章节中的说明操作。
如果您 cd 进入 grails-guides/querying-gorm-dynamic-finders/complete ,便可以直接进入已完成示例 |
3 测试服务
initial
和 complete
项目不是完整的 Grails 应用。取而代之的是,有一个 Grails 服务 QueryService
,您可以对其进行编辑。对于您所做的任何更改,您都应努力确保 QueryServiceSpec
中的所有单元测试都能继续通过。
要运行单元测试
$ ./gradlew :initial:test
Starting a Gradle Daemon (subsequent builds will be faster)
:initial:compileJava NO-SOURCE
:initial:compileGroovy
:initial:buildProperties
:initial:processResources
:initial:classes
:initial:compileTestJava NO-SOURCE
:initial:compileTestGroovy
:initial:processTestResources NO-SOURCE
:initial:testClasses
:initial:test
BUILD SUCCESSFUL
如果所有单元测试都通过,您应该会看到以上内容。可以在以下位置找到完整的 HTML 报告
initial/build/reports/tests/test/index.html
.
如果您对服务进行了导致单元测试失败的更改,您将会看到如下输出
$ ./gradlew test
Starting a Gradle Daemon (subsequent builds will be faster)
:initial:compileJava NO-SOURCE
:initial:compileGroovy
:initial:buildProperties
:initial:processResources
:initial:classes
:initial:compileTestJava NO-SOURCE
:initial:compileTestGroovy
:initial:processTestResources NO-SOURCE
:initial:testClasses
:initial:test
demo.QueryServiceSpec > test what players have last name Klein FAILED
org.spockframework.runtime.ConditionFailedWithExceptionError at QueryServiceSpec.groovy:123
Caused by: groovy.lang.MissingPropertyException at QueryServiceSpec.groovy:123
demo.QueryServiceSpec > test what players have last name King FAILED
org.spockframework.runtime.ConditionFailedWithExceptionError at QueryServiceSpec.groovy:123
Caused by: groovy.lang.MissingPropertyException at QueryServiceSpec.groovy:123
37 tests completed, 2 failed
:initial:test FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':initial:test'.
> There were failing tests. See the report at:
file:///<...path...>/querying-gorm-dynamic-finders/initial/build/reports/tests/test/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED
在那种情况下,请打开 HTML 报告文件以查看哪些测试失败以及原因。
您针对本指南的目标是使用 Dynamic Finders 更新每个 QueryService
方法,并且仍会通过所有 QueryServiceSpec
单元测试。
4 更新服务
Dynamic finders 是在运行时生成的函数。尽管最初不存在这些方法,但 Grails 使用 Groovy 元编程挂钩 methodMissing
和 propertyMissing
来识别您尝试调用它们的时间。然后,Grails 会检查您所使用的函数名称并生成一个适当的实现,包括相应的数据库查询。
Dynamic finders 会阅读类似英语的命令。例如,如果我想找出所有平均游戏时长在 30 到 90 分钟之间的棋盘游戏,它的外观可能如下
def games = Game.findAllByAverageDurationBetween(30, 90)
本指南将通过使用 dynamic finders 更新 QueryService
中的所有方法来探索构建 dynamic finders 的方式,使得新实现可继续通过 QueryServiceSpec
中的所有单元测试。
4.1 根据值查找一个
要更新的第一个函数是 QueryService.queryGame
,它带有单个 name
参数。现有代码加载 Game
领域类的所有实例,并搜索名称与参数匹配的单个实例。(我们只期望找到一个,因为 Game
的 name
属性是唯一的。)
Game queryGame(String name) {
Game.all.find {
it.name == name
}
}
Game
上的 all
属性的作用就像它听起来的那样:它从数据库中将所有实例全部提取到内存中。这种糟糕的实现方式在只需要一个实例时提取了所有实例;这是一段高低效的代码!使用动态查找器获取我们想要的一个实例(例如,带提供名称的实例)。
每个动态查找器都以一个前缀开头。由于我们预计只会找到一个游戏(因为名称被限定为唯一),我们可以使用 findBy
,如果找到一个匹配的实例,它将返回该实例,如果没有找到匹配项,它将返回 null
。
findBy 还可以用于仅检索多个匹配实例中的第一个。但是,你应该考虑添加一个排序列,如 参数化查询 中所述,以使你的查询更可靠。 |
确定前缀后,接下来我们需要匹配的属性。Game
领域类有一个 name
属性,低效的实现使用了它;通过将(使用驼峰命名法)属性名称追加到前缀,我们创建动态查找器 findByName
以找到所需的 game。应该对感兴趣的领域类调用此方法:在本例中,是 Game
。因此,我们的动态查找器调用将是 Game.findByName(…)
。
在为动态查找器方法 name 添加属性名称的同时,我们还应该为动态查找器方法 call 添加一个参数;在本例中,我们要使用 queryGame
方法提供的 name
参数。因此,queryGame
的最终高效实现为
Game queryGame(String name) {
Game.findByName(name)
}
更改实现并运行测试以查看它们是否仍然通过。
在为动态查找器方法追加属性名称时,你也会为 大多数情况下 的方法调用追加一个相应参数。少数不同之处将在本指南的后面章节中指出。 |
4.2 按值查找多个
了解了按属性值查找一个实例后,我们可以按(非唯一)属性值查找多个实例,方法是更改前缀。使用 findAllBy
代替 findBy
以返回一个匹配实例列表。如果没有任何匹配项,该列表将为空。
我们来更新 QueryService.queryGamesWithAverageDuration
,这是一个用于收集具有所提供平均持续时间的所有棋盘游戏的方法。以下是现有的低效实现
List<Game> queryGamesWithAverageDuration(Integer averageDuration) {
Game.all.findAll {
it.averageDuration == averageDuration
}
}
与之前一样,这种缓慢的实现将所有记录加载到内存,然后对其进行搜索。如果仅返回匹配记录,数据库引擎将更快,并且内存负载将大大降低,因此,让我们将其更改为动态查找器。
如要查找多个记录,请使用findAllBy
前缀。追加属性名称averageDuration
(更新大小写以与动态查找器方法名称的一致驼峰命名法保持一致),并将queryGamesWithAverageDuration
中的averageDuration
参数作为动态查找器方法调用的参数传递。
List<Game> queryGamesWithAverageDuration(Integer averageDuration) {
Game.findAllByAverageDuration(averageDuration)
}
4.3 根据不等式查找
之前的方法通过值相等性搜索匹配的实例。动态查找器还可以通过不等式进行搜索:是否属性不等于、大于或小于所提供的值。
让我们看一下QueryService.queryGamesNotConsideredStrategy
,它试图查找数据库中未标记为策略游戏的全部游戏。
List<Game> queryGamesNotConsideredStrategy() {
Game.all.findAll {
it.strategy != true
}
}
将谓词写成!it.strategy 会更习惯,但对于此演示,显式地对true 进行不等比较,以更好地说明低效率版本和动态查找器版本之间的相似性。 |
由于我们希望有多个结果,因此我们仍需要使用前缀findAllBy
。相关属性是strategy
,即布尔标志。但使用动态查找器findAllByStrategy(true)
会查找到全部策略游戏,而我们想要的是非策略游戏。为了找到它们,我们需要添加一个比较器。比较器将查询从相等性测试更改为其他内容。在以下部分中,你将看到很多这样的内容。在此,我们将使用比较器NotEqual
,当它追加到动态查找器方法名称中的属性名称时,它将生成匹配不等于的查询。
我们查找非策略游戏的改进查询如下
List<Game> queryGamesNotConsideredStrategy() {
// General case: using the NotEqual comparator.
Game.findAllByStrategyNotEqual(true) (1)
// Special case: using exceptional form for Boolean properties.
Game.findAllNotStrategy() (2)
}
1 | 此查询演示了使用NotEqual 比较器进行动态查找器的常规格式。 |
2 | 此查询演示了针对布尔属性的动态查找器的 特殊格式。 |
尽管此处显示的第二种格式更加简洁易读,但使用NotEqual 比较器的第一种格式对于任何数据库类型均可同样适用;此处仅与布尔属性一同使用,以便演示比较器的工作方式。 |
如果我们要进行大于或小于比较,该怎么办?让我们看一下QueryService.queryGamesExpectedShorterThan
,它将返回平均时长短于提供数值的游戏列表。
List<Game> queryGamesExpectedShorterThan(Integer duration) {
Game.all.findAll {
it.averageDuration < duration
}
}
此时,我们知道以findAllByAverageDuration
开头。由于我们希望匹配的值小于所提供的值,因此我们应使用LessThan
比较器。同样,此比较器追加在属性名称之后,因此我们的最终改进实现为
List<Game> queryGamesExpectedShorterThan(Integer duration) {
Game.findAllByAverageDurationLessThan(duration)
}
在你需要具有属性值大于提供参数的实例时,存在一个类似的比较器:使用GreaterThan
。例如,QueryService.queryGamesRatedMoreThan
将查找评级高于提供评级的全部游戏。原始实现
List<Game> queryGamesRatedMoreThan(BigDecimal rating) {
Game.all.findAll {
it.rating > rating
}
}
我们针对此查询的动态查找器将被称为findAllByRatingGreaterThan
。
List<Game> queryGamesRatedMoreThan(BigDecimal rating) {
Game.findAllByRatingGreaterThan(rating)
}
LessThan
和 GreaterThan
比较器是严格的不等式比较;也就是说,如果比较的值相等,则认为该实例不匹配。如果还需要包括相等,请使用非严格的 LessThanEquals
和 GreaterThanEquals
比较器。
4.4 计算匹配的实例
有时,你需要的是不是查询的实际结果,而是有多少个实例确实匹配。和其他时候一样,此低效率的实现加载所有记录,然后计算与谓词匹配的记录数。
int queryHowManyGamesRatedAtLeast(BigDecimal rating) {
Game.all.count {
it.rating >= rating
}
}
上述内容的更有效的实现将调用动态查找器 Game.findAllByRatingGreaterThanEquals(rating)
,然后返回结果列表的大小。但是,如果你仅需要一个数字(匹配记录的数量),则将数据库查询的结果提取到内存中是不高效的。
此种情况下,请使用前缀 countBy
。它的作用与 findAllBy
非常相似,但它不是返回匹配的实例,而是返回匹配的实例的数量。因此,要计算评级为某个值或更高评级的游戏数量
int queryHowManyGamesRatedAtLeast(BigDecimal rating) {
Game.countByRatingGreaterThanEquals(rating)
}
现在,你可能已经注意到,当使用动态查找器以高效的方式执行 QueryService 方法时,它实际上并没有多大用处。话虽这么说,Grails 服务通常会比本指南试图教你的内容做更多的业务逻辑。在此处使用服务有助于将工作与测试分开。 |
4.5 按值范围查找
如果需要查找某个属性介于两个值之间的实例,该怎么办?也许你想了解两个日期之间进行的所有比赛(由领域类 Match
表示)?
List<Game> queryMatchesPlayedBetweenDates(Date startDate, Date finishDate) {
Match.all.findAll {
startDate <= it.started && it.started <= finishDate
}
}
使用比较器 Between
。如前所述,大多数比较器都希望在动态查找器调用中获得匹配的参数。但是,Between
是其中一个例外,它需要获得两个参数:下限和上限。在 QueryService.queryMatchesPlayedBetweenDates
的情况下,上下限是开始和结束日期。
我们将使用 Match 的 started 属性作为要检查的属性。我们可以选择使用 finished 属性,但它可以为 null,而根据 Match 的约束,started 不能为 null。 |
现在,我们了解了构建动态查找器调用的部分内容:findAllBy
用于查找多个匹配的实例; started
作为要检查的属性; Between
作为操作符,用于查找介于提供的上下限之间的属性值。以下是高效的实现方式。
List<Game> queryMatchesPlayedBetweenDates(Date startDate, Date finishDate) {
Match.findAllByStartedBetween(startDate, finishDate)
}
一个类似的比较器是 InRange
,其工作方式与 Between
相似,但接受 Groovy 范围作为参数。作为一个示例,我们想要找出所有已进行的游戏中高分是多少。我们把 90-100 定义为高分。
Range highScore = 90..100
def numHighScores = queryService.queryHowManyScoresWithinRange(highScore)
这是旧的低效率的 queryHowManyScoresWithinRange
实现方式
int queryHowManyScoresWithinRange(Range range) {
Score.all.count {
it.score in range
}
}
由于我们只想了解高分数的数量(而不是分数本身),所以会使用 countBy
前缀。我们需要查看 Score
域类中的 score
属性,并且会使用 InRange
根据提供的范围进行比较。下面是改进的实现。
int queryHowManyScoresWithinRange(Range range) {
Score.countByScoreInRange(range)
}
4.6 通过相似字符串查找
除了不等式比较(对于数字和日期等类型),我们还要进行不精确的字符串比较… 以了解一个字符串相似或“类似于”另一个字符串。下面提供了三个应该使用动态查找器改进的示例。
首先,查找具有特定姓氏的所有玩家。
List<Game> queryPlayersWithLastName(String lastName) {
Player.all.findAll {
it.name.endsWith " ${lastName}"
}
}
GORM 为字符串比较提供了动态查找器比较器 Like
,它使用搜索字符串中的字符 %
作为通配符。因此,上述内容可以更有效地编写为
List<Player> queryPlayersWithLastName(String lastName) {
Player.findAllByNameLike("% ${lastName}")
}
%
通配符可以位于搜索字符串中的任何位置:前、后、中,甚至是多个位置。它可以匹配任何字符串,甚至是空字符串。
第二个示例查找所有游戏机制,其名称包含不区分大小写的文本片段。
List<Game> queryMechanicsContaining(String text) {
Mechanic.all.findAll {
StringUtils.containsIgnoreCase it.name, text
}
}
较早的比较器 Like
区分大小写;它匹配 %
通配符出现的任何内容,但其他文本是完全匹配的。如果要忽略大小写地搜索,请使用 Ilike
比较器。您仍然可以使用通配符,如下所示。
List<Mechanic> queryMechanicsContaining(String text) {
Mechanic.findAllByNameIlike("%${text}%")
}
在我们的第三个示例中,我们找到了名称与正则表达式模式匹配的所有游戏。
List<Game> queryGamesMatching(String pattern) {
Game.all.findAll {
it.name ==~ pattern
}
}
正则表达式是一种方便的工具,供开发人员以紧凑的形式定义复杂的搜索表达式。Groovy 提供 对正则表达式的支持,该支持基于 Java 中的正则表达式 API。 |
若要进行正则表达式匹配,请在动态查找器中使用 Rlike
比较器。
List<Game> queryGamesMatching(String pattern) {
Game.findAllByNameRlike(pattern) // Rlike: not universally supported
}
Rlike 比较器不受普遍支持!仅当基础数据库支持正则表达式时,它才受支持。如果数据库不支持正则表达式,则 Rlike 将回退到 Like 行为。 |
4.7 通过空/非空查找
由于我们的数据库和 Grails 域类可以包含可能包含空值的属性,因此我们需要寻找查询这些情况的方法。例如,在我们的原始服务实现中,我们有两个方法用于查找哪些已玩游戏已完成,哪些仍在进行中。
int queryHowManyMatchesInProgress() {
Match.all.count {
it.finished == null
}
}
int queryHowManyMatchesCompleted() {
Match.all.count {
it.finished != null
}
}
检查空值的比较器是 IsNull
和 IsNotNull
。它们很特别,因为它们不要求动态查找器提供任何参数。下面是改进的实现
int queryHowManyMatchesInProgress() {
Match.countByFinishedIsNull()
}
int queryHowManyMatchesCompleted() {
Match.countByFinishedIsNotNull()
}
4.8 在列表中查找属性
在上述部分中,可以通过属性查找实例:通过相等性、不相等性、介于范围内的值。但是,有时属性的匹配值不形成一个简单的范围。使用动态查找器,可以在其中属性针对提供的列表中的项进行测试的记录中进行查找:使用InList
比较运算符。
这里有两个示例。第一个示例查找匹配String
游戏名称列表的所有Game
记录,而第二个示例查找跟踪已列出游戏的比赛记录的所有Match
记录。
List<Game> queryGamesForNames(List<String> names) {
Game.all.findAll {
it.name in names
}
}
List<Game> queryMatchesForGames(List<Game> games) {
Match.all.findAll {
it.game in games
}
}
通过使用InList
比较运算符并传入要进行匹配的项的列表,我们可以减少这些方法以提高效率。
List<Game> queryGamesForNames(List<String> names) {
Game.findAllByNameInList(names)
}
List<Match> queryMatchesForGames(List<Game> games) {
Match.findAllByGameInList(games)
}
在第二个示例QueryService.queryMatchesForGames
中,请注意该列表不包含简单类型(如Number
或String
),但实际上包含Game
域对象的列表。这是合理的,因为Match
的属性game
是Game
的一个实例。
在任何时候当你具有类似这样的关系,你可以将域类实例用作查询的一部分。作为另一个示例,Score
域引用Player
域,因此,例如,查找特定玩家的所有比分
def jeffBrown = Player.findByName('Jeff Brown')
def scores = Score.findByPlayer(jeffBrown)
与InList
比较运算符相关的是NotInList
比较运算符,正如其名称所示,它在测试属性未在提供的列表中找到的位置查找匹配实例。因此,对于QueryService.queryGamesOtherThan
的低效实现
List<Game> queryGamesOtherThan(List<Game> games) {
Game.all.findAll {
!(it in games)
}
}
可以使用NotInList
比较运算符进行替换
List<Game> queryGamesOtherThan(List<Game> games) {
Game.findAllByNameNotInList(games*.name)
}
4.9 组合查询子句
有时,你会想要通过多个属性查找记录。我们举一个例子:我们想要查找支持一定数量玩家的所有棋盘游戏。我们低效的实现如下所示
int queryHowManyGamesSupportPlayerCount(Integer playerCount) {
Game.all.count {
it.minPlayers <= playerCount && playerCount <= it.maxPlayers
}
}
此实现使用相等性和小于或等于运算符;我们已经知道如何独立搜索这些内容。例如,要查找最小玩家数不高于请求的玩家数,我们会执行此操作
List<Game> games = Game.findAllByMinPlayersLessThanEqual(playerCount)
或者,要查找最大玩家数不少于请求的玩家数,我们会执行此操作
List<Game> games = Game.findAllByMaxPlayersGreaterThanEqual(playerCount)
因为minPlayers
和maxPlayers
是不同的属性,所以我们不能使用Between
或InRange
比较运算符,因为它们对单个属性执行操作。
相反,我们在这里介绍了组合运算符。这些是布尔运算符And
和Or
。因此,如果我们要组合两个要求(即属性及其比较运算符),如果要求两个要求都通过,则使用And
将它们连接起来,如果只需要一个通过,则使用Or
将它们连接起来。
对于我们当前的示例,我们希望结合使用 findAllByMinPlayersLessThanEqual
和 findAllByMaxPlayersGreaterThanEqual
,要求两个陈述都通过(它匹配低效实现的布尔与运算符 &&
)。在组合它们时,前缀(例如 findAllBy
)只需要指定一次;然后,我们的 And
组合查询将变成 findAllByMinPlayersLessThanEqualAndMaxPlayersGreaterThanEqual
,最后的动态查找方法调用将采用两个参数:一个用于 minPlayers
,另一个用于 maxPlayers
。
int queryHowManyGamesSupportPlayerCount(Integer playerCount) {
Game.countByMinPlayersLessThanEqualsAndMaxPlayersGreaterThanEquals(playerCount, playerCount)
}
另一个类似的示例:查找支持特定数量玩家的所有棋盘游戏(例如,minPlayers
和 maxPlayers
等于同一个数字)。原始的实现
List<Game> queryGamesSupportExactPlayerCount(Integer playerCount) {
Game.all.findAll {
it.minPlayers == playerCount && it.maxPlayers == playerCount
}
}
和改进的实现
List<Game> queryGamesSupportExactPlayerCount(Integer playerCount) {
Game.findAllByMinPlayersAndMaxPlayers(playerCount, playerCount)
}
你的动态查找器不仅仅局限于两个谓词。你可以在动态查找器中多次使用 And 组合任意数量的谓词(例如查询要求)。但是,超过两个或三个谓词后,你可能会发现自己的动态查找器很难阅读。复杂查询可以使用 其他查询技术实现更清晰的可读性;这些将成为未来指南的主题。 |
如果我们不要求组合的两个部分都通过,那么我们可以使用 Or
组合器(它与布尔或运算符 ||
相比较)。在 QueryService.queryGamesConsideredFamilyOrParty
中,原始的实现
List<Game> queryGamesConsideredFamilyOrParty() {
Game.all.findAll {
it.family || it.party
}
}
可以用一个改进的实现来替换
List<Game> queryGamesConsideredFamilyOrParty() {
Game.findAllByFamilyOrParty(true, true)
}
虽然动态查找器可以使用任意数量的 And 组合器或任意数量的 Or 组合器,但在同一个动态查找器中不能同时使用 And 和 Or 。因此,这样的查询和类似的查询都是无效的:Game.findByMinPlayersAndMaxPlayersOrParty(2, 5, true) 。要指定这样的条件,你需要使用 其他查询技术。 |
4.10 参数化查询
当对结果进行排序和分页时,任何数据库查询都会变得更有用。对于任何动态查找器,除了所使用属性/比较器所需的那些参数之外,还可以添加一个额外的 Map
参数来管理排序和分页。(此参数映射与 list()
方法使用的相同。)
要对结果进行排序,请在查询中使用 sort
、order
和 ignoreCase
。
-
sort
指定用于对所有实例进行排序的属性。 -
order
是一个字符串,它指定排序是升序/增序(asc
)还是降序/减序(desc
)。 -
ignoreCase
指定在排序期间是否忽略大小写。
要对结果进行分页,请在查询中使用 offset
和 max
。
-
offset
指定要返回的第一个结果的索引。 -
max
指定要返回的最大结果数。
分页参数会在排序参数之后应用。
以一个示例来说明,我想要找出可以同时支持至少两名玩家的所有策略游戏。
List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2)
现在对示例进行修改,对结果进行排序,首先显示支持最多玩家人数的策略游戏。由于我们修改了排序(而非包含在结果中的条件),我们添加了 sort
/order
查询参数。
List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2, [sort: 'maxPlayers', order: 'desc'])
接下来,我们仅显示前十个结果。添加 offset
/max
参数。
List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2, [sort: 'maxPlayers', order: 'desc', offset: 0, max: 10])
假设应用程序的用户单击了 Next
按钮,我们通过更新 offset
值对最后一项调用进行修改,以显示下一组十个结果。
List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2, [sort: 'maxPlayers', order: 'desc', offset: 10, max: 10])
最后,正如 前面 提到的,前缀 findBy
会返回查询的第一个匹配结果。对于稳健的代码,你应该考虑添加一个 sort
参数,或者用 findAllBy
替换 findBy
,并在参数中使用 max: 1
。例如
// This:
Game bestTwoPlayerGame = Game.findByMinPlayerAndMaxPlayer(2, 2, [sort: 'rating', order: 'desc'])
// is equivalent to this:
List<Game> bestTwoPlayerGame = Game.findAllByMinPlayerAndMaxPlayer(2, 2, [sort: 'rating', order: 'desc', max: 1])
除了在此列出的参数之外,还有一些参数。请参阅 list 文档,了解它们是否适用于你的情况。 |
5 个实用的特殊形式
动态查找器是构建简单数据库查询的利器:可读性强、全高效能,且几乎不用麻烦。除了前面部分描述的所有可能性之外,还有一些特殊且实用的形式可以添加到你的工具箱中。
5.1 查询,然后创建/保存
开发人员经常需要查询数据库,以查看特定实例是否存在,如果不存在,他们在执行其他工作之前就希望创建或保存它。你见过或写过多少次此类代码?
def category = Category.findByName('Zombies')
if (!category) {
category = new Category(name: 'Zombies')
category.save(flush: true)
}
// Now do something useful with `category`.
def game = Game.findByNameAndMinPlayersAndMaxPlayers('SET', 2, 20)
if (!game) {
game = new Game(name: 'SET', minPlayers: 2, maxPlayers: 20)
}
// Now do something useful with `game`.
GORM 为动态查找器提供了两个前缀,有助于消除此类样板:findOrCreateBy
和 findOrSaveBy
。这两者都尝试使用指定查询查找匹配的实例。如果找到,则会返回该实例。如果没有找到,则会创建一个新实例,并使用查询命名的属性值设置其属性。如果使用 findOrSaveBy
,也将保存该实例。以下语句等效于上述语句
Category category = Category.findOrSaveByName('Zombies')
Game game = Game.findOrCreateByNameAndMinPlayersAndMaxPlayers('SET', 2, 20)
在需要填写其他属性、构建关系或确保在保存之前满足某些约束条件的情况下,findOrCreateBy
通常可能是更合适的选择。但是,请记住,findOrCreateBy
不会 保存,因此在实例上调用 save
以使你的更改持久化。
在使用前缀 findOrCreateBy 和 findOrSaveBy 时,仅允许完全匹配,因为如果没有找到,你的动态查找器会尝试使用查询中提供的的值创建新实例。因此,例如,Game.findOrCreateByRating(5.5) 有效,但不是创建动态查找器 Game.findOrCreateByRatingGreaterThan(5.5) 。 |
5.2 简化布尔型匹配
我们之前看到,你可以通过它们的布尔型属性来匹配实例,例如这些示例
// Finds all strategy games.
Game.findAllByStrategy(true)
// Finds all non-party games.
Game.findAllByPartyNotEqual(true)
// Finds all family games the support at least four players.
Game.findAllByFamilyAndMinPlayersGreaterThanEquals(true, 4)
可以将包含一个布尔属性作为查询一部分的动态查找器缩短,方法是将属性名合并到前缀中并删除相应的参数。因此上述示例可以缩短为
// Finds all strategy games.
Game.findAllStrategy()
// Finds all non-party games.
Game.findAllNotParty()
// Finds all family games the support at least four players.
Game.findAllFamilyByMinPlayersGreaterThanEquals(4)
如果你仅检查布尔属性,则可以删除前缀的By
部分(因此findAllByBooleanProperty
变为findAllBooleanProperty
)。
对 true
值进行检查会使用附加到 findAll
的属性名,而对 false
值进行检查会使用附加到 findAllNot
的属性名。
如果你有其他条件(如上述第三个示例),然后在先前的前缀中将这些条件附加在By
后面。
此方式仅能将一个布尔属性合并到前缀中。如果你有其他布尔属性要在同一查询中匹配,请按照之前一样添加它们,例如使用 Game.findAllStrategyByFamily(true) 来查找所有既是 Strategy 又属于 Family 的游戏。 |
5.3 与分离标准和命名查询的链接
虽然动态查找器对于简单查询非常方便,但更复杂的查询需要其他技术。虽然此处未讨论这些高级查询技术,但可以通过将动态查找器链接(即附加)到这些技术上,进一步限制这些高级技术的查询结果。
例如,让我们查找所有使用涉及“手部管理”机制的游戏,并且平均持续时间少于 120 分钟。可以使用Game
域类的 命名查询 gamesWithMechanic
来查找所有使用特定机制的游戏。
static namedQueries = {
gamesWithMechanic { aMechanic ->
mechanics {
eq 'id', aMechanic.id
}
}
}
// Find all games using the "Hand Management" game mechanic.
Mechanic m = Mechanic.findByName("Hand Management")
def games = Game.gamesWithMechanic(m)
要查找平均持续时间少于 120 分钟的游戏,我们知道使用调用Game.findAllByAverageDurationLessThan(120)
。但是,它会搜索所有持续时间少于 120 分钟的游戏,而不管游戏机制如何。若要查找匹配这两个条件的游戏,请将动态查找器附加到命名查询。
List<Game> queryGamesWithMechanicNoLongerThanDuration(Mechanic mechanic, int duration) {
// Games provides a named query, 'gamesWithMechanic', to find all games that employ the provided game mechanic.
// Dynamic finders can be chained onto named queries to narrow the results.
Game.gamesWithMechanic(mechanic).findAllByAverageDurationLessThan(duration)
}
List<Game> queryGamesInCategoryWithAverageDuration(Category category, int duration) {
// Here is a detached criteria to find all games within the specific category.
DetachedCriteria<Game> detachedCriteria = new DetachedCriteria(Game).build {
categories {
eq 'id', category.id
}
}
// Dynamic finders can be chained onto detached criteria to narrow the results.
detachedCriteria.findAllByAverageDuration(duration)
}
// Find all "Economic" games of approximately 120 minutes.
def category = Category.findByName("Economic")
def games = queryGamesInCategoryWithAverageDuration(category, 120)
这些各种更高级的技术(命名查询、Where 查询、标准和分离标准查询以及 HQL)将在未来的 Grails 指南中进行深入探讨。 |
6 动态查找器总结
前缀
在这些前缀中,Flag
表示域类的某个布尔属性;将其替换为你希望测试的布尔属性的名称。
前缀 | 用途 |
---|---|
|
查找与查询匹配的单个实例。 |
|
查找与查询匹配的多个实例。 |
|
统计有多少个实例与查询匹配。 |
|
特殊形式:如果未找到现有匹配项,则创建一个新实例。 |
|
特殊方式:如果未找到现有的匹配,则创建并保存一个新实例。 |
|
布尔型属性的特殊形式:找到指定属性为 true 的所有实例。可以通过追加“By”扩展该查询。 |
|
布尔型属性的特殊形式:找到指定属性为 false 的所有实例。可以通过追加“By”扩展该查询。 |
比较器
在下表中,prop
表示领域类的某个属性;使用要测试的实际属性的名称替换它。使用实际值(或类似值)替换 val
(或类似值)以进行比较。
比较器 | 示例 | 同等的 Groovy 代码 | 用途 |
---|---|---|---|
<none> |
|
|
测试属性是否等于值。 |
|
|
|
测试属性是否不等于值。 |
|
|
|
测试属性是否小于值。 |
|
|
|
测试属性是否小于或等于值。 |
|
|
|
测试属性是否大于值。 |
|
|
|
测试属性是否大于或等于值。 |
|
|
|
测试属性是否介于 lowerVal 和 upperVal 之间。需要两个参数。 |
|
|
|
测试属性是否位于 val 指定的范围内( |
|
|
没有确切的等价项。类似于 |
使用通配符字符串比较测试属性是否匹配。使用 |
|
|
与 |
与 |
|
|
|
使用正则表达式测试属性是否匹配。 |
|
|
|
测试属性是否为 null。不需要参数。 |
|
|
|
测试属性是否不为 null。不需要参数。 |
组合器
在单个动态查找器方法名称中,你可以多次使用 And
或 Or
来组合多个标准,但不能在同一个动态查找器中同时使用 And
和 Or
。
组合器 | 示例 | 同等的 Groovy 代码 | 用途 |
---|---|---|---|
|
|
|
实例要匹配,则这两个表达式都必须匹配。 |
|
|
|
至少一个表达与实例相符。 |
参数
对任何动态查找程序添加一个最终的 Map
参数来处理分页和排序。该映射可能包含以下参数
参数 | 用途 | 默认值 |
---|---|---|
|
根据指定属性对所有匹配实例进行排序。在应用 |
无默认值;如果未指定,则结果不会排序。 |
|
将排序顺序设置为升序/增序 ( |
|
|
如果为 |
|
|
从所有匹配的排序实例中, |
|
|
从所有匹配的排序实例中, |
无默认值;如果未指定,则返回所有结果。 |
例如,以下操作按平均持续时间 (sort: 'averageDuration'
) 排序找出所有平均持续时间在 30 到 90 分钟之间的游戏,从最长的开始,按降序排列 (order: 'desc'),并仅返回前五个 (offset: 0, max: 5
) 结果。
Game.findAllByAverageDurationInRange(30..90, [sort: 'averageDuration', order: 'desc', offset: 0, max: 5])