HIbernate/JPA Annotation Join ManyToOne with multiple columns but with conditional OR - java

Let's say I have these Entities
Company,User,Project and Recommendation
But what I want to highlight is the Project and Recommendation table.
Below is the entity sample.
//Project entity
#Column(name = "id")
private String id;
#Column(name = "user_id")
private String userId;
#Column(name = "company_id", nullable = true)
private String companyId;
//Recommendation entity
#Column(name = "id")
private String id;
#Column(name = "user_id")
private String userId;
#Column(name = "company_id", nullable = true)
private String companyId;
#OneToMany(fetch = FetchType.LAZY)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumns({
#JoinColumn(name = "id", referencedColumnName = "user_id", nullable = true, insertable = false, updatable = false),
#JoinColumn(name = "id", referencedColumnName = "company_id", nullable = true, insertable = false, updatable = false)
})
private List<Project> projects;
Above tables are just illustration, sorry if that doesn't really make sense.
I want to join Project table inside Certification table.
They both are not directly related, so I need to join using these 2 columns, userId and companyId.
However, companyId is nullable. And whenever it is null, I still want to join the table only by userId.
What I want to achieve is probably along this line:
Select from Recommendation rec Join Project project
on rec.user_id = project.user_id and (rec.company_id = project.company_id OR rec.company_id is null)
Is it possible to use hibernate / JPA annotations to achieve this?
My project uses Hibernate 5 recently

Related

#ManyToOne in self-referencing entity with composite key - #IdClass, java, hibernate

Spent 3 days looking for a solution and finally I came here for community wisdom.
I have self-referencing entity like follows:
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Entity
#IdClass(CompositeUserId.class)
#Table(name = "user", schema = "dbo")
public class User implements Serializable {
#Id
#Column(name = "id")
private Integer id;
#Id
#Column(name = "first_name")
private String firstName;
#Id
#Column(name = "last_name")
private String lastName;
#ManyToOne
#JoinColumn(name = "parent_id", referencedColumnName = "id", insertable = false, updatable = false)
#JoinColumn(name = "first_name", referencedColumnName = "first_name", insertable = false, updatable = false)
#JoinColumn(name = "last_name", referencedColumnName = "last_name", insertable = false, updatable = false)
private User parent;
#OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
private Set<User> children;
my CompositeUserId.class:
#Getter
#Setter
#EqualsAndHashCode
#AllArgsConstructor
#NoArgsConstructor
public class UserCompositeId implements Serializable {
private Integer id;
private String firstName;
private String lastName;
When I try retrieve all data from my user table I get error:
org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find ...User with id UserCompositeId#19e66569; nested exception is javax.persistence.EntityNotFoundException:
I suppose there might be some kind of mistake in the #JoinColumn block.
Here is the sql query causing the error:
SELECT *
FROM dbo.user ur1 LEFT OUTER JOIN dbo.user ur2 ON ur1.first_name=ur2.first_name AND ur1.parent_id=ur2.id AND ur1.last_name=ur2.last_name
WHERE ur1.first_name='First Name' AND ur1.id=130 AND ur1.last_name='Last Name'
I ensured that the request does not return anything in the database by running it manually, but found out that if I will change id to parent_id it will return data, so again, probably some mistake in #JoinColumn block
You need you use #NotFound(action=NotFoundAction.IGNORE). If there is no records then it will assign null to it.
#ManyToOne
#JoinColumn(name = "parent_id", referencedColumnName = "id", insertable = false, updatable = false)
#JoinColumn(name = "first_name", referencedColumnName = "first_name", insertable = false, updatable = false)
#JoinColumn(name = "last_name", referencedColumnName = "last_name", insertable = false, updatable = false)
#NotFound(action=NotFoundAction.IGNORE)
private User parent;
The foreign key columns need different names than the primary key columns, e.g. like this (notice the 'parent' prefix in name):
#JoinColumn(name = "parent_id", referencedColumnName = "id", updatable = false, insertable = true)
#JoinColumn(name = "parent_firstname", referencedColumnName = "firstname", updatable = false, insertable = true)
#JoinColumn(name = "parent_lastname", referencedColumnName = "lastname", updatable = false, insertable = true)
#ManyToOne(fetch = FetchType.EAGER)
private UserEntity parent;
Additionally, the insertable value should be true, since otherwise no associations are persisted during the insert.
Be aware that a parent needs to be saved before a child can be associated. Since the parent fk is not updatable, is must be set on a newly created child before it is saved.
Feel free to have a look at this project containing the complete sample code:
Entity:
https://github.com/fladdimir/many-to-one-self-ref-composite-id/blob/master/src/main/java/org/demo/UserEntity.java
Integration-Test:
https://github.com/fladdimir/many-to-one-self-ref-composite-id/blob/master/src/test/java/org/demo/DemoApplicationTests.java
Also it is good practice to synchronize bidirectional associations:
https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/
The sample entity could e.g. use some methods like these:
public void addChild(UserEntity child) {
child.parent = this; // sync owning side of the association
this.children.add(child);
}
public void setParent(UserEntity parent) {
parent.addChild(this);
}

How to create a TypedQuery in JPQL that queries two tables using an implicit join?

I'm trying to learn how to use implicit joins in JPQL. An example I'm working from seems to suggest that one can reference a foreign key as a path and then have access to attributes in the table the foreign key references.
It's a 1:M mandatory relationship between User and Report.
userId is the foreign key that references User.
I've checked that I have the right libraries imported that I have the right mapping (#JoinColumn, #ManyToOne, #OneToMany, mappedBy, etc), everything seems to be okay.
#GET
#Path("findByTotalCalBurnedAndHeight/{height}/{totalCalBurned}")
#Produces({"application/json"})
public List<Report> findByTotalCalBurnedAndHeight(#PathParam("height") Integer height, #PathParam("totalCalBurned") Integer totalCalBurned) {
TypedQuery<Report> q = em.createQuery("SELECT r FROM Report r WHERE r.totalCalBurned = :totalCalBurned AND r.userId.height = :height", Report.class);
q.setParameter("height", height);
q.setParameter("totalCalBurned", totalCalBurned);
return q.getResultList();
}
As seen above I'm trying to access the "height" attribute in the "User" table with: r.UserId.height
Based on the example I'm working from I'd expect the join to work here however the result I'm getting is this error: "The state field path 'r.userId.height' cannot be resolved to a valid type."
Am I missing something here? Any feedback is much appreciated.
Update to show mapping:
In report class:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "REPORT_ID")
private Integer reportId;
#JoinColumn(name = "USER_ID", referencedColumnName = "USER_ID")
#ManyToOne(optional = false)
#Basic(optional = false)
#NotNull
#Column(name = "USER_ID")
private Integer userId;
In user class:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "USER_ID")
private Integer userId;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "userId")
#Basic(optional = false)
#NotNull
#Column(name = "HEIGHT")
private int height;
The problem is this part:
r.userId.height
In order for this to work, userId must be an entity. You can define a relationship with user field in Report and write something like:
"SELECT r FROM Report r left join r.user u WHERE r.totalCalBurned = :totalCalBurned AND u.height = :height"
Edit - change this:
#JoinColumn(name = "USER_ID", referencedColumnName = "USER_ID")
#ManyToOne(optional = false)
#Basic(optional = false)
#NotNull
#Column(name = "USER_ID")
private Integer userId;
into:
#JoinColumn(name = "USER_ID")
#ManyToOne(optional = false)
#Basic(optional = false)
#NotNull
private User user;

Hibernate association tables

I have 2 entities that have Id's annotated but those Id's aren the primary keys in the tables. I am still mapping the PK's in to the entity for now to limit the initial impact of the change. But the association table that uses the PK's to associate the many to many relationship is throwing the following error:
java.lang.IllegalArgumentException: Provided id of the wrong type for class. Expected: class java.util.UUID, got class java.lang.Long
The entity #Id is the UUID but the table PK which is a Long is mapped as the #JoinColumn
The composite key for the association entity
#Embeddable
public class CustomerAccountId implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "user_id", nullable = false)
private Long customerId;
#Column(name = "account_id", nullable = false)
private Long accountId;
The association entity:
#EmbeddedId
#AttributeOverrides({
#AttributeOverride(name = "customerId", column = #Column(name = "user_id", nullable = false)),
#AttributeOverride(name = "accountId", column = #Column(name = "account_id", nullable = false))
})
private CustomerAccountId id;
#ManyToOne
#JoinColumn(name = "user_id", insertable = false, updatable = false, referencedColumnName = "user_id")
private Customer customer;
#ManyToOne
#JoinColumn(name = "account_id", insertable = false, updatable = false, referencedColumnName = "id")
private Account account;
The failing entity:
#Column(name = "user_id")
private Long serialId; // also the primary key in the table
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#org.hibernate.annotations.Type(type="pg-uuid")
#Column(name = "uid", updatable = false)
private UUID id;
Does anyone know if this is even possible? or am I going to be forced to update the content in the association table when I push this change?
put #Id and #GeneratedValue on the field that represent the table data then hibernate will map a long (sgbd) whit a long (table)
or
your (sgbd) table data type must be compatible with the (java) uuid type.
Why these 2 keys on your table?
I think it's not possible to have 2 PK for one entity. At most you can have a composite key base on your serialID and the UUID.
see How to map a composite key with Hibernate? for that
Or mark as #Id the real PK in the SGBD. Use the other in Java as a classic value in the table's point of view
The solution I decided to go with is the following:
#ManyToOne
#JoinColumns ( value = {
#JoinColumn(name = "user_id", insertable = false, updatable = false, referencedColumnName = "user_id"),
#JoinColumn(name = "user_uid", insertable = false, updatable = false, referencedColumnName = "uid")
})
private Customer customer;
In a nutshell I simply added the UUID to the association table and used both columns for the Customer Entity.
To address #Tokazio's question about using UUID and serial Id, the data warehouse conversion is impacted significantly so I need to slowly move from serial Id's to UUID's to minimize impacts and prevent outages.

Spring Data JPA + Hibernate + Postgresql LTree. Is it possible to load whole subtree by given node with one query?

I'm using Spring Data JPA + Postgresql and I have entity "Category" which implements Postgresql LTree type and looks like this :
#Id
#SequenceGenerator(name = "categories_id_seq", sequenceName = "categories_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "categories_id_seq")
#Column(name = "id", updatable = false)
private Integer id;
#Column(name = "name")
private String name;
#Column(name = "title")
private String title;
#Column(name = "trail")
#Type(type = "ltree")
private String trail;
#Column(name = "layer")
private Integer layer;
#Column(name = "parent_id")
private Integer parentId;
#ManyToOne()
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "parent_id", insertable = false, updatable = false)
private Category parent;
#OneToMany
#Fetch(FetchMode.JOIN)
#BatchSize(size = 20)
#JoinColumn(name = "parent_id", insertable = false, updatable = false)
private Set<Category> childrens;
My problem is when i want to load whole subtree from given node to "leaf" nodes Hibernate issues 2 additional selects for each pair of parent and childrens.
How can i change this Hibernate behavior?
If you want to minimize the Hibernate overhead why don't you define your custom #NamedNativeQuery in the entity or directly use createNativeQuery(sqlString, Category.class)? ORM are useful but they also not very effective with complex queries/table structures.

Hibernate query causing lots extra unnecassery queries

I am developing a auction site. The problem lies in 3 entities i use:
Product (has zero or many ProductBid)
ProductBid (has zero or one ProductBidRejection)
ProductBidRejection
I use a hibernate query to get the bids:
select pb from ProductBid pb left join pb.rejection pbr where pbr is null and pb.product = :product order by pb.amount desc
This generates this query (via console):
select
productbid0_.id as id4_,
productbid0_.amount as amount4_,
productbid0_.bid_by as bid4_4_,
productbid0_.date as date4_,
productbid0_.product_id as product5_4_
from
product_bids productbid0_
left outer join
product_bid_rejections productbid1_
on productbid0_.id=productbid1_.product_bid_id
where
(
productbid1_.id is null
)
and productbid0_.product_id=?
But for each bid it gets it also generates:
select
productbid0_.id as id3_1_,
productbid0_.date_rejected as date2_3_1_,
productbid0_.product_bid_id as product4_3_1_,
productbid0_.reason as reason3_1_,
productbid0_.rejected_by as rejected5_3_1_,
productbid1_.id as id4_0_,
productbid1_.amount as amount4_0_,
productbid1_.bid_by as bid4_4_0_,
productbid1_.date as date4_0_,
productbid1_.product_id as product5_4_0_
from
product_bid_rejections productbid0_
inner join
product_bids productbid1_
on productbid0_.product_bid_id=productbid1_.id
where
productbid0_.product_bid_id=?
These are my entities:
ProductBid
#Entity
#Table(name = "product_bids")
public class ProductBid
{
#Column(name = "id", nullable = false)
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#JoinColumn(name = "product_id", nullable = false)
#Index(name="product")
#ManyToOne(fetch = FetchType.LAZY)
private Product product;
#Column(name = "amount", nullable = false)
private BigDecimal amount;
#JoinColumn(name = "bid_by", nullable = false)
#Index(name="bidBy")
#ManyToOne(fetch = FetchType.LAZY)
#Fetch(FetchMode.JOIN)
private User bidBy;
#Column(name = "date", nullable = false)
#Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
private DateTime date;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "productBid")
private ProductBidRejection rejection;
}
ProductBidRejection
#Entity
#Table(name = "product_bid_rejections")
public class ProductBidRejection
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private long id;
#Column(name = "reason", nullable = false, columnDefinition = "TEXT")
private String reason;
#Column(name = "date_rejected", nullable = false)
#Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
private DateTime dateRejected;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "rejected_by", nullable = false)
private User rejectedBy;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_bid_id", nullable = false)
#Fetch(FetchMode.JOIN)
private ProductBid productBid;
}
Its because you have #Fetch(FetchMode.JOIN) on ProductBid.
So for each of the ProductBidRejections you retrieve, it also loads a ProductBid.
UPDATE
Try this query. It will get distinct pb and eagerly fetch the PBR
select distinct pb from ProductBid pb left join fetch pb.rejection pbr where pbr is null and pb.product = :product order by pb.amount desc
Use Criteria instead of HQL your problem will be solve
session.createCriteria(ProductBid.class).add(Restrictions.eq("product",yourproduct)).list();
and in ProductBid Entity Class use annotation to join EAGER ly to ProductBidRejection

Categories