(快速参考)

11 验证

版本 6.2.0

11 验证

Grails 验证功能建立在 Spring 验证器 API 和数据绑定功能之上。但 Grails 更进一步,通过其约束机制提供一种统一的方法来定义验证“约束”。

Grails 中的约束是一种以声明式指定验证规则的方式。它们最常应用于 领域类,但 URL 映射命令对象 也支持约束。

11.1 声明约束

在一个领域类中,约束 被定义在约束属性中,它被分配给一个代码块

class User {
    String login
    String password
    String email
    Integer age

    static constraints = {
      ...
    }
}

然后使用与约束应用到的属性名匹配的方法调用与指定约束的命名参数一起使用

class User {
    ...

    static constraints = {
        login size: 5..15, blank: false, unique: true
        password size: 5..15, blank: false
        email email: true, blank: false
        age min: 18
    }
}

在此示例中,我们声明了 login 属性的长度必须在 5 到 15 个字符之间,它不能是空白的并且必须是唯一的。我们还对 passwordemailage 属性施加了其他约束。

默认情况下,所有领域类属性不可为 null(即它们都有一个隐式的 nullable: false 约束)。

可以在约束标题下的快速参考部分找到可用约束的完整参考。

请注意,仅评估一次约束,这可能与依赖于值(例如 java.util.Date 实例)的约束相关。

class User {
    ...

    static constraints = {
        // this Date object is created when the constraints are evaluated, not
        // each time an instance of the User class is validated.
        birthDate max: new Date()
    }
}

忠告 - 从约束中引用域类属性

非常容易尝试从静态约束块引用实例变量,但这在 Groovy(或 Java)中是不合法的。如果您这样做,您将得到一个 MissingPropertyException。例如,您可以尝试

class Response {
    Survey survey
    Answer answer

    static constraints = {
        survey blank: false
        answer blank: false, inList: survey.answers
    }
}

查看 inList 约束如何引用实例属性 survey?它不会起作用。相反,使用自定义 验证器

class Response {
    ...
    static constraints = {
        survey blank: false
        answer blank: false, validator: { val, obj -> val in obj.survey.answers }
    }
}

在此示例中,自定义验证器的 obj 参数是要验证的域实例,因此我们可以访问其 survey 属性并返回一个布尔值以指示 answer 属性的新值 val 是否有效。

11.2 验证约束

验证基础

调用 validate 方法以验证域类实例

def user = new User(params)

if (user.validate()) {
    // do something with user
}
else {
    user.errors.allErrors.each {
        println it
    }
}

域类上的 errors 属性是 Spring Errors 界面的实例。Errors 接口提供导航验证错误以及检索原始值的方法。

验证阶段

在 Grails 中有两阶段验证,第一阶段是 数据绑定,它发生在您将请求参数绑定到实例(例如)时

def user = new User(params)

至此,您可能已经在 errors 属性中出现错误,这是因为类型转换(例如将字符串转换为日期)。您可以使用 Errors API 检查这些错误并获取原始输入值

if (user.hasErrors()) {
    if (user.errors.hasFieldErrors("login")) {
        println user.errors.getFieldError("login").rejectedValue
    }
}

当您调用 validatesave 时,会发生第二阶段验证。这是当 Grails 按您定义的 constraints 验证绑定值的时间。例如,默认情况下 save 方法在执行之前调用 validate,从而允许您编写如下代码

if (user.save()) {
    return user
}
else {
    user.errors.allErrors.each {
        println it
    }
}

11.3 在类之间共享约束

在 Grails 中的常见模式是为验证用户提交的数据使用 命令对象,然后将命令对象的属性复制到相关的域类。这通常意味着您的命令对象和域类共享属性及它们的约束。您可以在两者之间手动复制并粘贴约束,但这是一种非常容易出错的方法。相反,请使用 Grails 的全局约束和导入机制。

全局约束

除了在域类、命令对象和 其他可验证类 中定义约束之外,您还可以将它们定义在 grails-app/conf/runtime.groovy

grails.gorm.default.constraints = {
    '*'(nullable: true, size: 1..20)
    myShared(nullable: false, blank: false)
}

这些约束不会附加到任何特定类,但可以轻松地从任何可验证类中引用它们

class User {
    ...

    static constraints = {
        login shared: "myShared"
    }
}

请注意 shared 参数的使用,其值是 grails.gorm.default.constraints 中定义的一个约束的名称。尽管配置设置的名称如此,但你可以从任何可验证类(例如命令对象)中引用这些共享约束。

* 约束是一个特例:它表示关联的约束(在上面的示例中为“nullable”和“size”)将应用于所有可验证类中的所有属性。可以通过可验证类中声明的约束覆盖这些默认值。

导入约束

Grails 2 引入了一种共享约束的替代方法,它允许你将一组约束从一个类导入到另一个类中。

假设你有这样一个领域类

class User {
    String firstName
    String lastName
    String passwordHash

    static constraints = {
        firstName blank: false, nullable: false
        lastName blank: false, nullable: false
        passwordHash blank: false, nullable: false
    }
}

然后你要创建一个命令对象 UserCommand,该对象共享领域类的某些属性和相应的约束。你使用 importFrom() 方法来执行此操作

class UserCommand {
    String firstName
    String lastName
    String password
    String confirmPassword

    static constraints = {
        importFrom User

        password blank: false, nullable: false
        confirmPassword blank: false, nullable: false
    }
}

这将从 User 领域类导入所有约束并将其应用于 UserCommand。导入将忽略源类(User)中没有任何对应属性的任何约束(UserCommand)。在上面的示例中,只有“firstName”和“lastName”约束将导入到 UserCommand,因为这两个类共享的属性仅为这两个属性。

如果你想要更多地控制要导入哪些约束,请使用 includeexclude 参数。这两个参数都接受一个由简单字符串或正则表达式字符串组成的列表,这些字符串与源约束中的属性名称进行匹配。因此,例如,如果你只想导入“lastName”约束,则可以使用

...
static constraints = {
    importFrom User, include: ["lastName"]
    ...
}

或者如果你想要以“Name”结尾的所有约束

...
static constraints = {
    importFrom User, include: [/.*Name/]
    ...
}

当然,exclude 会执行相反的操作,指定哪些约束不应该导入。

11.4 客户端上的验证

显示错误

通常,如果你收到验证错误,则会重定向回视图以进行渲染。一旦在那里,你需要某种方式显示错误。Grails 支持一组丰富的标签来处理错误。若要将错误作为列表呈现,可以使用 renderErrors

<g:renderErrors bean="${user}" />

如果你需要更多控制,可以使用 hasErrorseachError

<g:hasErrors bean="${user}">
  <ul>
   <g:eachError var="err" bean="${user}">
       <li>${err}</li>
   </g:eachError>
  </ul>
</g:hasErrors>

突出显示错误

当输入字段不正确时,使用红框或某个指示符突出显示通常很有用。也可以使用 hasErrors 作为方法来执行此操作。例如

<div class='value ${hasErrors(bean:user,field:'login','errors')}'>
   <input type="text" name="login" value="${fieldValue(bean:user,field:'login')}"/>
</div>

此代码检查 user bean 的 login 字段是否有任何错误,如果有,它会将 errors CSS 类添加到 div,允许您使用 CSS 规则来突出显示 div

检索输入值

每个错误实际上是 Spring 中 FieldError 类的实例,它保留着其原始输入值。这非常有用,因为您可以使用错误对象来使用 fieldValue 标记还原用户输入的值

<input type="text" name="login" value="${fieldValue(bean:user,field:'login')}"/>

此代码将检查 User bean 中是否存在 FieldError,如果存在,则获取 login 字段的原输入值。

11.5 验证和国际化

关于 Grails 中错误的另一个需要注意的重要事项是错误消息不会在任何地方硬编码。FieldError 类使用 Grails 的 i18n 支持从消息包中解析消息。

约束和消息代码

代码本身由惯例决定。例如,考虑我们之前研究过的约束

package com.mycompany.myapp

class User {
    ...

    static constraints = {
        login size: 5..15, blank: false, unique: true
        password size: 5..15, blank: false
        email email: true, blank: false
        age min: 18
    }
}

如果约束被违反,Grails 会按惯例查找消息代码

约束 错误代码

blank

className.propertyName.blank

creditCard

className.propertyName.creditCard.invalid

email

className.propertyName.email.invalid

inList

className.propertyName.not.inList

matches

className.propertyName.matches.invalid

max

className.propertyName.max.exceeded

maxSize

className.propertyName.maxSize.exceeded

min

className.propertyName.min.notmet

minSize

className.propertyName.minSize.notmet

notEqual

className.propertyName.notEqual

nullable

className.propertyName.nullable

range

className.propertyName.range.toosmallclassName.propertyName.range.toobig

size

className.propertyName.size.toosmallclassName.propertyName.size.toobig

unique

className.propertyName.unique

url

className.propertyName.url.invalid

validator

classname.propertyName. + Closure 返回的字符串

对于 blank 约束,这将是 user.login.blank,因此您需要在 grails-app/i18n/messages.properties 文件中使用以下消息

user.login.blank=Your login name must be specified!

类名在有和没有包的情况下都会被查找,其中包含包的版本优先。因此,例如,com.mycompany.myapp.User.login.blank 将在 user.login.blank 之前使用。这允许您的 domain 类消息代码与插件冲突的情况。

有关哪些代码用于哪些约束的参考,请参阅每个约束的参考指南(例如,blank)。

显示消息

renderErrors 标签将自动使用 message 标签查找消息。如果您需要更多控制渲染功能,则可以自己处理

<g:hasErrors bean="${user}">
  <ul>
   <g:eachError var="err" bean="${user}">
       <li><g:message error="${err}" /></li>
   </g:eachError>
  </ul>
</g:hasErrors>

在此示例中,eachError 标签的主体内部我们与 message 标签一起使用其 error 参数为给定的错误读取消息。

11.6 将验证应用于其他类

域类命令对象 默认支持验证。其他类可以通过在类中定义静态 constraints 属性(如上所述)然后告诉框架来使其可验证。重要的是,应用程序向框架注册可验证类。仅定义 constraints 属性是不够的。

Validateable 特征

定义静态 constraints 属性并实现 Validateable 特征的类将是可验证的。考虑此示例

src/main/groovy/com/mycompany/myapp/User.groovy
package com.mycompany.myapp

import grails.validation.Validateable

class User implements Validateable {
    ...

    static constraints = {
        login size: 5..15, blank: false, unique: true
        password size: 5..15, blank: false
        email email: true, blank: false
        age min: 18
    }
}
编程访问

访问可验证对象上的约束略有不同。您可以通过访问类的 constraintsMap 静态属性在其他上下文中以编程方式访问命令对象的约束。该属性是 MapConstrainedProperty> 的一个实例

在上面的示例中,访问 User.constraintsMap.login.blank 将产生 false,而 User.constraintsMap.login.unique 将产生 true。