How to properly apply PrePersist like logic using jpa/spring-boot - spring

I have a very simple use case for the following model
#Entity
#Table(name='Foo')
class Foo {
#Id
String id = UUID.randomUUID()
String bar
Date foo_updated
}
I'd like to set the foo_updated value to new Date() when I see the incoming json payload has a value for "bar" (ie- this is a new value included in the POST/ a part of the PATCH update / included and proven to be different in a PUT)
I was hoping to simply apply the #PrePersist annotation on this model and add a simple conditional asking if "bar" was valid ...but quickly realized I wouldn't know if the value was "different" from what was in the db already (for the PATCH/PUT scenario).
I'm starting down the road of "add my own RestController" and apply this logic on the way in using the spring 4 ResponseEntity approach but ... I feel this might end up being a lot more work/more code to maintain.
As I'm new to spring-boot/spring-mvc/jpa I'm curious what other options I have and what the preferred approach would be for this seemingly "simple" requirement
Thanks for the help!

I came across this approach recently which involves recording the previous state on load. You now have access to the previous state after the new values are bound.
#Entity
#Table(name='Foo')
class Foo {
#Id
String id = UUID.randomUUID()
String bar
Date foo_updated
#Transient
private Foo previousState;
#PostLoad
private void setPreviousState(){
previousState = new Foo();
//copy the fields
}
}
However in your case can't you just do:
#Entity
#Table(name='Foo')
class Foo {
#Id
private String id = UUID.randomUUID()
private String bar
private Date lastUpdated;
public void setBar(String bar){
if(! this.bar.equals(bar){
lastUpdated = new Date();
}
}
}

Related

EqualsVerifier in SpringBoot

I am creating the tests for two classes that share a list of data. When I use EqualsVerifier I get an error because it is asking me for a list with data shared by these two classes.
This is the error:
Recursive datastructure. Add prefab values for one of the following types: CustomerView, List<YearConfigView>, YearConfigView
This is the #Test class:
#Test
public void CustomerViewTest() {
EqualsVerifier.forClass(CustomerView.class).withRedefinedSuperclass().withGenericPrefabValues(CustomerView.class).verify();
}
#Test
public void YearConfigViewTest() {
EqualsVerifier.forClass(YearConfigView.class).suppress(Warning.ALL_FIELDS_SHOULD_BE_USED).verify();
}
CustomerView.java:
public class CustomerView extends EntityBase<Integer> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private List<YearConfigView> yearConfigs;
#JsonProperty("current_year_config")
public YearConfigView getCurrentYearConfig() {
if (this.getYearConfigs() == null || this.getYearConfigs().isEmpty()) {
return null;
}
int currentYear = LocalDate.now().getYear();
return this.yearConfigs.parallelStream().filter(yc -> yc.getYear() == currentYear).findAny().orElse(null);
}
}
YearConfigView.java:
public class YearConfigView extends EntityBase<Integer> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private CustomerView customer;
private Integer year;
private String comments;
}
My problem: What do I have to change or add in EqualsVerifier to solve the problem?
Creator of EqualsVerifier here.
Without seeing your classes (I'd like to see exactly which fields CustomerView and YearConfigView have, and how equals and hashCode are implemented on both classes), it's hard to say for certain what's going on, but I suspect it's this:
CustomerView has a reference to YearConfigView (or perhaps List<YearConfigView>), and YearConfigView has a reference to CustomerView.
EqualsVerifier, while doing its thing, tries to make instances of the classes it's verifying, and giving its fields proper values too. In order to do that, it must recursively instantiate the class's fields and give those values too. Usually, that's not a problem, but sometimes you run into a loop, like in your case: in order to create a value for CustomerView, it must have a value for YearConfigView and vice versa.
The way to avoid this, is by giving EqualsVerifier some 'prefab values'. I see you've already tried to do something like this, by adding .withGenericPrefabValues(CustomerView.class). (This method requires 2 parameters so I suspect you may have removed some code before posting it to StackOverflow 😉) This only works if CustomerView is itself a generic class, which I can't verify because you didn't post that particular piece of code. In any event, you shouldn't give generic prefab values or regular prefab values for the class you're testing.
In general, though, your tests should both give a prefab value for the other class. That would look like this:
#Test
public void CustomerViewTest() {
YearConfigView one = new YearConfigView();
one.setYear(2020);
YearConfigView two = new YearConfigView();
two.setYear(2021);
EqualsVerifier.forClass(CustomerView.class)
.withRedefinedSuperclass()
.withPrefabValues(YearConfigView.class, one, two)
.verify();
}
#Test
public void YearConfigViewTest() {
CustomerView one = new CustomerView();
one.setName("Alice");
CustomerView two = new CustomerView();
two.setName("Bob");
EqualsVerifier.forClass(YearConfigView.class)
.suppress(Warning.ALL_FIELDS_SHOULD_BE_USED)
.withPrefabValues(CustomerView.class, one, two)
.verify();
}
Note that I still don't know which fields are included in your equals methods, so I'm only making an educated guess about how to instantiate your classes.
For more information, see the relevant page in the EqualsVerifier documentation. Since the classes are JPA entities, this page might also be helpful: it explains how the #Id is treated by EqualsVerifier.

Javers compare entity to DTO

i'm new to Javers, and i`m currently using it to create a patch update for my entity. But i'm having some struggle with one specific scenario
I want to compare a Entity against a EntityDTO/VO and get only the changes in values, the current comparison only returns that is a NewObject and ObjectRemoved changes.
Example:
public class Entity{
private ObjectId id;
private String name;
private String description;
}
public class EntityDTO{
private String name;
}
//
Entity oldState = new Entity(new ObjectId(), "oldName" , "oldDescription);
EntityDTO newState = new EntityDTO( "newName" );
JaversBuilder.javers().build().compare(oldState, newState).getChanges();
//This is returning only NewObject/ObjectRemoved changes, and the intended is to be ValueChange/ReferenceChange
The questions is, is there a way to compare only the similar attributes between the objects?
No, in JaVers, you can compare only objects of the same type.

Add data to database from Controller, different methods but same row

I have an entity model, for simplification purposes let's say it looks like this :
public class Results {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Long firstUser;
private Long secondUser;
private Double average;
private Double median;
private Double score;
}
This is my ResultsService Implementation:
public class ResultsServiceImpl implements ResultsService{
#Autowired
private CalculateDataRepository calculateDataRepository;;
#Autowired
private ResultsService resultsService;
Results results=new Results();
public void Average(Long id1, Long id2){
UserData firstClient = calculateDataRepository.findOne(id1);
userData secondClient = calculateDataRepository.findOne(id2);
clientId = firstClient.getClient().getId();
secondId = secondClient.getClient().getId();
Double average=(firstClient.getA()+secondClient.getA())/2;
results.setAverage(average);
}
public void Score(Long id1, Long id2){
SurveyData firstClient = surveyDataRepository.findOne(id1);
SurveyData secondClient = surveyDataRepository.findOne(id2);
clientId = firstClient.getClient().getId();
secondId = secondClient.getClient().getId();
Double average=(firstClient.getB()+secondClient.getB());
results.setScore(average);
results.setFirstUser(clientId );
results.setSecondUser(secondId );
resultsService.save(results);
}
....
I tried declaring Results results=new Results(); inside every method, but when I save them they get saved in different rows, instead of the same one.
How do I hold the reference so that when I call the setter of a field in one function, it's in the same row as the setter of a field in the other function.
To keep the problem focused, I tried to avoid showing the implementation of calculateDataRepository which is just the repository of an entity where some results are saved for different users.
The Results Method has no foreign field reference nor a reference from somewhere else, as there are fields firstUser and secondUser which I set from one of the methods;
Thank you.
Edit:
Results results=resultsService.findByFirstUserAndSecondUser(clientId, secondId);
if(results==null) {
results= new Results();
// Store to db ?
}
results.setAverage();
resultsService.save(results);
Actually you need a method in ResultsRepository
Results findByFirstAndSecond(Long first, Long second);
In the each Average and Score methods (BTW Java naming convention requires to have method names start from lowercase letter) call the findByFirstAndSecond(id1, id2)
If the method returns null (no such result) create a new instance and save in the DB (INSERT). If some Results is returned store the info there and save changes in DB (UPDATE).

NamedEntityGraph Returns All Columns and Objects

I am trying to utilize a NamedEntityGraph to limit the return data for specific queries. Mainly I do not want to return full object details when listing the object. A very simple class example is below.
#Entity
#Table(name="playerreport",schema="dbo")
#NamedEntityGraphs({
#NamedEntityGraph(name = "report.simple",
attributeNodes =
{#NamedAttributeNode(value="intId")
}
)
})
public class PlayerReportEntity {
#Id
#Column(name="intid",columnDefinition="uniqueidentifier")
private String intId;
#Column(name="plyid",columnDefinition="uniqueidentifier")
#Basic(fetch=FetchType.LAZY)
private String plyId;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name = "plyid", insertable=false,updatable=false)
private PlayerEntity player;
No matter what I do to plyId and player are always returned. Is there any way to only return the requested columns (intId) ?
As for the collection Hibernate does not do the join for the player object but it still returns player as null. So that part is working to an extent.
I am using a JPARepository below to generate Crud Statements for me
public interface PlayerReportRepository extends JpaRepository<PlayerReportEntity, String> {
#EntityGraph(value="report.simple")
List<PlayerIntelEntity> findByPlyId(#Param(value = "playerId") String playerId);
#Override
#EntityGraph(value="report.simple")
public PlayerIntelEntity findOne(String id);
}
A chunk of text from here - "Hence it seems that the #NamedEntityGraph only affects fields that are Collections, but fields that are not a Collection are always loaded." from JIRA
Please use the Example 47 on this page and use repositories accordingly.
In essence, hibernate is right now loading all the feilds in the class and for collections it will work if you follow the example stated above.
Thanks.

Spring Data MongoDB: Accessing and updating sub documents

First experiments with Spring Data and MongoDB were great. Now I've got the following structure (simplified):
public class Letter {
#Id
private String id;
private List<Section> sections;
}
public class Section {
private String id;
private String content;
}
Loading and saving entire Letter objects/documents works like a charm. (I use ObjectId to generate unique IDs for the Section.id field.)
Letter letter1 = mongoTemplate.findById(id, Letter.class)
mongoTemplate.insert(letter2);
mongoTemplate.save(letter3);
As documents are big (200K) and sometimes only sub-parts are needed by the application: Is there a possibility to query for a sub-document (section), modify and save it?
I'd like to implement a method like
Section s = findLetterSection(letterId, sectionId);
s.setText("blubb");
replaceLetterSection(letterId, sectionId, s);
And of course methods like:
addLetterSection(letterId, s); // add after last section
insertLetterSection(letterId, sectionId, s); // insert before given section
deleteLetterSection(letterId, sectionId); // delete given section
I see that the last three methods are somewhat "strange", i.e. loading the entire document, modifying the collection and saving it again may be the better approach from an object-oriented point of view; but the first use case ("navigating" to a sub-document/sub-object and working in the scope of this object) seems natural.
I think MongoDB can update sub-documents, but can SpringData be used for object mapping? Thanks for any pointers.
I figured out the following approach for slicing and loading only one subobject. Does it seem ok? I am aware of problems with concurrent modifications.
Query query1 = Query.query(Criteria.where("_id").is(instance));
query1.fields().include("sections._id");
LetterInstance letter1 = mongoTemplate.findOne(query1, LetterInstance.class);
LetterSection emptySection = letter1.findSectionById(sectionId);
int index = letter1.getSections().indexOf(emptySection);
Query query2 = Query.query(Criteria.where("_id").is(instance));
query2.fields().include("sections").slice("sections", index, 1);
LetterInstance letter2 = mongoTemplate.findOne(query2, LetterInstance.class);
LetterSection section = letter2.getSections().get(0);
This is an alternative solution loading all sections, but omitting the other (large) fields.
Query query = Query.query(Criteria.where("_id").is(instance));
query.fields().include("sections");
LetterInstance letter = mongoTemplate.findOne(query, LetterInstance.class);
LetterSection section = letter.findSectionById(sectionId);
This is the code I use for storing only a single collection element:
MongoConverter converter = mongoTemplate.getConverter();
DBObject newSectionRec = (DBObject)converter.convertToMongoType(newSection);
Query query = Query.query(Criteria.where("_id").is(instance).and("sections._id").is(new ObjectId(newSection.getSectionId())));
Update update = new Update().set("sections.$", newSectionRec);
mongoTemplate.updateFirst(query, update, LetterInstance.class);
It is nice to see how Spring Data can be used with "partial results" from MongoDB.
Any comments highly appreciated!
I think Matthias Wuttke's answer is great, for anyone looking for a generic version of his answer see code below:
#Service
public class MongoUtils {
#Autowired
private MongoTemplate mongo;
public <D, N extends Domain> N findNestedDocument(Class<D> docClass, String collectionName, UUID outerId, UUID innerId,
Function<D, List<N>> collectionGetter) {
// get index of subdocument in array
Query query = new Query(Criteria.where("_id").is(outerId).and(collectionName + "._id").is(innerId));
query.fields().include(collectionName + "._id");
D obj = mongo.findOne(query, docClass);
if (obj == null) {
return null;
}
List<UUID> itemIds = collectionGetter.apply(obj).stream().map(N::getId).collect(Collectors.toList());
int index = itemIds.indexOf(innerId);
if (index == -1) {
return null;
}
// retrieve subdocument at index using slice operator
Query query2 = new Query(Criteria.where("_id").is(outerId).and(collectionName + "._id").is(innerId));
query2.fields().include(collectionName).slice(collectionName, index, 1);
D obj2 = mongo.findOne(query2, docClass);
if (obj2 == null) {
return null;
}
return collectionGetter.apply(obj2).get(0);
}
public void removeNestedDocument(UUID outerId, UUID innerId, String collectionName, Class<?> outerClass) {
Update update = new Update();
update.pull(collectionName, new Query(Criteria.where("_id").is(innerId)));
mongo.updateFirst(new Query(Criteria.where("_id").is(outerId)), update, outerClass);
}
}
This could for example be called using
mongoUtils.findNestedDocument(Shop.class, "items", shopId, itemId, Shop::getItems);
mongoUtils.removeNestedDocument(shopId, itemId, "items", Shop.class);
The Domain interface looks like this:
public interface Domain {
UUID getId();
}
Notice: If the nested document's constructor contains elements with primitive datatype, it is important for the nested document to have a default (empty) constructor, which may be protected, in order for the class to be instantiatable with null arguments.
Solution
Thats my solution for this problem:
The object should be updated
#Getter
#Setter
#Document(collection = "projectchild")
public class ProjectChild {
#Id
private String _id;
private String name;
private String code;
#Field("desc")
private String description;
private String startDate;
private String endDate;
#Field("cost")
private long estimatedCost;
private List<String> countryList;
private List<Task> tasks;
#Version
private Long version;
}
Coding the Solution
public Mono<ProjectChild> UpdateCritTemplChild(
String id, String idch, String ownername) {
Query query = new Query();
query.addCriteria(Criteria.where("_id")
.is(id)); // find the parent
query.addCriteria(Criteria.where("tasks._id")
.is(idch)); // find the child which will be changed
Update update = new Update();
update.set("tasks.$.ownername", ownername); // change the field inside the child that must be updated
return template
// findAndModify:
// Find/modify/get the "new object" from a single operation.
.findAndModify(
query, update,
new FindAndModifyOptions().returnNew(true), ProjectChild.class
)
;
}

Resources