显示导航

使用 GORM 动态查找器查询数据库

本指南将演示如何使用 GORM 动态查找器高效地查询数据库。

作者:Matthew Moss

Grails 版本 3.3.1

1 Grails 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和交付!。

2 开始入门

在指南中,您将更新 Grails 服务以更有效地查询数据库。服务方法当前从表中加载所有记录并在内存中对其进行搜索;您将更改这些方法以使用 GORM 动态查找器,它们在各方面更有效率。

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 测试服务

initialcomplete 项目不是完整的 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 元编程挂钩 methodMissingpropertyMissing 来识别您尝试调用它们的时间。然后,Grails 会检查您所使用的函数名称并生成一个适当的实现,包括相应的数据库查询。

Dynamic finders 会阅读类似英语的命令。例如,如果我想找出所有平均游戏时长在 30 到 90 分钟之间的棋盘游戏,它的外观可能如下

def games = Game.findAllByAverageDurationBetween(30, 90)

本指南将通过使用 dynamic finders 更新 QueryService 中的所有方法来探索构建 dynamic finders 的方式,使得新实现可继续通过 QueryServiceSpec 中的所有单元测试。

4.1 根据值查找一个

要更新的第一个函数是 QueryService.queryGame,它带有单个 name 参数。现有代码加载 Game 领域类的所有实例,并搜索名称与参数匹配的单个实例。(我们只期望找到一个,因为 Gamename 属性是唯一的。)

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

LessThanGreaterThan 比较器是严格的不等式比较;也就是说,如果比较的值相等,则认为该实例匹配。如果还需要包括相等,请使用非严格的 LessThanEqualsGreaterThanEquals 比较器。

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 的情况下,上下限是开始和结束日期。

我们将使用 Matchstarted 属性作为要检查的属性。我们可以选择使用 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
    }
}

检查空值的比较器是 IsNullIsNotNull。它们很特别,因为它们不要求动态查找器提供任何参数。下面是改进的实现

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中,请注意该列表不包含简单类型(如NumberString),但实际上包含Game域对象的列表。这是合理的,因为Match的属性gameGame的一个实例

在任何时候当你具有类似这样的关系,你可以将域类实例用作查询的一部分。作为另一个示例,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)

因为minPlayersmaxPlayers是不同的属性,所以我们不能使用BetweenInRange比较运算符,因为它们对单个属性执行操作。

相反,我们在这里介绍了组合运算符。这些是布尔运算符AndOr。因此,如果我们要组合两个要求(即属性及其比较运算符),如果要求两个要求都通过,则使用And将它们连接起来,如果只需要一个通过,则使用Or将它们连接起来。

对于我们当前的示例,我们希望结合使用 findAllByMinPlayersLessThanEqualfindAllByMaxPlayersGreaterThanEqual,要求两个陈述都通过(它匹配低效实现的布尔与运算符 &&)。在组合它们时,前缀(例如 findAllBy)只需要指定一次;然后,我们的 And 组合查询将变成 findAllByMinPlayersLessThanEqualAndMaxPlayersGreaterThanEqual,最后的动态查找方法调用将采用两个参数:一个用于 minPlayers,另一个用于 maxPlayers

    int queryHowManyGamesSupportPlayerCount(Integer playerCount) {
        Game.countByMinPlayersLessThanEqualsAndMaxPlayersGreaterThanEquals(playerCount, playerCount)
    }

另一个类似的示例:查找支持特定数量玩家的所有棋盘游戏(例如,minPlayersmaxPlayers 等于同一个数字)。原始的实现

    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 组合器,但在同一个动态查找器中不能同时使用 AndOr。因此,这样的查询和类似的查询都是无效的:Game.findByMinPlayersAndMaxPlayersOrParty(2, 5, true)。要指定这样的条件,你需要使用 其他查询技术

4.10 参数化查询

当对结果进行排序和分页时,任何数据库查询都会变得更有用。对于任何动态查找器,除了所使用属性/比较器所需的那些参数之外,还可以添加一个额外的 Map 参数来管理排序和分页。(此参数映射与 list() 方法使用的相同。)

要对结果进行排序,请在查询中使用 sortorderignoreCase

  • sort 指定用于对所有实例进行排序的属性。

  • order 是一个字符串,它指定排序是升序/增序(asc)还是降序/减序(desc)。

  • ignoreCase 指定在排序期间是否忽略大小写。

要对结果进行分页,请在查询中使用 offsetmax

  • 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 为动态查找器提供了两个前缀,有助于消除此类样板:findOrCreateByfindOrSaveBy。这两者都尝试使用指定查询查找匹配的实例。如果找到,则会返回该实例。如果没有找到,则会创建一个新实例,并使用查询命名的属性值设置其属性。如果使用 findOrSaveBy,也将保存该实例。以下语句等效于上述语句

    Category category = Category.findOrSaveByName('Zombies')

    Game game = Game.findOrCreateByNameAndMinPlayersAndMaxPlayers('SET', 2, 20)

在需要填写其他属性、构建关系或确保在保存之前满足某些约束条件的情况下,findOrCreateBy 通常可能是更合适的选择。但是,请记住,findOrCreateBy 不会 保存,因此在实例上调用 save 以使你的更改持久化。

在使用前缀 findOrCreateByfindOrSaveBy 时,仅允许完全匹配,因为如果没有找到,你的动态查找器会尝试使用查询中提供的的值创建新实例。因此,例如,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)
    }

同样,动态查找器可以附加到 分离标准Where 查询 中,以进一步限制标准查询的结果。

    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 表示域类的某个布尔属性;将其替换为你希望测试的布尔属性的名称。

前缀 用途

findBy

查找与查询匹配的单个实例。

findAllBy

查找与查询匹配的多个实例。

countBy

统计有多少个实例与查询匹配。

findOrCreateBy

特殊形式:如果未找到现有匹配项,则创建一个新实例。

findOrSaveBy

特殊方式:如果未找到现有的匹配,则创建并保存一个新实例。

findAllFlag[By]

布尔型属性的特殊形式:找到指定属性为 true 的所有实例。可以通过追加“By”扩展该查询。

findAllNotFlag[By]

布尔型属性的特殊形式:找到指定属性为 false 的所有实例。可以通过追加“By”扩展该查询。

比较器

在下表中,prop 表示领域类的某个属性;使用要测试的实际属性的名称替换它。使用实际值(或类似值)替换 val(或类似值)以进行比较。

比较器 示例 同等的 Groovy 代码 用途

<none>

findAllByProp(val)

prop == val

测试属性是否等于值。

NotEqual

findAllByPropNotEqual(val)

prop != val

测试属性是否不等于值。

LessThan

findAllByPropLessThan(val)

prop < val

测试属性是否小于值。

LessThanEquals

findAllByPropLessThanEquals(val)

prop <= val

测试属性是否小于或等于值。

GreaterThan

findAllByPropGreaterThan(val)

prop > val

测试属性是否大于值。

GreaterThanEquals

findAllByPropGreaterThanEquals(val)

prop >= val

测试属性是否大于或等于值。

Between

findAllByPropBetween(lowerVal, upperVal)

(lowerVal <= prop) && (prop <= upperVal)

测试属性是否介于 lowerVal 和 upperVal 之间。需要两个参数。

InRange

findAllByPropInRange(val)

prop in val

测试属性是否位于 val 指定的范围内(Range 类型)。

Like

findAllByPropLike(val)

没有确切的等价项。类似于 prop.endsWith(val)prop.startsWith(val)prop.contains(val),具体取决于搜索字符串。

使用通配符字符串比较测试属性是否匹配。使用 % 作为通配符。区分大小写。

Ilike

findAllByPropIlike(val)

Like 相同。

Like 相同,但忽略大小写。

Rlike

findAllByPropRlike(val)

prop ==~ val

使用正则表达式测试属性是否匹配。

IsNull

findAllByPropIsNull()

prop == null

测试属性是否为 null。不需要参数。

IsNotNull

findAllByPropIsNull()

prop != null

测试属性是否不为 null。不需要参数。

组合器

在单个动态查找器方法名称中,你可以多次使用 AndOr 来组合多个标准,但不能在同一个动态查找器中同时使用 AndOr

组合器 示例 同等的 Groovy 代码 用途

And

findByNameAndYearLessThan('Foo', 2017)

(name == 'Foo') && (year < 2017)

实例要匹配,则这两个表达式都必须匹配。

findAllByAgeLessThanOrAgeGreaterThan(8,88)

(年龄 < 8) || (年龄 > 88)

至少一个表达与实例相符。

参数

对任何动态查找程序添加一个最终的 Map 参数来处理分页和排序。该映射可能包含以下参数

参数 用途 默认值

sort

根据指定属性对所有匹配实例进行排序。在应用 offsetmax 参数之前进行排序。

无默认值;如果未指定,则结果不会排序。

order

将排序顺序设置为升序/增序 ('asc') 或降序/减序 ('desc')。

asc

ignoreCase

如果为 true,则排序不区分大小写;如果为 false,则区分大小写。

true

offset

从所有匹配的排序实例中,offset 指定要返回的第一个实例的索引。用于分页结果。

0

max

从所有匹配的排序实例中,max 指定要返回的最大实例数。用于分页结果。

无默认值;如果未指定,则返回所有结果。

例如,以下操作按平均持续时间 (sort: 'averageDuration') 排序找出所有平均持续时间在 30 到 90 分钟之间的游戏,从最长的开始,按降序排列 (order: 'desc'),并仅返回前五个 (offset: 0, max: 5) 结果。

    Game.findAllByAverageDurationInRange(30..90, [sort: 'averageDuration', order: 'desc', offset: 0, max: 5])

7 在使用 Grails 时是否需要帮助?

Object Computing, Inc. (OCI) 赞助了此指南的创建。可使用各种咨询和支持服务。

Grails 的家园是 OCI

认识团队