Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
89d77d5
Introduce CreatedDate and LastModifiedDate annotations
codeconsole Oct 6, 2025
841fe22
Introduce GrailsExtension importGrailsAnnotations that will auto impo…
codeconsole Oct 6, 2025
568060d
Merge branch '7.0.x' into 7.0.x-autotimestamp-enhancements
codeconsole Oct 8, 2025
f0de080
Fix for mongodb autotimestamp properties not being marked dirty. Prop…
codeconsole Oct 8, 2025
2c91003
unused import
codeconsole Oct 8, 2025
e935901
Skip null check on AutoTimestamp properties
codeconsole Oct 8, 2025
7fed24c
Hide AutoTimestamp properties from scaffold input/edit views
codeconsole Oct 8, 2025
00a4f03
remove duplicate method
codeconsole Oct 8, 2025
0997abd
Merge branch '7.0.x' into 7.0.x-autotimestamp-enhancements
codeconsole Oct 8, 2025
8019058
Merge branch '7.0.x' into 7.0.x-autotimestamp-enhancements
codeconsole Oct 9, 2025
4d93af3
Merge branch '7.0.x' into 7.0.x-autotimestamp-enhancements
codeconsole Oct 15, 2025
a73aac7
Revert setting properties dirty in AutoTimestampEventListener as this…
codeconsole Oct 15, 2025
809f1d0
Deprecate @AutoTimestamp
codeconsole Oct 15, 2025
f05cee7
Cache annotation lookups when not in development mode
codeconsole Oct 16, 2025
f37dcc1
Add jakarta.validation.constraints.* to common annotation star imports
codeconsole Oct 16, 2025
a052ed1
null check on persistentProperty
codeconsole Oct 16, 2025
8602be4
remove unused imports
codeconsole Oct 17, 2025
df38a80
Support for Spring Data annotations
codeconsole Oct 18, 2025
e1597d9
@CreatedBy and @LastModifiedBy support
codeconsole Oct 18, 2025
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 @@ -19,6 +19,8 @@
package org.grails.datastore.gorm

import grails.gorm.annotation.AutoTimestamp
import grails.gorm.annotation.CreatedDate
import grails.gorm.annotation.LastModifiedDate
import grails.persistence.Entity
import org.apache.grails.data.simple.core.GrailsDataCoreTckManager
import org.apache.grails.data.testing.tck.base.GrailsDataTckSpec
Expand All @@ -28,7 +30,7 @@ import static grails.gorm.annotation.AutoTimestamp.EventType.CREATED

class CustomAutoTimestampSpec extends GrailsDataTckSpec<GrailsDataCoreTckManager> {
void setupSpec() {
manager.domainClasses.addAll([AutoTimestampedChildEntity, AutoTimestampedParentEntity, Image, RecordCustom])
manager.domainClasses.addAll([AutoTimestampedChildEntity, AutoTimestampedParentEntity, Image, RecordCustom, RecordWithAliases])
}

void "Test when the auto timestamp properties are customized, they are correctly set"() {
Expand Down Expand Up @@ -147,6 +149,69 @@ class CustomAutoTimestampSpec extends GrailsDataTckSpec<GrailsDataCoreTckManager
e.modified != null
e.created != null
}

void "Test @CreatedDate and @LastModifiedDate annotation aliases"() {
when: "An entity with alias annotations is persisted"
def r = new RecordWithAliases(name: "Test")
r.save(flush: true, failOnError: true)
manager.session.clear()
r = RecordWithAliases.get(r.id)
sleep(1) // give the date comparison below a chance diff

then: "the timestamp properties are set"
r.createdAt != null
r.updatedAt != null
r.createdAt.time < new Date().time
r.updatedAt.time < new Date().time

when: "An entity is modified"
Date previousCreated = r.createdAt
Date previousUpdated = r.updatedAt
r.name = "Test 2"
sleep(1) // give the save a chance to set a different time
r.save(flush: true)
manager.session.clear()
r = RecordWithAliases.get(r.id)

then: "the lastModified property is updated and createdDate is not"
r.updatedAt != null
previousUpdated.time < r.updatedAt.time
previousCreated.time == r.createdAt.time
}

void "Test @CreatedDate and @LastModifiedDate with insertOverwrite config"() {
when: "An entity is persisted and insertOverwrite is false"
AutoTimestampEventListener autoTimestampEventListener =
RecordWithAliases.gormPersistentEntity.mappingContext.eventListeners.find { it.class == AutoTimestampEventListener }
autoTimestampEventListener.insertOverwrite = false

def r = new RecordWithAliases(name: "Test")
def now = new Date()
r.createdAt = new Date(now.time)
r.updatedAt = r.createdAt
sleep(1) // give the save a chance to set a different time
r.save(flush: true, failOnError: true)
manager.session.clear()
r = RecordWithAliases.get(r.id)

then: "the timestamp properties are not overwritten"
now.time == r.updatedAt.time
now.time == r.createdAt.time

when: "An entity is modified"
Date previousCreated = r.createdAt
Date previousUpdated = r.updatedAt
r.name = "Test 2"
sleep(1) // give the save a chance to set a different time
r.save(flush: true)
manager.session.clear()
r = RecordWithAliases.get(r.id)

then: "the lastModified property is updated and createdDate is not"
r.updatedAt != null
previousUpdated.time < r.updatedAt.time
previousCreated.time == r.createdAt.time
}
}

@Entity
Expand Down Expand Up @@ -184,4 +249,14 @@ class AutoTimestampedParentEntity {
@Entity
class AutoTimestampedChildEntity extends AutoTimestampedParentEntity {
String name
}

@Entity
class RecordWithAliases {
Long id
String name
@CreatedDate
Date createdAt
@LastModifiedDate
Date updatedAt
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* A property annotation used to apply auto-timestamping on a field
* upon gorm insert events. This is an alias for @AutoTimestamp(EventType.CREATED).
*
* @author Scott Murphy Heiberg
* @since 7.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface CreatedDate {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* A property annotation used to apply auto-timestamping on a field
* upon gorm insert and update events. This is an alias for @AutoTimestamp(EventType.UPDATED).
*
* @author Scott Murphy Heiberg
* @since 7.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface LastModifiedDate {
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.springframework.context.ApplicationEvent;

import grails.gorm.annotation.AutoTimestamp;
import grails.gorm.annotation.CreatedDate;
import grails.gorm.annotation.LastModifiedDate;
import org.grails.datastore.gorm.timestamp.DefaultTimestampProvider;
import org.grails.datastore.gorm.timestamp.TimestampProvider;
import org.grails.datastore.mapping.config.Entity;
Expand Down Expand Up @@ -191,12 +193,18 @@ protected void storeDateCreatedAndLastUpdatedInfo(PersistentEntity persistentEnt
storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property);
} else {
Field field = getFieldFromHierarchy(persistentEntity.getJavaClass(), property.getName());
if (field != null && field.isAnnotationPresent(AutoTimestamp.class)) {
AutoTimestamp autoTimestamp = field.getAnnotation(AutoTimestamp.class);
if (autoTimestamp.value() == AutoTimestamp.EventType.UPDATED) {
storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property);
} else {
if (field != null) {
if (field.isAnnotationPresent(CreatedDate.class)) {
storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property);
} else if (field.isAnnotationPresent(LastModifiedDate.class)) {
storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property);
} else if (field.isAnnotationPresent(AutoTimestamp.class)) {
AutoTimestamp autoTimestamp = field.getAnnotation(AutoTimestamp.class);
if (autoTimestamp.value() == AutoTimestamp.EventType.UPDATED) {
storeTimestampAvailability(entitiesWithLastUpdated, persistentEntity, property);
} else {
storeTimestampAvailability(entitiesWithDateCreated, persistentEntity, property);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ class GrailsExtension {
*/
boolean importJavaTime = false

/**
* Whether grails annotation packages should be default import packages.
* When enabled, automatically imports:
* - grails.gorm.annotation.* (if grails-datamapping-core is in classpath)
* - grails.plugin.scaffolding.annotation.* (if grails-scaffolding is in classpath)
*/
boolean importGrailsAnnotations = false

/**
* Whether the spring dependency management plugin should be applied by default
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,17 +226,47 @@ class GrailsGradlePlugin extends GroovyPlugin {

protected Closure<String> getGroovyCompilerScript(GroovyCompile compile, Project project) {
GrailsExtension grails = project.extensions.findByType(GrailsExtension)
if (!grails.importJavaTime) {

List<String> starImports = []

// Add java.time if enabled
if (grails.importJavaTime) {
starImports.add('java.time')
}

// Add Grails annotation packages if enabled and dependencies are present
if (grails.importGrailsAnnotations) {
// Check for grails-datamapping-core (grails.gorm.annotation.*)
def datamappingCoreDep = project.configurations.getByName('compileClasspath').dependencies.find { Dependency d ->
d.group == 'org.apache.grails.data' && d.name == 'grails-datamapping-core'
}
if (datamappingCoreDep) {
starImports.add('grails.gorm.annotation')
}

// Check for grails-scaffolding (grails.plugin.scaffolding.annotation.*)
def scaffoldingDep = project.configurations.getByName('compileClasspath').dependencies.find { Dependency d ->
d.group == 'org.apache.grails' && d.name == 'grails-scaffolding'
}
if (scaffoldingDep) {
starImports.add('grails.plugin.scaffolding.annotation')
}
Comment on lines +250 to +256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand why you added it while at it, this technically does not have anything to do with time.
It should probably be in a subsequent PR?

Copy link
Contributor Author

@codeconsole codeconsole Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbglasius I agree it could of been in a separate PR, but it's kind of a gray area because the added config is importGrailsAnnotations and that is just adding 1 annotation @Scaffold. Plus there is a lot I want to address and the latency in getting PRs reviewed is a bit slow at the moment for what I want to get added/fixed.

The motivation for this PR is actually for importGrailsAnnotations. I was originally against
@matrei 's suggest of having 2 different annotations for AutoTimestamp because of the extra import statement on every domain class, but with importGrailsAnnotations, I don't mind.

In fact, with importGrailsAnnotations, I am even willing to get rid of @AutoTimestamp now. @matrei what are your thoughts about getting rid of @AutoTimestamp?

}

// Return null if no imports are needed
if (starImports.isEmpty()) {
return null
}

// Build the import statements
return { ->
'''withConfig(configuration) {
def importStatements = starImports.collect { pkg -> " star '$pkg'" }.join('\n')
"""withConfig(configuration) {
imports {
star 'java.time'
${importStatements}
}
}
'''
"""
}
}

Expand Down
Loading