My controller accepts an object to save it, and the object has a field named addTime,and i have to set this field by myself ,how can i do to let this operation automatically by springboot features。
#PostMapping("/HotelVersionDistribute/apply")
#Override
public Result<Boolean> apply(#RequestBody HotelVersionDistribute entity) {
// i dont want to do it,but i have no idea
entity.setAddTime(LocalDateTime.now());
HotelClientVersion hotelClientVersion = hotelClientVersionMapper.selectById(entity.getVersionId());
if(hotelClientVersion == null){
log.warn("version_id={},not exit", entity.getVersionId());
return Result.error(Result.CODE_REASOURCE_NOT_EXIST, "版本未找到");
}
saveApply(entity, hotelClientVersion);
return Result.success(true);
}
In general such columns ( createdBy , createdDate , updatedBy ,updatedDate )like addTime are called audit columns.
Spring provides Auditing support for Spring Data / Spring Data JPA . Since the question does not mention the same and assuming you are using one of those , please read through and implement your requirement.
From the documentation : Auditing
Spring Data provides sophisticated support to transparently keep track
of who created or changed an entity and when the change happened. To
benefit from that functionality, you have to equip your entity classes
with auditing metadata that can be defined either using annotations or
by implementing an interface
Two ways:
Direct set value in class:
public class HotelVersionDistribute {
private LocalDateTime addTime = LocalDateTime.now();
...
}
Use MySQL's default value mechanism, and set column updatable to false
public class HotelVersionDistribute {
#Column(insertable = false, updatable = false)
private LocalDateTime addTime;
...
}
Related
I am using the JOOQ library in order to fetch the result from a select query into my custom DTO class. Because my Custom DTO class has an ENUM Type field mapped as an Integer Column in my database I am using a custom data type converter. The query I perform is just a basic select query:
public TestSuiteDto findByAuditTestSuiteId(Integer auditTestSuiteId) {
TestSuiteJTable ts = TestSuiteJTable.TEST_SUITE;
AuditTestSuiteJTable ats = AuditTestSuiteJTable.AUDIT_TEST_SUITE;
List<TestSuiteDto> result = dsl.select(
ts.ID,
ts.DESCRIPTION,
ts.PROFILE_TYPE_ID,
ts.CREATED,
ts.ACTIVE,
ts.DEPRECATION,
ts.FEEDBACK_QUESTIONNAIRE_ID)
.from(ts
.join(ats).on(ts.ID.eq(ats.TEST_SUITE_ID)))
.where(ats.ID.eq(auditTestSuiteId))
.fetchInto(TestSuiteDto.class);
//do some stuff before returning
return result.get(0);
}
My custom DTO class looks like this
#Data
public class TestSuiteDto {
private Integer id;
private String description;
private ProfileType profileType;
private LocalDateTime created;
private boolean active;
private LocalDateTime deprecation;
private Integer feedbackQuestionnaireId;
}
The problem is that during the fetching process the SETTER of the DTO class is never triggered for the ENUM type field e.g. profileType even though I have configured a custom data type converter:
#Slf4j
public class ProfileTypeConverter implements Converter<Integer, ProfileType> {
#Override
public ProfileType from(Integer databaseObject) {
log.info("ProfileTypeConverter.from {} -> {}", databaseObject, ProfileType.getFromId(databaseObject));
return ProfileType.getFromId(databaseObject);
}
#Override
public Integer to(ProfileType userObject) {
log.info("ProfileTypeConverter.to");
return userObject.getId();
}
#Override
public Class<Integer> fromType() {
log.info("ProfileTypeConverter.fromType");
return Integer.class;
}
#Override
public Class<ProfileType> toType() {
log.info("ProfileTypeConverter.toType");
return ProfileType.class;
}
}
I have added some logs just to check if the converter is triggered at all and I see that the converter is triggered as expected (from method of the Converter class is called during the execution of the JOOQ SQL query). I have also delombok my DTO class in order to add logs in SETTERs and GETTERs and see if those are also properly called. I found out that all the SETTERs are properly called except for the profileType one. Because of that when I retrieve the DTO from the result list the value of the profileType field is null. The column in my database (mysql) that maps to the profileType ENUM is called PROFILE_TYPE_ID and it is of type Integer. I have also configured a forcedType in the pom.xml following the examples on JOOQ documentation webpage.
<forcedType>
<includeExpression>${jdbc.database}.TEST_SUITE.PROFILE_TYPE_ID</includeExpression>
<userType>mypackage.type.ProfileType</userType>
<converter>mypackage.converter.ProfileTypeConverter</converter>
</forcedType>
and this is how I have configured the ProfileType Field in pom.xml
<field>
<expression>${jdbc.database}.TEST_SUITE.PROFILE_TYPE_ID</expression>
<fieldIdentifier>
<expression>PROFILE_TYPE_ID</expression>
</fieldIdentifier>
<fieldMember>
<expression>profileType</expression>
</fieldMember>
<fieldGetter>
<expression>getProfileType</expression>
</fieldGetter>
<fieldSetter>
<expression>setProfileType</expression>
</fieldSetter>
</field>
JOOQ version: 3.14.16, Java 8
Why do things behave this way?
The reason is that you have a name mismatch:
Query
ts.PROFILE_TYPE_ID,
DTO
private ProfileType profileType;
If you want to rely on the reflection based DefaultRecordMapper, then you must name those things accordingly, otherwise, they won't be mapped. The fact that you have a converter is irrelevant, if the names don't match. Imagine you had 20 columns of type ProfileType. You wouldn't want to have DefaultRecordMapper map values purely based on their type.
Regarding your comments:
I have added some logs just to check if the converter is triggered at all and I see that the converter is triggered as expected (from method of the Converter class is called during the execution of the JOOQ SQL query)
Yes of course. The Converter belongs to the projected column. The conversion happens before the mapping (i.e. the into(TestSuiteDto.class) call)
Solutions
There are multiple alternatives to solve this:
Call your DTO attribute profileTypeId, or to add JPA annotations to it to map between SQL names and Java names
Rename your SQL column (in DDL)
Alias your SQL column using PROFILE_TYPE_ID.as("profile_type")
Use a computed column PROFILE_TYPE and attach the converter to that, keeping the PROFILE_TYPE_ID as it is (you can also use client side computed columns for that, in order not to affect your schema)
Use type safe constructor based mapping, rather than reflection based mapping, e.g. using fetch(Records.mapping(TestSuiteDto::new))
There are probably more possible solutions.
Working on a SpringBoot application using MongoDB as a persistent store.
Using spring data and MongoRepository to access MongoDB.
Using Javers to provide auditting.
If I use mongoRepository.insert(document) followed later by a mongoRepository.save(document) and then use javers to query the changes to that document, javers does not detect the differences between the object inserted and the object saved. It reports only a single change as if the save call was the original object persisted.
If I replace the insert call with a save and let spring data handle whether or not to insert or update, javers reports the expected change.
Example:
Consider the following:
#JaversSpringDataAuditable
public interface SomeDocumentRepository extends MongoRepository<SomeDocument, String> {
}
#Builder
#Data
#Document(collection = "someDocuments")
public class SomeDocument {
#Id
private String id;
private String status;
}
#Service
public class SomeDocumentService {
#Autowired
private SomeDocumentRepository someDocumentRepository;
public SomeDocument insert(SomeDocument doc) {
return someDocumentRepository.insert(doc);
}
public SomeDocument save(SomeDocument doc) {
return someDocumentRepository.save(doc);
}
}
#Service
public class AuditService {
#Autowired
private Javers javers;
public List<Change> getStatusChangesById(String documentId) {
JqlQuery query = QueryBuilder
.byInstanceId(documentId, SomeDocument.class)
.withChangedProperty("status")
.build();
return javers.findChanges(query);
}
}
If I call my service as follows:
var doc = SomeDocument.builder().status("new").build();
doc = someDocumentService.insert(doc);
doc.setStatus("change1");
doc = someDocumentService.save(doc);
and then call the audit service to get the changes:
auditService.getStatusChangesById(doc.getId());
I get a single change with "left" set to a blank and "right" set to "change1".
If I call "save" instead of "insert" like:
var doc = SomeDocument.builder().status("new").build();
doc = someDocumentService.save(doc);
doc.setStatus("change1");
doc = someDocumentService.save(doc);
and then call the audit service to get the changes I get 2 changes, the first being the most recent change with "left" set to "new", and "right" set to "change1" and a second change with "left" set to "" and "right" set to "new".
Is this a bug?
That's a good point. In case of Mongo, Javers covers only the methods from the CrudRepository interface. See https://github.com/javers/javers/blob/master/javers-spring/src/main/java/org/javers/spring/auditable/aspect/springdata/JaversSpringDataAuditableRepositoryAspect.java
Looks like MongoRepository#insert() should be also covered by the aspect.
Feel free to contribute a PR to javers, I will merge it. If you want to discuss the design first - please create a discussion here https://github.com/javers/javers/discussions
I am using Spring Data JDBC.
I have an entity that has fields annotated with #CreatedDate and #LastModifiedDate.
However, in some cases I want to set these two fields manually.
Is there a way to bypass #CreatedDate and #LastModifiedDate in some cases without removing the annotations from the entity? Or is there a callback that I can add before the entity gets saved?
Populating the auditing information is done by the RelationalAuditingCallback and IsNewAwareAuditingHandler.
The first one basically is the adapter to the module specific part (Spring Data Relational in this case) while the second modifies the entity.
You can implement your own variant of the IsNewAwareAuditingHandler stuff it in a RelationalAuditingCallback and register it as a bean. I did something similar a short time ago in this project on GitHub:
#Bean
RelationalAuditingCallback isNewAwareAuditingHandler(JdbcMappingContext context) {
return new RelationalAuditingCallback(new CustomAuditingHandler(context));
}
private static class CustomAuditingHandler extends IsNewAwareAuditingHandler {
public CustomAuditingHandler(JdbcMappingContext context) {
super(PersistentEntities.of(context));
}
#Override
public Object markAudited(Object source) {
if (!(source instanceof Product)) {
return source;
}
Product product = (Product) source;
if (product.createdDate == null) {
product.createdDate = Instant.now();
}
return source;
}
}
Please consider the logic in the CustomAuditingHandler a place holder. There you should plugin your way to determine if you set the value manually. Maybe your entity implements an interface that offers that information as a transient field, or you store that information in a thread local variable.
if u use above solution on spring boot, it ok. but in use #EnableJdbcAuditing, u should remove #EnableJdbcAuditing.
if it use that, RelationalAuditionCallback is dupulicated on ApplicationContext.
Here's a test based on #Jens Schauder's idea.
https://github.com/yangwansu/try-spring-data-jdbc/blob/main/src/test/java/masil/example/springdata/jdbc/ch9_14_1/ManuallySetupTest.java
In my datamodel a have many entities where attributes are mapped to enumerations like this:
#Enumerated(EnumType.STRING)
private MySpecialEnum enumValue;
MySpecialEnum defines some fixed values. The mapping works fine and if the database holds a NULL-value for a column I get NULL in the enumValue-attribute too.
The problem is, that my backend module (where I have no influence on) uses spaces in CHAR-columns to identify that no value is set. So I get an IllegalArgumentException instead of a NULL-value.
So my question is: Is there a JPA-Event where I can change the value read from the database before mapping to the enum-attribute?
For the write-access there is the #PrePersist where I can change Null-values to spaces. I know there is the #PostLoad-event, but this is handled after mapping.
Btw: I am using OpenJpa shipped within WebSphere Application Server.
You could map the enum-type field as #Transient (it will not be persisted) and map another field directly as String, synchronizing them in #PostLoad:
#Transient
private MyEnum fieldProxy;
private String fieldDB;
#PostLoad
public void postLoad() {
if (" ".equals(fieldDB))
fieldProxy = null;
else
fieldProxy = MyEnum.valueOf(fieldDB);
}
Use get/setFieldProxy() in your Java code.
As for synchronizing the other way, I'd do it in a setter, not in a #PreUpdate, as changes to #Transient fields probably do not mark the entity as modified and the update operation might not be triggered (I'm not sure of this):
public void setFieldProxy(MyEnum value) {
fieldProxy = value;
if (fieldProxy == null)
fieldDB = " ";
else
fieldDB = value.name();
}
OpenJPA offers #Externalizer and #Factory to handle "special" database values.
See this: http://ci.apache.org/projects/openjpa/2.0.x/manual/manual.html#ref_guide_pc_extern_values
You might end up with something like this: not tested...
#Factory("MyClass.mySpecialEnumFactory")
private MySpecialEnum special;
...
public static MySpecialEnum mySpecialEnumFactory(String external) {
if(StringUtils.isBlank(external) return null; // or why not MySpecialEnum.NONE;
return MySpecialEnum.valueOf(external);
}
I have a class which is mapped to a table using the hibernate notations of auto increment. This class works fine when I set values and update this to the database and I get a correct updated value in the table.
But the issue is when I create a new object of this class and try to get the id, it returns me a 0 instead of the auto_incremented id.
The code of the class is
#Entity(name="babies")
public class Baby implements DBHelper{
private int babyID;
#Id
#Column(name="babyID", unique=true, nullable= false)
#GeneratedValue(strategy = GenerationType.AUTO)
public int getBabyID() {
return babyID;
}
public void setBabyID(int babyID) {
this.babyID = babyID;
}
}
The code I use to get the persistent value is
Baby baby = new Baby();
System.out.println("BABY ID = "+baby.getBabyID());
This returns me a
BABY ID = 0
Any pointers would be appreciated.
Thanks,
Sana.
Hibernate only generates the id after an entity becomes persistent, ie after you have saved it to the database. Before this the object is in the transient state. Here is an article about the Hibernate object states and lifecycle
The ID is set by hibernate when object is saved and became persistable.
The annotation are only informing hibernate, how he should behave with class, property, method that annotation refer to.
Another thing if You have current id value how hibernate, would be able to recognize that he should insert or only update that value.
So this is normal expected behavior.