Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ class ObjectB {
String name

static constraints = {
name nullable: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ import grails.gorm.annotation.Entity
class Contract {
BigDecimal salary
static belongsTo = [player: Player]

static constraints = {
player nullable: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class Business {
people: Person
]

static constraints = {
name nullable: false
}
}
@Entity
abstract class Person {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Person {
Integer age
String phone
static constraints = {
age min: 18, max: 65
age min: 18, max: 65, nullable: false
name blank: false
phone nullable: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ class ObjectB {
String name

static constraints = {
name nullable: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ import grails.gorm.annotation.Entity
class Contract {
BigDecimal salary
static belongsTo = [player: Player]

static constraints = {
player nullable: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class Business {
people: Person
]

static constraints = {
name nullable: false
}
}
@Entity
abstract class Person {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Person {
Integer age
String phone
static constraints = {
age min: 18, max: 65
age min: 18, max: 65, nullable: false
name blank: false
phone nullable: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class Plant implements Serializable, MongoEntity<Plant> {
boolean goesInPatch
String name

static constraints = {
name nullable: false
}

static mapping = {
name index:true
goesInPatch index:true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class Person implements MongoEntity<Person> {
Birthday birthday

static constraints = {
name blank: false
name blank: false, nullable: false
birthday nullable: true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.grails.datastore.gorm.mongo

import grails.gorm.tests.Person
import grails.gorm.tests.Pet
import org.apache.grails.data.mongo.core.GrailsDataMongoTckManager
import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,18 @@ class SportValidate {
class TeamValidate {

String name

static constraints = {
name nullable: false
}
}

@Entity
class ArenaValidate {

String name

static constraints = {
name nullable: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ class ClassWithListArgBeforeValidate implements Serializable {
}

static constraints = {
name(blank: false)
name(blank: false, nullable: false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ class ClassWithNoArgBeforeValidate implements Serializable {
}

static constraints = {
name(blank: false)
name(blank: false, nullable: false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ class ClassWithOverloadedBeforeValidate implements Serializable {
}

static constraints = {
name(blank: false)
name(blank: false, nullable: false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class DefaultConstraintEvaluator implements ConstraintsEvaluator {
protected final MappingContext mappingContext;
protected final Map<String, Object> defaultConstraints;
protected final boolean cacheAutoTimestampAnnotations;
protected final boolean defaultNullable;

public DefaultConstraintEvaluator() {
this(new DefaultConstraintRegistry(new StaticMessageSource()), new KeyValueMappingContext("default"), Collections.<String, Object>emptyMap(), true);
Expand All @@ -98,10 +99,15 @@ public DefaultConstraintEvaluator(ConstraintRegistry constraintRegistry, Mapping
}

public DefaultConstraintEvaluator(ConstraintRegistry constraintRegistry, MappingContext mappingContext, Map<String, Object> defaultConstraints, boolean cacheAutoTimestampAnnotations) {
this(constraintRegistry, mappingContext, defaultConstraints, cacheAutoTimestampAnnotations, true);
}

public DefaultConstraintEvaluator(ConstraintRegistry constraintRegistry, MappingContext mappingContext, Map<String, Object> defaultConstraints, boolean cacheAutoTimestampAnnotations, boolean defaultNullable) {
this.constraintRegistry = constraintRegistry;
this.mappingContext = mappingContext;
this.defaultConstraints = defaultConstraints;
this.cacheAutoTimestampAnnotations = cacheAutoTimestampAnnotations;
this.defaultNullable = defaultNullable;
}

@Override
Expand Down Expand Up @@ -270,7 +276,14 @@ protected void applyDefaultConstraints(String propertyName, PersistentProperty p
}

protected void applyDefaultNullableConstraint(PersistentProperty p, ConstrainedProperty cp) {
applyDefaultNullableConstraint(cp, false);
// Grails 8: persistent properties are nullable by default, aligning Grails with the rest of
// the JVM persistence/validation ecosystem (JPA/Hibernate, Spring Data, Micronaut Data and
// Jakarta Bean Validation are all "nullable unless you say otherwise"). To restore the
// legacy required-by-default behavior, either set the boolean flag:
// grails.gorm.default.nullable = false
// or apply a wildcard default constraint:
// grails.gorm.default.constraints = { '*'(nullable: false) }
applyDefaultNullableConstraint(cp, defaultNullable);
}

protected void applyDefaultNullableConstraint(ConstrainedProperty cp, boolean defaultNullable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ class DefaultValidatorRegistry implements ValidatorRegistry, ConstraintRegistry,
// Default to true if not explicitly configured
boolean cacheAnnotations = connectionSourceSettings.cacheAutoTimestampAnnotations != null ?
connectionSourceSettings.cacheAutoTimestampAnnotations : true
this.constraintsEvaluator = new DefaultConstraintEvaluator(constraintRegistry, mappingContext, defaultConstraintsMap, cacheAnnotations)
boolean defaultNullable = connectionSourceSettings.default.nullable
this.constraintsEvaluator = new DefaultConstraintEvaluator(constraintRegistry, mappingContext, defaultConstraintsMap, cacheAnnotations, defaultNullable)
this.mappingContext = mappingContext
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package grails.gorm.validation

import org.grails.datastore.gorm.validation.constraints.registry.DefaultValidatorRegistry
import org.grails.datastore.mapping.core.connections.ConnectionSourceSettings
import org.grails.datastore.mapping.keyvalue.mapping.config.GormKeyValueMappingFactory
import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext
import org.grails.datastore.mapping.model.MappingContext
import org.grails.datastore.mapping.model.PersistentEntity
import org.grails.datastore.mapping.model.config.GormMappingConfigurationStrategy
import org.grails.datastore.mapping.validation.ValidationErrors
import org.grails.datastore.mapping.validation.ValidatorRegistry
import org.springframework.validation.Errors
import org.springframework.validation.Validator
import spock.lang.Shared
import spock.lang.Specification

import jakarta.persistence.Entity

/**
* Demonstrates the Grails 8 default: an unconstrained persistent property is nullable by default,
* aligning Grails with the rest of the JVM persistence/validation ecosystem. This spec deliberately
* does NOT opt out (no {@code grails.gorm.default.constraints} override), so it exercises the real
* framework default applied by {@code DefaultConstraintEvaluator}.
*/
class NullableByDefaultSpec extends Specification {

@Shared Validator widgetValidator

void setupSpec() {
MappingContext mappingContext = new KeyValueMappingContext("test")
mappingContext.mappingFactory = new GormKeyValueMappingFactory("test")
mappingContext.syntaxStrategy = new GormMappingConfigurationStrategy(mappingContext.mappingFactory)

PersistentEntity widgetEntity = mappingContext.addPersistentEntity(Widget)

ValidatorRegistry registry = new DefaultValidatorRegistry(mappingContext, new ConnectionSourceSettings())
widgetValidator = registry.getValidator(widgetEntity)
}

void "an unconstrained property is nullable by default"() {
given: "a widget whose only populated property is the explicitly-required one"
Widget widget = new Widget(requiredName: 'gizmo')
Errors errors = new ValidationErrors(widget)

when:
widgetValidator.validate(widget, errors)

then: "the unconstrained properties are valid while null - they are nullable by default"
!errors.hasErrors()
!errors.getFieldError('description')
!errors.getFieldError('quantity')
}

void "a property with an explicit nullable: false constraint is still required"() {
given: "a widget missing the explicitly-required property"
Widget widget = new Widget()
Errors errors = new ValidationErrors(widget)

when:
widgetValidator.validate(widget, errors)

then: "only the explicitly-required property is rejected; the unconstrained ones are not"
errors.hasErrors()
errors.getFieldError('requiredName')?.code == 'nullable'
!errors.getFieldError('description')
!errors.getFieldError('quantity')
}

void "grails.gorm.default.nullable = false restores required-by-default"() {
given: "a validator built with the YAML-friendly flag disabled"
MappingContext mappingContext = new KeyValueMappingContext("test")
mappingContext.mappingFactory = new GormKeyValueMappingFactory("test")
mappingContext.syntaxStrategy = new GormMappingConfigurationStrategy(mappingContext.mappingFactory)
PersistentEntity widgetEntity = mappingContext.addPersistentEntity(Widget)

ConnectionSourceSettings settings = new ConnectionSourceSettings()
settings.default.nullable = false
Validator validator = new DefaultValidatorRegistry(mappingContext, settings).getValidator(widgetEntity)

and: "a widget whose unconstrained properties are null"
Widget widget = new Widget(requiredName: 'gizmo')
Errors errors = new ValidationErrors(widget)

when:
validator.validate(widget, errors)

then: "the unconstrained properties are required again - the flag restored legacy behaviour"
errors.hasErrors()
errors.getFieldError('description')?.code == 'nullable'
errors.getFieldError('quantity')?.code == 'nullable'
}
}

@Entity
class Widget {
String description
Integer quantity
String requiredName

static constraints = {
requiredName nullable: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ class Author {
]

static constraints = {
name(nullable: false) // explicit now that properties are nullable by default — this spec
// uses the null-name error to detect that root beforeValidate didn't run
publisher(nullable: true)
ownedPublisher(nullable: true)
defaultPublisher(nullable: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ public interface Settings {
* The default constraints
*/
String SETTING_DEFAULT_CONSTRAINTS = PREFIX + '.' + "default.constraints";
/**
* Whether an unconstrained persistent property is nullable by default (Grails 8 default: true)
*/
String SETTING_DEFAULT_NULLABLE = PREFIX + '.' + "default.nullable";
/**
* The custom types
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ class ConnectionSourceSettings implements Settings {
* The default constraints
*/
Closure constraints

/**
* Whether an unconstrained persistent property is nullable by default (Grails 8 default: true).
* Set {@code grails.gorm.default.nullable = false} to restore the legacy required-by-default behaviour.
*/
boolean nullable = true
}

/**
Expand Down
21 changes: 20 additions & 1 deletion grails-doc/src/en/ref/Constraints/nullable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,26 @@ under the License.
=== Purpose


Allows a property to be set to `null`. By default Grails does not allow `null` values for properties.
Allows a property to be set to `null`.

NOTE: As of Grails 8, an unconstrained persistent property is *nullable by default*, aligning Grails with the rest of the JVM persistence/validation ecosystem (JPA/Hibernate, Spring Data, Micronaut Data and Jakarta Bean Validation). Declare `nullable: false` to make a property required. To restore the legacy required-by-default behaviour for an application, set `grails.gorm.default.nullable` to `false`:

[source,yaml]
.grails-app/conf/application.yml
----
grails:
gorm:
default:
nullable: false
----

The equivalent in `grails-app/conf/application.groovy`:

[source,groovy]
.grails-app/conf/application.groovy
----
grails.gorm.default.nullable = false
----


=== Examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.grails.datastore.gorm.validation.constraints.eval.ConstraintsEvaluato
import org.grails.datastore.gorm.validation.constraints.eval.DefaultConstraintEvaluator
import org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry
import org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry
import org.grails.datastore.mapping.config.Settings
import org.grails.datastore.mapping.model.MappingContext
import org.grails.validation.ConstraintEvalUtils

Expand All @@ -50,7 +51,10 @@ class DefaultConstraintEvaluatorFactoryBean implements FactoryBean<ConstraintsEv
ConstraintsEvaluator getObject() throws Exception {
ConstraintRegistry registry = new DefaultConstraintRegistry(messageSource)

new DefaultConstraintEvaluator(registry, grailsDomainClassMappingContext, ConstraintEvalUtils.getDefaultConstraints(grailsApplication.config))
boolean cacheAutoTimestampAnnotations = grailsApplication.config.getProperty(Settings.SETTING_AUTO_TIMESTAMP_CACHE_ANNOTATIONS, Boolean, true)
boolean defaultNullable = grailsApplication.config.getProperty(Settings.SETTING_DEFAULT_NULLABLE, Boolean, true)
new DefaultConstraintEvaluator(registry, grailsDomainClassMappingContext,
ConstraintEvalUtils.getDefaultConstraints(grailsApplication.config), cacheAutoTimestampAnnotations, defaultNullable)
}

@Override
Expand Down
Loading
Loading