Below code simply creates two objects with two lists having same object marked with id only the content of the element is changed. The diff object generates a diff without any reference to parent list element but rather to the actual rop
public class JaversTest {
public static void main(String[] args) {
User u1 = new User();
User u2 = new User();
Email e1 = new Email();
e1.setId("id1");
e1.setMail("Vikrant5mahajan#gmail.com");
Email e2 = new Email();
e2.setId("id1");
e2.setMail("Vikrant6mahajan#gmail.com");
u1.setEmails(Collections.singletonList(e1));
u2.setEmails(Collections.singletonList(e2));
Javers javers = JaversBuilder.javers().build();
Diff diff = javers.compare(e1, e2);
System.out.println(diff.prettyPrint());
}
#Data
public static class User {
List<Email> emails = new ArrayList<>();
}
#Data
public static class Email {
#Id
String id;
String mail;
}
}
The output i get is
1. ValueChange{globalId:'com.practo.test.api.diff.JaversTest$Email/id1', property:'mail', oldVal:'Vikrant5mahajan#gmail.com', newVal:'Vikrant6mahajan#gmail.com'}
The JaVers diff is perfectly right. I'm not sure what you expect but in your snippet you have created two versions of an email object, and then you compare them. Since the mail field is changed, you get the ValueChange.
What else could you get from the diff engine? Your emails have no references to users or lists, so, obviously, javers is unaware of them.
Btw, your mapping is wrong. Email is a typical Value Object not an Entity, try to remove #Id ann.
Related
I'm currently working on a Spring Boot project for an online shop. It's my first project with Spring Boot (and my first post here), so my coding is not the best.
Context for the questions:
My shop (for now) has a lists of products and whishlists of different users (shopping lists), which have a bidirectional #ManyToMany relation (i left here the relevant details for my question(s)):
Product.java entity:
#Entity
public class Product extends RepresentationModel\<Product\>{
#Id
#GeneratedValue
#JsonView(ProductView.DescriptionExcluded.class)
private Integer id;
#ManyToMany()
#JoinTable(
name = "Shopping_Product",
joinColumns = { #JoinColumn(name = "id", referencedColumnName = "id") },
inverseJoinColumns = { #JoinColumn(name = "list_id", referencedColumnName = "list_id") })
#JsonIgnore
private Set<ShoppingList> shoppinglists = new HashSet<>();
// Constructor, getters, setters ....
ShoppingList.java entity:
#Entity
public class ShoppingList {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(ShoppingListView.ProductsExcluded.class)
private Integer list_id;
#JsonView(ShoppingListView.ProductsIncluded.class)
#ManyToMany(mappedBy = "shoppinglists")
private Set<Product> products = new HashSet<>();
// Constructor, getters, setters ...
I chose Product as the owner because i wanted to delete (tho it would be more fit to show something like "offer expired", but I'll stick to delete for now) the product from all existing lists when the admin takes it down from the shop, which works as expected:
ProductResource.java (controller):
#DeleteMapping("/categs/*/sub/*/products/{id}")
public ResponseEntity<String> deleteProduct(#PathVariable int id) {
Optional<Product> optional = productRepository.findById(id);
if(!optional.isPresent()) throw new NotFoundException("Product id - " + id);
Product prod = optional.get();
productRepository.delete(prod);
return ResponseEntity.ok().body("Product deleted");
}
My problems now are related to the ShoppingList entity, which is not the owner.
Any call I make to the Product resource (controller) works as expected, but anything from the other side either fails or returns incomplete results, like the following:
1.
I call retrieve all products from a list and it returns only the first object (the list has at least 2):
ShoppingListResource.java (controller):
#RestController
public class ShoppingListResource {
#Autowired
private ProductRepository productRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private ShoppingListRepository shoppinglistRepository;
#GetMapping("/user/lists/{id}")
public Set<Product> getShoppinglistProducts(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only check your list(s)!");
// All lists are shown for a product
// Product p = productRepository.findById(10111).get();
// Set<ShoppingList> set = p.getShoppinglists();
// set.stream().forEach(e -> log.info(e.toString()));
// Only first product is shown for a list
return shoppingList.getProducts();
This is what hibernate does on the last row (only returns 1/2 products)
Hibernate: select products0_.list_id as list_id2_3_0_,
products0_.id as id1_3_0_,
product1_.id as id1_1_1_,
product1_.description as descript2_1_1_,
product1_.name as name3_1_1_,
product1_.price as price4_1_1_,
product1_.subcat_id as subcat_i5_1_1_ from shopping_product products0_ inner join product product1_ on products0_.id=product1_.id where products0_.list_id=?
As i said above, I can delete a product and it gets removed automatically from all existing lists, but when i try the same from ShoppingList entity does nothing:
Same controller
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentPrincipalName = authentication.getName();
ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
String name = shoppingList.getUser().getUsername();
if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only delete your list(s)!");
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
Also, when i try to add/delete product from an existing list, does nothing.
This is my repo with full code, if you'd like to test directly (dev branch is up to date):
https://github.com/dragostreltov/online-store/tree/dev
You can just use admin admin as authentication (on the H2 console too). More details on the readme.
All DB data at app start is inserted from a .sql file.
I checked other similar questions and tried different methods on my ShoppingList entity (on the delete issue), like:
#PreRemove
public void removeListsFromProducts() {
for(Product p : products) {
p.getShoppinglists().remove(this);
}
}
Spring/Hibernate: associating from the non-owner side
And still doesn't work.
UPDATE:
I found out what issues I was having, I'll post an answer with the solution.
For anyone who's got the same/similar problems as I did, this is how I resolved them:
For point 1
(Hibernate only retrieves the first product from a shoppingList (Set))
I made multiple tests on my retrieve method and found out my Set was only containing 1 object, despite calling .add(product) twice.
As you can see, I'm using HashSet for both entities:
In Product (owner):
private Set<ShoppingList> shoppinglists = new HashSet<>();
In ShoppingList (mappedBy):
private Set<Product> products = new HashSet<>();
Thanks to this answer: https://stackoverflow.com/a/16344031/18646899
I learnt:
HashSet (entirely reasonably) assumes reflexivity, and doesn't check for equality when it finds that the exact same object is already in the set, as an optimization. Therefore it will not even call your equals method - it considers that the object is already in the set, so doesn't add a second copy.
In particular, if x.equals(x) is false, then any containment check would also be useless.
Taking this into account, I overwrote the hashCode() and equals() methods in Product.class and now
shoppingList.getProducts()
works as expected.
For point 2
(not being able to delete associations of non-owner entity before deleting the row from it's table)
Added lazy fetch and cascade to Product #ManyToMany:
#ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH})
And added the following methods:
In Product class:
public void addShoppinglist(ShoppingList list) {
this.shoppinglists.add(list);
list.getProducts().add(this);
}
public void removeShoppinglist(ShoppingList list) {
this.shoppinglists.remove(list);
list.getProducts().remove(this);
}
In ShoppingList class:
public void addProduct(Product product) {
this.products.add(product);
product.getShoppinglists().add(this);
}
public void removeProduct(Product product) {
this.products.remove(product);
product.getShoppinglists().remove(this);
}
Added #Transactional and modified the method inside the controller (ShoppingListResource) for deleteShoppingList:
#RestController
public class ShoppingListResource {
...
#Transactional
#DeleteMapping("/user/lists/{id}")
public ResponseEntity<String> deleteShoppinglist(#PathVariable int id) {
...
shoppingList.getProducts().stream().forEach(e -> {
e.removeShoppinglist(shoppingList);
});
shoppinglistRepository.delete(shoppingList);
return ResponseEntity.ok().body("Shopping list deleted");
}
}
And now this is working as expected, the shoppingList's associations are deleted first then the shoppingList itself.
I have an object with attributes like below:
public class Model1 {
private String aa;
private List<String> bb;
private List<CC> list;
}
public class CC{
private String cc1;
private List<String> cc2;
}
When I use javers, I am able to get changes made to aa and bb. But for List<CC> it just says, list/2 was added or list/0 was removed but it does not give exact data that was added or removed.
How do I get the complete data of CC object which was added, removed or changed from list?
Use Case: A user can CRUD multiple choice questions using a single page web application written in JavaScript.
Creating a new question and adding some options all happens within the browser / Frontend (FE).
The FE creates and uses temporary ids ("_1", "_2", ...) for both the question and all the options until the user clicks a save button.
When saving the newly created question the FE sends a JSON containing the temporary ids to the backend
As result the FE expects a 201 CREATED containing a map temporary id -> backend id to update its ids.
The user decides to add another Option (which on the FE side uses a temporary id again)
The user clicks save and the FE sends the updated question with a mixture of backend ids (for the question and the existing options) and a temporary id (for the newly created option)
To update the id of the the newly created option, the FE expects the reponse to contain the mapping for this id.
How should we implement the counterpart for the last part (5-7 adding an option) on the backend side?
I try this, but I cannot get the child ids after persistence.
Entities
#Entity
public class Question {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(mappedBy = "config", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Option> options = new ArrayList<>();
// ...
}
#Entity
public class Option {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne
#JoinColumn(name = "question_id", nullable = false)
private Question question;
public Option(Long id, Config config) {
this.id = id;
this.question = question;
}
// ...
}
Controller
#RestController
#RequestMapping("/questions")
public class AdminQuestionsController {
#Autowired
private QuestionRepository questionRepo;
#Autowired
private OptionRepository optionRepo;
#PutMapping("/{id}")
#ResponseStatus(HttpStatus.OK)
public QuestionDTO updateQuestion(#PathVariable("id") String id, #RequestBody QuestionDTO requestDTO) {
Question question = questionRepo.findOneById(Long.parseLong(id));
// will hold a mapping of the temporary id to the newly created Options.
Map<String, Option> newOptions = new HashMap<>();
// update the options
question.getOptions().clear();
requestDTO.getOptions().stream()
.map(o -> {
try { // to find the existing option
Option theOption = question.getOptions().stream()
// try to find in given config
.filter(existing -> o.getId().equals(existing.getId()))
.findAny()
// fallback to db
.orElse(optionRepo.findOne(Long.parseLong(o.getId())));
if (null != theOption) {
return theOption;
}
} catch (Exception e) {
}
// handle as new one by creating a new one with id=null
Option newOption = new Option(null, config);
newOptions.put(o.getId(), newOption);
return newOption;
})
.forEach(o -> question.getOptions().add(o));
question = questionRepo.save(question);
// create the id mapping
Map<String, String> idMap = new HashMap<>();
for (Entry<String, Option> e : newOptions.entrySet()) {
idMap.put(e.getKey(), e.getValue().getId());
// PROBLEM: e.getValue().getId() is null
}
return QuestionDTO result = QuestionDTO.from(question, idMap);
}
}
In the controller I marked the Problem: e.getValue().getId() is null
How should such a controller create the idMap?
It would be best if you save each option individually and then save the generated Id on the map.
I did the test below and it works perfectly.
#Autowired
void printServiceInstance(QuestionRepository questions, OptionRepository options) {
Question question = new Question();
questions.save(question);
question.add(new Option(-1L, question));
question.add(new Option(-2L, question));
question.add(new Option(-3L, question));
question.add(new Option(-4L, question));
Map<Long, Long> idMap = new HashMap<>();
question.getOptions().stream()
.filter(option -> option.getId() < 0)
.forEach(option -> idMap.put(option.getId(), options.save(option).getId()));
System.out.println(idMap);
}
Console out:
{-1=2, -2=3, -3=4, -4=5}
UPDATED:
Or will be a better code style if the front end just control de order of the options, and get the new ids based on the order of the unsaved options.
Option:
#Column(name = "order_num")
private Integer order;
public Option(Long id, Integer order, Question question) {
this.id = id;
this.question = question;
this.order = order;
}
Update example:
#Autowired
void printServiceInstance(QuestionRepository questions, OptionRepository options) {
Question question = new Question();
Question merged = questions.save(question);
merged.add(new Option(-1L, 1, merged));
merged.add(new Option(-2L, 2, merged));
merged.add(new Option(-3L, 3, merged));
merged.add(new Option(-4L, 4, merged));
questions.save(merged);
System.out.println(questions.findById(merged.getId()).get().getOptions());//
}
Console out: [Option [id=2, order=1], Option [id=3, order=2], Option [id=4, order=3], Option [id=5, order=4]]
Note there is no need of a map to control the new ids, the front-end should know by get it by the order of the options.
you can create additional field in both Question and Option class and marked as #Transient to make sure it's not persisted.
class Question {
....
private String id; // actual data field
#Transient
private String tempId;
// getter & setter
}
Initially when UI sends data, set tmpId and persist your Object. On successful operation, id will have actual id value. Now, let's create mapping (tmpId -> actualId).
Map<String, String> mapping = question.getOptions().stream()
.collect(Collectors.toMap(Option::getTmpId, Option::getId, (first, second) -> second));
mapping.put(question.getTmpId(), question.getId());
As you want only newly created object, we can do that two ways. Either add filter while creating mapping or remove later.
As mentioned, after first save, UI will update tmpId with actual Id and on next update, you will get a mix (actual for already saved, and tempId for newly created). In case of already saved, tmpId and actualId will be same.
mapping.entrySet().removeIf(entry -> entry.getKey().equals(entry.getValue()));
Regarding your controller code, you are clearing all existing option before adding a new option. If you are getting Question Object which has id (actual) field already populated, you can persist it directly. it will not affect anything. Additional if it has some change in that, that will be persisted.
Regarding your controller code, As you are clearing
question.getOptions().clear();
After this, you can simply add new options.
question.setOptions(requestDTO.getOptions());
question = questionRepo.save(question);
I hope it helps now.
So, you need to distinct FE-generated ID from BE-generated ?
You can
use negative ID on FE-generated, positive on BE
choose special prefix/suffix for FE-generated("fe_1", "fe_2", ...)
keep in Session list of already mapped IDs (server-side)
keep list of FE-generated ID and send it with data on POST (client-side)
In any case, beware of collisions when mixing two generators of ID.
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.
I'm trying to save an Entity in DB using Spring Data/Crud Repository(.save) that has in it another entity that was loaded through a #Cache method. In other words, I am trying to save an Ad Entity that has Attributes entities in it, and those attributes were loaded using Spring #Cache.
Because of that, I'm having a Detached Entity Passed to Persist Exception.
My question is, is there a way to save the entity still using #Cache for the Attributes?
I looked that up but couldn't find any people doing the same, specially knowing that I am using CrudRepository that has only the method .save(), that as far as I know manages Persist, Update, Merge, etc.
Any help is very much appreciated.
Thanks in advance.
Ad.java
#Entity
#DynamicInsert
#DynamicUpdate
#Table(name = "ad")
public class Ad implements SearchableAdDefinition {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private User user;
#OneToMany(mappedBy = "ad", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<AdAttribute> adAttributes;
(.....) }
AdAttribute.java
#Entity
#Table(name = "attrib_ad")
#IdClass(CompositeAdAttributePk.class)
public class AdAttribute {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ad_id")
private Ad ad;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "attrib_id")
private Attribute attribute;
#Column(name = "value", length = 75)
private String value;
public Ad getAd() {
return ad;
}
public void setAd(Ad ad) {
this.ad = ad;
}
public Attribute getAttribute() {
return attribute;
}
public void setAttribute(Attribute attribute) {
this.attribute = attribute;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
#Embeddable
class CompositeAdAttributePk implements Serializable {
private Ad ad;
private Attribute attribute;
public CompositeAdAttributePk() {
}
public CompositeAdAttributePk(Ad ad, Attribute attribute) {
this.ad = ad;
this.attribute = attribute;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CompositeAdAttributePk compositeAdAttributePk = (CompositeAdAttributePk) o;
return ad.getId().equals(compositeAdAttributePk.ad.getId()) && attribute.getId().equals(compositeAdAttributePk.attribute.getId());
}
#Override
public int hashCode() {
return Objects.hash(ad.getId(), attribute.getId());
}
}
Method using to load Attributes:
#Cacheable(value = "requiredAttributePerCategory", key = "#category.id")
public List<CategoryAttribute> findRequiredCategoryAttributesByCategory(Category category) {
return categoryAttributeRepository.findCategoryAttributesByCategoryAndAttribute_Required(category, 1);
}
Method used to create/persist the Ad:
#Transactional
public Ad create(String title, User user, Category category, AdStatus status, String description, String url, Double price, AdPriceType priceType, Integer photoCount, Double minimumBid, Integer options, Importer importer, Set<AdAttribute> adAtributes) {
//Assert.notNull(title, "Ad title must not be null");
Ad ad = adCreationService.createAd(title, user, category, status, description, url, price, priceType, photoCount, minimumBid, options, importer, adAtributes);
for (AdAttribute adAttribute : ad.getAdAttributes()) {
adAttribute.setAd(ad);
/* If I add this here, I don't face any exception, but then I don't take benefit from using cache:
Attribute attribute = attributeRepository.findById(adAttribute.getAttribute().getId()).get();
adAttribute.setAttribute(attribute);
*/
}
ad = adRepository.save(ad);
solrAdDocumentRepository.save(AdDocument.adDocumentBuilder(ad));
return ad;
}
I don't know if you still require this answer or not, since it's a long time, you asked this question. Yet i am going to leave my comments here, someone else might get help from it.
Lets assume, You called your findRequiredCategoryAttributesByCategory method, from other part of your application. Spring will first check at cache, and will find nothing. Then it will try to fetch it from Database. So it will create an hibernate session, open a transaction, fetch the data, close the transaction and session. Finally after returning from the function, it will store the result set in cache for future use.
You have to keep in mind, those values, currently in the cache, they are fetched using a hibernate session, which is now closed. So they are not related to any session, and now at detached state.
Now, you are trying to save and Ad entity. For this, spring created a new hibernate session, and Ad entity is attached to this particular session. But the attributes object, that you fetched from the Cache are detached. That's why, while you are trying to persist Ad entity, you are getting Detached Entity Exception
To resolve this issue, you need to re attach those objects to current hibernate session.I use merge() method to do so.
From hibernate documentation here https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/Session.html
Copy the state of the given object onto the persistent object with the same identifier. If there is no persistent instance currently associated with the session, it will be loaded. Return the persistent instance. If the given instance is unsaved, save a copy of and return it as a newly persistent instance. The given instance does not become associated with the session. This operation cascades to associated instances if the association is mapped with cascade="merge".
Simply put, this will attach your object to hibernate session.
What you should do, after calling your findRequiredCategoryAttributesByCategory method, write something like
List attributesFromCache = someService.findRequiredCategoryAttributesByCategory();
List attributesAttached = entityManager.merge( attributesFromCache );
Now set attributesAttached to your Ad object. This won't throw exception as attributes list is now part of current Hibernate session.