In Spring Data JDBC bypass #CreatedDate and #LastModifiedDate - java

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

Related

Return empty collection when this collection is lazy intitialized

I have this object:
Entity
#Entity
public class someClass{
private String name;
private String labelKey;
#ManyToMany(cascade = {CascadeType.PERSIST,CascadeType.MERGE}, fetch = FetchType.LAZY)
private Set<Product> products = new HashSet<>();
}
DTO
public class someClass{
private String name;
private String labelKey;
private Set<Product> products = new HashSet<>();
}
My problem is that when I get this object but products are lazy initialized, when I mapp entity to DTO using Dozer, I get a LaziInitializedException, then i want to get that when I get products lazy initialized, this products will return a empry Set.
Is this possible?
Thanks for your time and sorry for my english, it's not my native language.
As you can see in this tutorial here you can instruct dozer to exclude some field from the mapping.
If you do so, then the dozer will not invoke the method of getProducts of your entity class and therefore the exception LaziInitializedException will not be thrown.
At the same time because your DTO object is initialized with an empty HashSet for the field products, this is what will remain at the end in the DTO.
So your requirement will work, where your entity is lazily initialized for products and your DTO returns an empty list while at the same time the mapping happens from dozer.
Here is the configuration that you need for the mapper of dozer.
BeanMappingBuilder mappingExclusion = new BeanMappingBuilder() {
#Override
protected void configure() {
mapping(SomeClassEntity.class, SomeClassDto.class).exclude("products");
}
};
mapper = new DozerBeanMapper();
mapper.addMapping(mappingExclusion);
Then you can use it to do the mapping as following
mapper.map(someClassEntityInstance, someClassDtoInstance);
You could create/modify your Getter such that:
public Set<Product> getProducts() {
if (products == null) {
return new HashSet<>();
//or products = new HashSet<>(), but I'm not sure of the side effects as far as database framework is concerned.
}
return products;
}
Try marking your service class or method as #Transactional to let Spring handle session management.
public class ServiceUsingSomeClass {
final SomeClassRepository someClassRepository;
//Constructor ...
#Transactional
showProducts() {
someClassRepository.findAll();
// Do something with Set<Product>
}
}
If you only want to avoid fetching the association in cases where you use Dozer for DTO mapping, you could configure it to ignore products field in source object by extending DozerConverter and using that custom converter.
I also feel that maybe that means your target type doesn't really need to have
a products field to begin with, since you're not going to populate it.
If there's many places like this in your codebase, consider using projections to only fetch the properties necessary for the purpose at hand.
#fella7ena brings up a point about #Transactional, however this is actually unrelated - you can still come across LazyInitializationException within a transaction. This happens because Hibernate loses track of the relation between the java bean's persistence state and the database state. If you actually wanted to fetch products association from the database, you would have to use eager fetchtype (leads to n+1 issue), batching, or entitygraphs.

Javers not recognizing insert as an initial change

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

how springboot project to set request argument

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;
...
}

Exposing hypermedia links on collection even it's empty using Spring Data Rest

First at all I read the previous question: Exposing link on collection entity in spring data REST
But the issue still persist without trick.
Indeed if I want to expose a link for a collections resources I'm using the following code:
#Component
public class FooProcessor implements ResourceProcessor<PagedResources<Resource<Foo>>> {
private final FooLinks fooLinks;
#Inject
public FooProcessor(FooLinks fooLinks) {
this.FooLinks = fooLinks;
}
#Override
public PagedResources<Resource<Foo>> process(PagedResources<Resource<Foo>> resource) {
resource.add(fooLinks.getMyCustomLink());
return resource;
}
}
That works correctly except when collection is empty...
The only way to works is to replace my following code by:
#Component
public class FooProcessor implements ResourceProcessor<PagedResources> {
private final FooLinks fooLinks;
#Inject
public FooProcessor(FooLinks fooLinks) {
this.FooLinks = fooLinks;
}
#Override
public PagedResources process(PagedResources resource) {
resource.add(fooLinks.getMyCustomLink());
return resource;
}
}
But by doing that the link will be exposed for all collections.
I can create condition for exposing only for what I want but I don't think is clean.
I think spring does some magic there trying to discover the type of the collection - on an empty collection you cannot tell which type it is of - so spring-data-rest cannot determine which ResourceProcessor to use.
I think I have seen in
org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.ResourcesProcessorWrapper#isValueTypeMatch that they try to determine the type by looking at the first element in the collection and otherwise just stop processing:
if (content.isEmpty()) {
return false;
}
So I think you cannot solve this using spring-data-rest. For your controller you could fall back to writing a custom controller and use spring hateoas and implement your own ResourceAssemblerSupport to see the link also on empty collections.

JPA: Map invalid database values to enums

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);
}

Categories