I'd like to update a collection field List<Bar> bars in JPA entity Foo, but without changing the original one - just set the updatedBars to bars field before save.
But my code isn't working, probably because the updatedBars are not persistent despite the fact that some Bar objects are from original collection.
Any suggestions, please.
P.S. This way of updating collection seems easier to me, because I can just populate an updatedBars in stream copying and updating, creating, and just ignoring values from original collection instead of performing separate operations like remove, update, add on the original collection.
Here is my code example:
#Getter
#Setter
#Entity
#Table(name = "foo")
public class Foo {
#Id
#Column(name = "id")
private Long id;
#JoinColumn(name = "foo_id")
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
private List<Bar> bars;
}
#Getter
#Setter
#Entity
#Table(name = "bar")
public class Bar {
#Id
#Column(name = "id")
private Long id;
#Column(name = "foo_id")
private Long fooId;
#Column(name = "value")
private Long value;
}
#Transactional
public void refresh(Long fooId){
Foo foo = fooRepository.find(fooId);
List<Bar> updatedBars = getUpdatedBars(foo.getBars());
foo.setBars(updatedBars);
}
private List<Bar> getUpdatedBars(List<Bar> originalBars) {
//do any staff to return new list of Bars, not changing the original one
//some items are removed, some updated (same entity from original list, with changed value field), some added (entities without ids)
}
UPDATE:
Updating the content of original collection also did't work.
#Transactional
public void refresh(Long fooId){
Foo foo = fooRepository.find(fooId);
List<Bar> originalBars = foo.getBars();
List<Bar> updatedBars = getUpdatedBars(originalBars);
originalBars.clear();
originalBars.addAll(updatedBars);
}
I don't know what without changing the original one should mean, but you could just detach the entity right after loading so that the database state is not updated when you change the collection.
What you are describing is a deep copy. If the type parameter of the list is a primitive, there are a few options but if it's a complex object such as the case an option is iterating source list and creating new objects in destination.
public void copy(List<Bar> dest, List<Bar> source){
dest.clear();
for (int i = 0; i < source.size(); i++) {
dest.add(new Bar(source.get(i)));
}
}
There are other options such as serializing and deserializing using a library like Jackson or Gson. That would create a new list of objects without need to do it "manually".
ref: How do you make a deep copy of an object?
Related
I have an issue when trying to update the contents of a cart with new values added one by one. I am using Spring boot with Hibernate, JPA Repositories, MySQL Database and a front-end built with vanilla JS.I will describe my issue in the following lines.
I am having an user entity that looks like this:
#Entity
#Table(name = "users")
#Getter
#Setter
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
#Column(name = "cartprod_id")
#OneToMany(cascade = {CascadeType.ALL})
private List<CartItem> cartProduct;
This entity has a List<CartItem> field that looks like this:
#Entity
#Getter
#Setter
public class CartItem {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "product_id")
private int productId;
#JsonIgnore
#ManyToOne
private User user;
}
And the relationship between them in the Database looks like in this image:
The idea of the above code is to have a way to keep the state of the products Cart of an user.
The flow is as follows :
I am doing a request from the front-end each time a user adds a product in the cart, when this happens, I am saving the contents of the cart(that consist of only the products ID's) in the database using the CartItem entity.
The problem
With this approach, instead of saving only the last product added to the cart(or replace the cart completely with the new values similar to an update), it is inserting the same object over and over again but with all the new appended values instead of overwriting the old object(table) in the database. An example of this would be in this first image . As you can see I have added a product to the cart with id 327 first.
Next I am adding a product with id 328 but it also adds 327 a second time, this goes on and on, the more products I add. This last code snippet contains my controller code .
#PostMapping("/savecart")
public ResponseEntity<String> saveCartToDb(#RequestBody List<CartItem> cartItemList, Principal principal){
System.out.println(cartItemList);
User logedInUser = userService.findUserByUsername(principal.getName()).get();
List<CartItem> cartItem = logedInUser.getCartProduct();
if(cartItem.isEmpty()){
logedInUser.setCartProduct(cartItemList);
userService.saveNewUser(logedInUser);
}else {
cartItem = cartItemList;
logedInUser.setCartProduct(cartItem);
userService.saveNewUser(logedInUser);
}
// userService.saveNewUser(logedInUser);
return ResponseEntity.ok().body("ASD");
}
How can I overwrite the contents of the List<CartItems> for the user so that I will not append new and old values again and again ? Would a SET help as it won't allow duplicates ?
I have also tried this How do I update an entity using spring-data-jpa? but I a not sure that I need to create a #Query for this issue.
I managed to make it work. The solution is the following.
This is a bidirectional one-to-many / many-to-one issue. In order to be able to remove child elements from the parent, we need to decouple(detach) them from the parent. Since parent and child are bound together by a foreign key the detachment has to be done on both ends. If one has a reference to the other this will not work so BOTH REFERENCES have to be removed. 2 methods are needed and both need to be called when doing decoupling. This is what I used.
private Set<CartProduct> cartProduct;
This one to be added in the parent class (User).
public void removeChild(CartProduct child) {
this.cartProduct.remove(child);
}
This one to be added in the Child class
public void removeParent() {
this.user.removeChild(this);
this.user = null;
}
Methods also have to be called like this
for(CartProduct cartItem : cartItemList){
cartItem.removeParent();
logedInUser.removeChild(cartItem);
}
L.E
It may be that with the above implementation you will get a
java.util.ConcurrentModificationException: null
it happen to me in one of the cases too. In order to fix this I used an Iterator like below.
for (Iterator<CartProduct> iterator = cartItemList.iterator(); iterator.hasNext();) {
CartProduct cartItem = iterator.next();
if (cartItem != null) {
iterator.remove();
}
}
I have the following problem: I have three connected classes. I have annotated them but I am getting wrong results (described below):
#Entityd
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassBList")
private List<ClassB> listB;
...
}
#Entity
#Table(name="ClassB")
public class ClassB {
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassCList")
private List<ClassC> listC;
...
}
#Entity
#Table(name="ClassC")
public class ClassC {
#Id
#GeneratedValue()
private Long id = 0L;
...
#ElementCollection
private List<String> listD;
...
}
When I work with this structure for the first ClassA I create,save and load everything is ok. For a new instance of ClassA which I save to repo and load again, I suddenly have the strings of the first ClassA in listD.
The result I need is that every class is "independently" saved. So the collections of each class should hold unique (each one with its own id and sublists) objects.
What would be the best way (annotations) to model this classes in Java 8 with Spring Boot 2.2.0.M5 and javax.persistence-api 2.2 ?
EDIT:
I have now removed class B and rewrote classA to:
#Entity
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
#MapKey(name = "type")
private Map<String,Set<ClassC>> classCmap;
...
}
This is giving me an error like:
org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class
How can I model/solve/annotate this?
If you don't need to query data based on listD, I would suggest to keep the list as text in the database and use a converter:
#Converter
public class ListDConverter implements AttributeConverter<List<String>, String> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(List<String> listD) {
try {
return objectMapper.writeValueAsString(listD);
} catch(IOException e){
return null;
}
}
#Override
public List<String> convertToEntityAttribute(String stringListD) {
if(stringListD == null){
return Collections.emptyList();
}
try {
return objectMapper.readValue(stringListD, new TypeReference<List<String>>() {});
}catch(IOException e){
return Collections.emptyList();
}
}
}
and in your ClassC entity class :
#Convert(converter = ListDConverter.class)
private List<String> listD;
Why do I like this approach :
No extra table and joins => better performance
Easier to read listD in the database
#ElementCollection describes a table. So your code is probably creating a "listD" table with one column of type string, with no primary key.
Also, do you really want to use the SELECT fetch mode? That's going to generate 1 + b + b*c queries when you could just implement your data as sets (since you have unique identifiers) and use JOIN, which would result in one and only one query.
See this site for an explanation on how to use #ElementCollection.
I am starting with JPA coming from EF and tried to do a simple master-detail insert where the child has a composite key.
The foo is inserted alright (no errors, Hibernate just printing insert into Foo statement), but the bar just gets ignored.
I found this question where the relation is defined in the key, but I couldn't get it to work either (same issue as with my original solution, no exceptions, just no child insert either).
My code currently looks like this:
#Entity
public class Foo {
#Id
private String fooID;
#OneToMany(mappedBy = "foo")
private List<Bar> bars = new ArrayList<>();
// getter, setter,...
}
#Entity
public class Bar {
#EmbeddedId
private BarId id;
public Bar(){
this.id = new BarId();
}
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#MapsId("fooID")
#JoinColumn(name = "fooID", referencedColumnName = "fooID")
private Foo foo;
public void setFooId(String fooId){
this.id.setFooId(fooId);
}
public void setBarNo(int barNo){
this.id.setBarNo(barNo);
}
// other getter, setter,...
}
#Embeddable
public class BarId implements Serializable {
private String fooID;
private int barNo;
// getter, setter, hashCode, equals,...
}
// ...
EntityManager em = factory.createEntityManager();
em.getTransaction().begin();
Foo newFoo = new Foo();
newFoo.setFooID("baz");
Bar newBar = new Bar();
newBar.setFooId(newFoo.getFooId()); // even necessary?
newBar.setBarNo(1);
newBar.setFoo(newFoo);
newFoo.getBars().add(newBar);
em.persist(newFoo);
em.getTransaction().commit();
I am using JPA 2.2 with Hibernate 5.4 if that makes any difference.
To better clarify what I'm after (for everyone who knows a little EF):
foo.HasKey(f => f.FooID);
foo.HasMany(f => f.Bars).WithOne(b => b.Foo).HasForeignKey(b => b.FooID);
bar.HasKey(b => new {b.FooID, b.BarNo});
What do I have to change to get this to work?
Or am I using JPA entirely wrong to begin with?
Figured it out: the cascade = CascadeType.PERSIST has to be put on the master side and not on the detail side of the relation like so:
#OneToMany(mappedBy = "foo", cascade = CascadeType.PERSIST)
private List<Bar> bars = new ArrayList<>();
im having a problem when adding a new entry in a many-to-many relationship because the list is huge. Ex:
Item item = new Item(1);
Category cat = dao.find(1, Category.class);
List<Category> list = new ArrayList<>();
list.add(cat);
item.setCategoryList(list);
cat.getItemList().add(item);
The problem is that the Category Itens list is huge, with a lot of itens, so performing the cat.getItemList() takes a very long time. Everywhere i look for the correct way to add a many-to-many entry says that a need to do that. Can someone help?
Edit:
A little context: I organize my itens with tags, so 1 item can have multiple tags and 1 tag can have multiple itens, the time has pass and now i have tags with a lot of itens ( > 5.000), and now when i save a new item with one of thoses tags it takes a long time, i have debuged my code and found that most of the delay is in the cat.getItensList() line, with makes sense since it has a extensive list o itens. I have searched a lot for how to do this, and everyone says that the correct way to save a entry in a many-to-many case is to add to the list on both sides of the relationship, but if one side is huge, it will takes a lot of time since calling the getItensList() loads them in the context. Im looking for a way to save my item refering the tag witout loading all of the itens of that tag.
Edit 2:
My classes:
Item:
#Entity
#Table(name = "transacao")
#XmlRootElement
public class Transacao implements Serializable {
#ManyToMany(mappedBy = "transacaoList")
private List<Tagtransacao> tagtransacaoList;
...(other stuff)
}
Tag:
#Entity
#Table(name = "tagtransacao")
#XmlRootElement
public class Tagtransacao implements Serializable {
#JoinTable(name = "transacao_has_tagtransacao", joinColumns = {
#JoinColumn(name = "tagtransacao_idTagTransacao", referencedColumnName = "idTagTransacao")}, inverseJoinColumns = {
#JoinColumn(name = "transacao_idTransacao", referencedColumnName = "idTransacao")})
#ManyToMany
private List<Transacao> transacaoList;
...(other stuff)
}
Edit 3:
WHAT I DID TO SOLVE:
As answered by Ariel Kohan, i tried to do a NativeQuery to insert the relationship:
Query query = queryDAO.criarNativeQuery("INSERT INTO " + config.getNomeBanco() + ".`transacao_has_tagtransacao` "
+ "(`transacao_idTransacao`, `tagtransacao_idTagTransacao`) VALUES (:idTransacao, :idTag);");
query.setParameter("idTransacao", transacao.getIdTransacao());
query.setParameter("idTag", tag.getIdTagTransacao());
I was able to reduce the time of que query from 10s to 300milis what it is impressive. In the end its better for my project that it is already runnig to do that instead of creating a new class that represents the many-to-many reletionship. Thanks to everyone who tried to help \o/
In this case, I would prevent your code from load the item list in memory.
To do that, I can think about two options:
Using a #Modyfing query to insert the items directly in the DB.
[Recommended for cases where you want to avoid changing your model]
You can try to create the query using normal JPQL but, depending on your model, you may need to use a native query. Using native query would be something like this:
#Query(value = "insert into ...", nativeQuery = true)
void addItemToCategory(#Param("param1") Long param1, ...);
After creating this query, you will need to update your code removing the parts where you load the objects in memory and adding the parts to call the insert statements.
[Update]
As you mentioned in a comment, doing this improved your performance from 10s to 300milis.
Modify your Entities in order to replace #ManyToMany with #OneToManys relationship
The idea in this solution is to replace a ManyToMany relationship between entities A and B with an intermediate entity RelationAB. I think you can do this in two ways:
Save only the Ids from A and B in RelationAB as a composite key (of course you can add other fields like a Date or whatever you want).
Add an auto-generated Id to RelationAB and add A and B as other fields in the RelationAB entity.
I did an example using the first option (you will see that the classes are not public, this is just because I decided to do it in a single file for the sake of simplicity. Of course, you can do it in multiple files and with public classes if you want):
Entities A and B:
#Entity
class EntityA {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public EntityA() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
#Entity
class EntityB {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public EntityB() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
RelationABEntity and RelationABId:
#Embeddable
class RelationABId implements Serializable {
private Long entityAId;
private Long entityBId;
public RelationABId() {
}
public RelationABId(Long entityAId, Long entityBId) {
this.entityAId = entityAId;
this.entityBId = entityBId;
}
public Long getEntityAId() {
return entityAId;
}
public void setEntityAId(Long entityAId) {
this.entityAId = entityAId;
}
public Long getEntityBId() {
return entityBId;
}
public void setEntityBId(Long entityBId) {
this.entityBId = entityBId;
}
}
#Entity
class RelationABEntity {
#EmbeddedId
private RelationABId id;
public RelationABEntity() {
}
public RelationABEntity(Long entityAId, Long entityBId) {
this.id = new RelationABId(entityAId, entityBId);
}
public RelationABId getId() {
return id;
}
public void setId(RelationABId id) {
this.id = id;
}
}
My Repositories:
#Repository
interface RelationABEntityRepository extends JpaRepository<RelationABEntity, RelationABId> {
}
#Repository
interface ARepository extends JpaRepository<EntityA, Long> {
}
#Repository
interface BRepository extends JpaRepository<EntityB, Long> {
}
A test:
#RunWith(SpringRunner.class)
#DataJpaTest
public class DemoApplicationTest {
#Autowired RelationABEntityRepository relationABEntityRepository;
#Autowired ARepository aRepository;
#Autowired BRepository bRepository;
#Test
public void test(){
EntityA a = new EntityA();
a = aRepository.save(a);
EntityB b = new EntityB();
b = bRepository.save(b);
//Entities A and B in the DB at this point
RelationABId relationABID = new RelationABId(a.getId(), b.getId());
final boolean relationshipExist = relationABEntityRepository.existsById(relationABID);
assertFalse(relationshipExist);
if(! relationshipExist){
RelationABEntity relation = new RelationABEntity(a.getId(), b.getId());
relationABEntityRepository.save(relation);
}
final boolean relationshipExitNow = relationABEntityRepository.existsById(relationABID);
assertTrue(relationshipExitNow);
/**
* As you can see, modifying your model you can create relationships without loading big list and without complex queries.
*/
}
}
The code above explains another way to handle this kind of things. Of course, you can make modifications according to what you exactly need.
Hope this helps :)
This is basically copied from a similar answer I gave earlier but similar question as well. The code below ran when I first write it but I changed the names to match this question so there might be some typos. The spring-data-jpa is a layer on top of JPA. Each entity has its own repository and you have to deal with that. For dealing with the many-to-many relations specifically in spring-data-jpa you can make a separate repository for the link table if you think it's a good idea.
#Entity
public class Item {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemCategory> categories;
#Entity
public class Category {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemCategory> items;
#Entity
public class ItemCategory {
#EmbeddedId
private ItemcategoryId id = new ItemcategoryId();
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("itemId")
private Item Item;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("categoryId")
private Category category;
public ItemCategory() {}
public ItemCategory(Item Item, Category category) {
this.item = item;
this.category = category;
}
#SuppressWarnings("serial")
#Embeddable
public class ItemCategoryId implements Serializable {
private Long itemId;
private Long categoryId;
#Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ItemCategoryId that = (ItemCategoryId) o;
return Objects.equals(itemId, that.itemId) && Objects.equals(categoryId, that.categoryId);
}
#Override
public int hashCode() {
return Objects.hash(itemId, categoryId);
}
And to use it. Step 3 shows the way you are currently doing it and creates a read of the existing joins before doing the update. Step 4 just inserts a relation directly in the join table and does not cause a pre-read of the existing joins.
#Transactional
private void update() {
System.out.println("Step 1");
Category category1 = new Category();
Item item1 = new Item();
ItemCategory i1c1 = new ItemCategory(Item1, Category1);
categoryRepo.save(Category1);
ItemRepo.save(Item1);
ItemCategoryRepo.save(p1t1);
System.out.println("Step 2");
Category category2 = new Category();
Item item2 = new Item();
ItemCategory p2t2 = new ItemCategory(item2, category2);
ItemRepo.save(item2);
categoryRepo.save(category2);
ItemCategoryRepo.save(p2t2);
System.out.println("Step 3");
category2 = CategoryRepo.getOneWithitems(2L);
category2.getitems().add(new ItemCategory(item1, category2));
categoryRepo.save(Category2);
System.out.println("Step 4 -- better");
ItemCategory i2c1 = new ItemCategory(item2, category1);
itemCategoryRepo.save(i2c1);
}
I don't explicitly set the ItemCategoryId id's. These are handled by the persistence layer (hibernate in this case).
Note also that you can update ItemCategory entries either explicity with its own repo or by adding and removing them from the list since CascadeType.ALL is set, as shown. The problem with using the CascadeType.ALL for spring-data-jpa is that even though you prefetch the join table entities spring-data-jpa will do it again anyway. Trying to update the relationship through the CascadeType.ALL for new entities is problematic.
Without the CascadeType neither the items or categories lists (which should be Sets) are the owners of the relationship so adding to them wouldn't accomplish anything in terms of persistence and would be for query results only.
When reading the ItemCategory relationships you need to specifically fetch them since you don't have FetchType.EAGER. The problem with FetchType.EAGER is the overhead if you don't want the joins and also if you put it on both Category and Item then you will create a recursive fetch that gets all categories and items for any query.
#Query("select c from Category c left outer join fetch c.items is left outer join fetch is.Item where t.id = :id")
Category getOneWithItems(#Param("id") Long id);
I'm having two tables -
Foo { foo_id, name }
Foo_properties { fp_id, foo_id, phoneNumber}
Now I want to map this in my object model using hibernate..
I need a foo_id in Foo_properties because i want to maintain referential integrity and want to add ON DELETE CASCADE constraint.
so I mapped the relation in the following way -
#Entity
public class Foo{
#Id
private long foo_id;
private String name;
#OneToOne(mappedBy = "foo")
private FooProperties fooProperties;
}
#Entity
public class FooProperties{
#Id
private long fp_id;
private String phoneNumber;
#OneToOne
#JoinColumn(name = "foo_id", nullable = false)
private Foo foo;
}
Now since the owning side is FooProperties class, I'm facing following issues -
If I set the new instance of FooProperties to Foo the existing FooProperties still remains in DB and hibernate doesn't delete that instance, e.g.
Foo foo = entityManager.find(Foo.class, fooId);
foo.setFooProperties(new FooProperties("xxx-xxx-xxx"));
entityManager.merge(foo);
This results into the new row in FooProperties table along with the existing one. Now I don't understand how I can change my mapping to so I can have above code (or variant of it) working for all scenarios, that means I need Foo as a owning side and foo_id in FooProperties. Is there any way to define the mapping like this?
NOTE: I already asked question based on this but I think I wasn't clear in previous question so asked this another one.
You were already told to use orphanRemoval = true or CascadeType.DELETE_ORPHAN. However, due to casuistics in interpretation of JPA Specification it wouldn't work as expected for one-to-one relationships (HHH-5559).
You can achieve a proper behaviour of orphanRemoval with the following trick:
#Entity
public class Foo{
#OneToMany(mappedBy = "foo", orphanRemoval = true)
private List<FooProperties> fooProperties;
public FooProperties getFooProperties() {
if (fooProperties == null || fooProperties.isEmpty()) return null;
else return fooProperties.get(0);
}
public void setFooProperties(FooProperties newFooProperties) {
if (fooProperties == null) fooProperties = new ArrayList<FooProperties>();
else fooProperties.clear();
if (newFooProperties != null)
fooProperties.add(newFooProperties);
}
...
}
#Entity
public class FooProperties{
#ManyToOne
#JoinColumn(name = "foo_id", nullable = false)
private Foo foo;
...
}
Or even this, if you don't need FooPropeties.foo:
#Entity
public class Foo{
#OneToMany(orphanRemoval = true)
#JoinColumn(name = "foo_id", nullable = false)
private List<FooProperties> fooProperties;
// getter/setter as above
...
}
Bar is the owner of the association (as indicated by the mappedBy on the inverse side) and thus the cascade has to be set there.
Edit:
To invert that, this might help.
There are 2 options for you to choose from, since you don't want to change your mapping :
Do it via your service layer logic. I think you have a similar question already.
Use the Hibernate annotation #Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN) on the Foo side of the relationship. However this is explicitly Hibenate and JPA 2 doesn't include support for the same.
I think instead of calling merge on the entity, if you directly call update on session object then hibernate will first delete the existing row and then it will add the new one. I implemented the same, but, in my case I used xml for mapping the entity. I hope this will help you.