I'm creating a database entity object Order, and assign it to multiple entities of type BookingCode.
Problem: this creates a single order in db, which is fine. But the order itself has a #OneToOne OrderDescription, which occurs duplicate in the database.
#Entity
public class BookingCode {
#Id
private Long id;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DETACH})
private Order order;
}
#Entity
public class Order {
#Id
private Long id;
private String orderName;
#OneToOne(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private OrderDescription description;
}
#Entity
public class OrderDescription {
#Id
private Long id;
//for simplicity just one text element; of course multiple fields in real life
private String text;
#OneToOne
private Order order;
}
Test:
Order order = new Order();
order.setOrderName("test");
OrderDescription d = new OrderDescription("testdescr");
d.setOrder(order);
order.setDescription(d);
List<BookingCodes> codes = new ArrayList<>();
BookingCode code = new BookingCode();
code.setOrder(order);
codes.add(order);
BookingCode code2 = new BookingCode();
code2.setOrder(order); //using the same offer entity!
codes.add(order2);
codes = dao.save(codes); //CrudRepository from Spring
dao.findOne(codes.get(0).getId()); //this works, find an order which has one of the OrderDescriptions
Result:
In my database I then have two OrderDescription entries, where I would expect only one, because I reused the same Order object and assigned it to different BookingCode objects.
Like:
table order_descrption:
1;"de";"testdescr";"123456"
2;"de";"testdescr";"123456"
As Order has a #OneToOne relation to OrderDescription
And I even don't understand why the select using findOne() works correctly. Because in database I now have two OrderDescriptions that map to the same Order, but an Order can only have one of them.
Persist the order first and then assign it to both bookingCode .
I had a similar issue where I had an Order obj and its variable prevOrder was referring to itself i.e. Order entity. And when I stored order, it would end up storing duplicate records for prevOrder.
I had the following code:
#Entity
#Table(name = "orders")
public class Order implements Serializable {
#Id
#GeneratedValue(generator = "order_id_generator")
#SequenceGenerator(name = "order_id_generator", sequenceName = "order_id_sequence", allocationSize = 1)
#Column(name = "id", updatable = false, nullable = false)
private Long id;
#OneToOne(cascade = CascadeType.ALL, optional = true)
#JoinColumn(name = "previous_order_id", unique = true, updatable = false, referencedColumnName = "id")
private Order previousOrder;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "previousOrder")
private Order nextOrder;
...
I tried various things including overriding equals and hashcode of Order, and adding a OneToOne mappedBy field 'nextOrder' etc. But noticed JPA didn't even call equals() to determine object's uniqueness. Ultimately I found out that JPA uses id field as the object's identifier and I wasn't storing the generated id while storing the object to a distrobuted cache. So it was all the time creating fresh objects during persistence.
Related
I have yet another #OneToMany question. In this case, I'm trying to model a person having a list of excluded people they shouldn't be able to send items to. This is a Spring Boot app using JPA.
In the code below, the exclusions list populates properly but the excludedBy List does not. Because of this, I believe that is causing the deletion of a Person that is excluded by another person to fail because the Exclusion in excludedBy is not mapped on the object properly.
#Entity
#Table(name = "person")
public class Person {
#Id
#GeneratedValue
#Column(nullable = false)
Long id;
...
#OneToMany(mappedBy = "sender", cascade = { CascadeType.ALL })
List<Exclusion> exclusions = new ArrayList<>();
//This is not getting populated
#JsonIgnore
#OneToMany(mappedBy = "receiver", cascade = { CascadeType.ALL })
List<Exclusion> excludedBy = new ArrayList<>();
...
}
#Entity
#Table(name = "exclusions")
public class Exclusion {
#Id
#GeneratedValue
#Column(nullable = false)
Long id;
#ManyToOne
#JsonIgnore
Person sender;
#ManyToOne
Person receiver;
...
}
I would expect that this would have mapped the bidirectional relationship properly and as such the excludedBy List would be populated as well.
Any wisdom on this matter would be great!
1 - An #Id is by default not nullable, not required:
#Column(nullable = false)
2 - There is no need for an #Id in this class. Both sides of the exclusion are together unique. Not needed:
#Id
#GeneratedValue
Long id;
3 - An "Exclusion" requires both an excludedBy and an excluded, give them names that match and they are your #Id. It is a 2 way ManyToMany relationship.
#Entity
#Table(name = "exclusions")
public class Exclusion {
#Id
#ManyToMany // An ID so not optional, so no need for (optional = false)
Person excludedBy;
#Id
#ManyToMany // An ID so not optional, so no need for (optional = false)
Person excluded;
}
Entity Exclusion always knows both sides of the story.
#ManyToMany(mappedBy = "excludedBy", cascade = { CascadeType.ALL })
List<Exclusion> excluded = new ArrayList<>();
#ManyToMany(mappedBy = "excluded", cascade = { CascadeType.ALL })
List<Exclusion> excludedBy = new ArrayList<>();
Tip: JSON DTOs shouldn't be defined in your JPA DTOs, otherwise you can't change your internal data model independently of your external API model.
I had this problem in the past. Your key problem ist that your ORM Mapper hibernate does not know which of your database entries need to be assinged to exclusions and which are assiged to excludedBy. You need a discriminator and add the constraint in your select. I would propose a solution that looks something like this:
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "PRIMARY_KEX_IN_EXCLUSION_TABLE", referencedColumnName = "id")
#Where(clause = "is_excluded_by = 0")
private Set<Exclusion> exclusions;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "PRIMARY_KEX_IN_EXCLUSION_TABLE", referencedColumnName = "id")
#Where(clause = "is_excluded_by = 1")
private Set<Exclusion> excludedBy;
the value isExcludedBy needs to be a database column, part of your Entity and set in your code manually.
I think you also need to use Set instead of List when having multiple collections in one Entity. https://vladmihalcea.com/spring-data-jpa-multiplebagfetchexception/
I have two entity classes.
Order.java
#Entity
#Table(name = "order_table")
public class Order implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "order_id", referencedColumnName = "id", nullable = false, insertable=false, updatable=false)
private Set<Item> items;
// getters & setters & toString
Item.java
#Entity
#Table(name = "item")
public class Item implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#Column(name = "order_id", nullable = false)
private Long orderId;
// getters & setters && toString
I created a test class like this:
#Test
public void createOrderWithItems() {
Item item = new Item();
item.setName("Iron Man");
Order order = new Order();
order.setName("Toy");
order.getItems().add(item);
Order created = service.createOrder(order);
Order orderById = service.getOrderById(order.getId());
System.out.println("Created Order: " + orderById);
Item itemById = service.getItemById(item.getId());
System.out.println("Created item: " + itemById);
Assert.notNull(created.getId(), "Order ID is Null");
}
Test is green but if you check output, you'll see that orderId field in the Item class is null.
Created Order: Order{id=1, name='Toy', items=[Item{id=2, name='Iron Man', orderId=null}]}
Created item: Item{id=2, name='Iron Man', orderId=null}
Does JPA not update this column in the db automatically? Is this column is redundant? If so, how can I retrieve this information from test code?
You need to set orderId explicitly.
item.setOrderId(order.getId());
order.getItems().add(item);
You can create a method addItem(Item item) in your Order class and hide this logic within it.
Cascading will create an entry in db but it won't initialize field. JPA annotations just indicate to JPA provider how to perform mapping between entity and table.
Moreover, check your annotations. #JoinColumn should be used in the entity which owns the relationship (the corresponding table has column as a foreign key). Check the top answer for this question for detailed explanations: What's the difference between #JoinColumn and mappedBy when using a JPA #OneToMany association
When retrieving values from DB hibernate does not keep insertion order in collections of type Set but keeps it in List collections. I've tried to specify LinkedHashSet manually but it does not help.
I have below parent entity
#Entity
#Table(name = "radio")
#PrimaryKeyJoinColumn(name = "id", referencedColumnName = "id")
public class Radio extends AbstractField {
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "radio_id")
private Set<Choice> choices = new LinkedHashSet<>();
}
And child entity:
#Entity
#Table(name = "choice")
public class ChoiceEntity {
#Id
#GeneratedValue
#Column(name = "id", nullable = false)
private UUID id;
#Column(name = "value")
private String value;
}
When I am retriving values from the DB, hibernate stores them in choices collection in random order. When I've changed collection type to use List everything works fine.
Is it possible to configure hibernate to keep DB order in the collections of type Set without using any addition order_row.
Use #javax.persistence.OrderBy or #org.hibernate.annotations.OrderBy. The former expects a fragment defined in HQL/JPQL whereas the latter expects a SQL fragment
I am experiencing a problem with hibernate and lazy loading of objects.
basically I want to load an class which has an eagerly loaded field and not load the lazy fields of child classes
Take the following QuestionVO class
#Entity
#Table(name = "question")
public class QuestionVO extends BaseDAOVO implements Serializable {
/**
*
*/
private static final long serialVersionUID = -5867047752936216092L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#Column(name = "questionText", unique = false, nullable = false, length = 4000)
#Size(min = 3, max = 4000)
#Pattern(regexp = MobileAppsRegexConstants.GENERAL_ALLOWED_CHARCHTERS, message = "Question Text Not valid.")
private String questionText;
#ManyToOne(fetch = FetchType.EAGER)
#Cascade({ CascadeType.SAVE_UPDATE })
#JoinColumn(name = "MENU_STYLE_ID", nullable = true)
private MenuStyleVO menuStyle;
}
Take the following MenuStyleVO class
#Entity
#Table(name = "menu_style")
public class MenuStyleVO extends BaseDAOVO implements Serializable{
/**
*
*/
private static final long serialVersionUID = 3697798179195096156L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#Column(name = "menuStyleName", unique = false, nullable = false, length = 200)
private String menuStyleName;
#Column(name = "menuTemplate", unique = false, nullable = false, length = 200)
private String menuTemplate;
#OneToOne(fetch = FetchType.LAZY, optional=false)
#Cascade({ CascadeType.SAVE_UPDATE })
#JoinColumn(name="logo_id")
#JsonProperty("logo")
private ApplicationImageVO logo;
}
And this ApplicationImageVO class
#Entity
#Table(name = "application_image")
public class ApplicationImageVO extends BaseDAOVO implements Serializable {
/**
*
*/
private static final long serialVersionUID = -9158898930601867545L;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "image1242x2208")
#Cascade({ CascadeType.ALL })
#JsonIgnore
private SubmissionLauncherImagesVO launcherImage1242x2208;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "image1536x2048")
#Cascade({ CascadeType.ALL })
#JsonIgnore
private SubmissionLauncherImagesVO launcherImage1536x2048;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "image2048x1536")
#Cascade({ CascadeType.ALL })
#JsonIgnore
private SubmissionLauncherImagesVO launcherImage2048x1536;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "logo")
#Cascade({ CascadeType.ALL })
#JsonIgnore
private MenuStyleVO menuStyleLogo;
}
If L load the QuestionVO class from the database using the following hibernate criteria code - all the lazy fields of MenuStyleVO and ApplicationImageVO are also loaded.
On complicated use cases, this results in this query getting very slow
public QuestionVO findMasterAppQuestionById(int id) {
Criteria criteria = currentSession().createCriteria(QuestionVO.class);
criteria.add(Restrictions.eq("id", id));
QuestionVO questionVO = (QuestionVO) criteria.uniqueResult();
return questionVO;
}
What I am wondering is - would it be possible to load the QuestionVO class and its eager fields and tell hibernate to ignore lazy fields from the other classes bar those that are needed?
Cheers
Damien
Last time we faced an issue like this we used a constructor on parent class, which use only the desired fields of determined query.
I can't remember in fully how constructor inside a jpql query works, but it must be something like this:
select new com.package.class(c.field1, c.field2) from com.package.class c
Remember, a constructor with same arguments must be present on the desired entity.
Pros:
- Better query perfomance;
- Can be replicated with other arguments;
Cons:
- Pretty limited, you can only use this hack on the main entity you are querying;
- Includes a constructor only for determined query, poor design;
Also, you should take a look on EnttyGraphs of JPA. Seems quite promising, but didn't work as desired in our project.
Btw, Hibernate has put us many times on performance issues, hope this hack help you, good luck!
Edit:
Why this pattern would help in performance issues?
Basically, with the example i've showed before, you are not loading everything via Hibernate, only the two fields (field1 and field2) of the main entity. Without using a constructor you shoudn't be able to do that, because your query would not result in a collection of the desired entity, but in a collection of two objects each iteration (Object[]). Using the constructor pattern you are creating instances of the desired entity, but only selecting a few fields from database, and that's why this pattern can help you, you are returning a collection of the desired entity with only a few fields.
I have two classes.
public class Invoice {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "invoice_id", unique = true)
private int invId;
#OneToMany(mappedBy = "invoiceList", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private List<Item> itemList;
#Column(name = "invoice_amt", nullable = false)
private Double invAmt;
}
And,
public class Item {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "item_id", unique = true)
private int itemId;
#ManyToOne(optional = false, targetEntity = Invoice.class)
#JoinColumn(name="invoice_id")
private List<Invoice> invoiceList;
}
I am new to JPA. So my understanding may not be accurate.
My understanding is that, if I save Invoice, the invoice_id of that instant should cascade down to invoice_id of all the items.
However, I see Item being saved but get null in place of invoice_id of the Item.
What am I missing?
UPDATE!!! UPDATE!!!
Ok so I changed the #ManyToOne to be a singular attribute and did objItem.setInvoice(objInvoice) and saved it. However, I still get NULL on invoice_id.
You are annotating a many-to-one relation, but use collections on both sides. This will not work. The one-side has to map the relation to a singular attribute. In your case, it would be
#ManyToOne
private Invoice invoice
Perhaps you rather need a many-to-many relation. In this case, you will need to change the annotations to #ManyToMany and get rid of the cascades (they tend not to work as expected from a many-side).
targetEntity attribute and the #JoinColumn annotation are redundant on the invoice attribute of Item.
In order for the Item to save the id of the related invoice, you first need to set the invoice attribute of the Item since item is the owning side (the one where the relation information is stored).
I'm not sure this is your only problem, but a 1:n relationship shouldn't have a List both ways. If you turn List<Invoice> into a simple Invoice object, you'll at least be closer to a solution. We can go from there if your code still fails.
public class Item {
#ManyToOne(optional = false, targetEntity = Invoice.class)
#JoinColumn(name = "invoice_id")
private Invoice invoice;
}
public class Invoice {
#OneToMany(mappedBy = "invoiceList", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private List<Item> itemList;
}