显示导航

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

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

作者:Matthew Moss

Grails 版本 5.0.1

1 Grails 培训

Grails 培训 - 由创建和积极维护 Grails 框架的开发者开发和提供!

2 开始使用

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

2.1 需要哪些内容

要完成本指南,您需要以下内容

  • 一些时间

  • 一个不错的文本编辑器或 IDE

  • 安装了 JDK 1.8 或更高版本并正确配置了 JAVA_HOME

2.2 如何完成指南

要开始执行,请执行以下操作

Grails 指南库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用,附带一些其他代码,以供你快速上手。

  • complete 完成的示例。这是指导中所述步骤的运行结果,将这些更改应用于 initial 文件夹。

如果要完成指南中的任务,请转到 initial 文件夹

  • cdgrails-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 报告文件,查看哪些测试失败以及原因。

本指南的目标是更新每个 QueryService 方法,使用动态查找器实现,并且仍然通过所有 QueryServiceSpec 单元测试。

4 更新服务

动态查找器 是在运行时生成的函数。尽管这些方法最初不存在,但 Grails 使用 Groovy 元编程钩子 methodMissingpropertyMissing 来识别你尝试调用其中一个方法的情况。然后,Grails 将检查你使用的函数名,并生成适当的实现,其中包括相应的数据库查询。

动态查找器读起来像英语命令。例如,如果我想找到所有平均游戏时长介于 30 到 90 分钟之间的棋盘游戏,则像这样

def games = Game.findAllByAverageDurationBetween(30, 90)

本指南将探讨通过更新 QueryService 中的所有方法(使用动态查找器实现,并确保新实现仍然通过 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(…​)

我们向动态查找器方法名称添加属性名称的同时,还应向动态查找器方法调用添加一个参数;本例中,我们希望使用 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(更新大小写以保持动态查找器方法名称的驼峰式大小写),并传递来自queryGamesWithAverageDurationaverageDuration参数作为动态查找器方法调用的参数。

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 按值范围查找

如果您需要查找某个属性介于两个值之间的实例,该怎么办?也许您想了解在两个日期之间进行的全部游戏(由 Domain 类 Match 表示)?

List<Game> queryMatchesPlayedBetweenDates(Date startDate, Date finishDate) {
    Match.all.findAll {
        startDate <= it.started && it.started <= finishDate
    }
}

使用 Between 比较器。如前所述,大多数比较器在动态查找器调用中需要一个匹配的参数。但是,Between 是例外之一,它需要两个参数:上界和下界。对于 QueryService.queryMatchesPlayedBetweenDates,上界和下界是开始日期和结束日期。

我们将使用 Matchstarted 属性作为要检查的属性。我们本可以选择查看 finished 属性,但根据 Match 的限制,started 不能为 null,而 finished 可以为 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 按 Null/Non-Null 查找

由于我们的数据库和 Grails 域类可能包含可能保存 Null 值的属性,因此我们需要找到查询那些情况的方法。例如,在我们的原始服务实现中,我们有两个方法用于查找哪些已玩游戏已完成,哪些仍在进行中。

int queryHowManyMatchesInProgress() {
    Match.all.count {
        it.finished == null
    }
}

int queryHowManyMatchesCompleted() {
    Match.all.count {
        it.finished != null
    }
}

用于检查 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域对象的列表。这是合理的,因为Matchgame属性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)

由于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)

现在,我们修改查询以对结果排序,先显示支持的最大玩家人数最多的游戏。由于我们修改的是排序(而不是 inclusion 的条件),因此,我们将添加 sort/order 查询参数。

    List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2, [sort: 'maxPlayers', order: 'desc'])

然后,我们仅显示前 10 个结果。添加 offset/max 参数。

    List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2, [sort: 'maxPlayers', order: 'desc', offset: 0, max: 10])

假设应用程序用户点击了 下一页 按钮,我们就可以修改上一次调用以通过更新 offset 值显示下一组 10 个结果。

    List<Game> games = Game.findAllByStrategyAndMinPlayers(true, 2, [sort: 'maxPlayers', order: 'desc', offset: 10, max: 10])

最后,正如 前面所述,前缀为 findBy 的查询将返回查询结果中第一个匹配的结果。为了使代码更稳定,您应该考虑添加一个 sort 参数,甚至把 findBy 替换为 findAllBy,在您的参数中使用 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)以查找被认为是策略和家庭的全部游戏。

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)

对于匹配的实例,这两个表达式都必须匹配。

Or

findAllByAgeLessThanOrAgeGreaterThan(8,88)

(age < 8) || (age > 88)

对于匹配的实例,至少一个表达式必须匹配。

参数

分页和排序是通过向任何动态查找器添加最终 Map 参数来完成的。该映射可能包含以下参数

参数 目的 默认值

sort

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

无默认值;如果未指定,则结果将不按顺序排列。

order

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

asc

ignoreCase

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

true

offset

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

0

max

在所有匹配的已排序的实例中,max 指定要返回的实例的上限。用于分页结果。

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

例如,下面是查找平均持续时间在 30 到 90 分钟之间,按其平均持续时间进行排序(sort: 'averageDuration')的所有游戏,从最长的开始(顺序:'desc'),只返回前 5 个结果(offset: 0, max: 5)。

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

7 你需要 Grails 的帮助吗?

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

OCI 是 Grails 的家园

认识团队