Querying composite table in Hibernate - spring

I am working on a Spring-MVC application where I have a many-to-many relationship in which I have to query in 2 tables to get the values I require. I will explain in more detail.
I have 2 tables GroupAccount, GroupMembers with many-to-many
relationship. Now there is a junction table called membertable where
id from GroupMembers and GroupAccount is stored.
This is what I am looking for :
I pass a groupAccounId and username as parameters. Now, in the
GroupMembers table, there is a username stored. In groupAccount,
there is groupAccountId is stored.
Now in the memberjunction, I have composite key
memberid,GroupAccountId, I would like the member id for the username
which has a matching groupAccountId I submit.
Below is the SQL code and Spring-mvc code to understand more better.
CREATE TABLE public.groupaccount (
groupid NUMERIC NOT NULL,
groupname VARCHAR,
groupaccountstatus BOOLEAN DEFAULT false NOT NULL,
adminusername VARCHAR,
CONSTRAINT groupid PRIMARY KEY (groupid)
);
CREATE TABLE public.groupmembers (
memberid INTEGER NOT NULL,
musername VARCHAR
CONSTRAINT memberid PRIMARY KEY (memberid)
);
CREATE TABLE public.memberjunction (
memberid INTEGER NOT NULL,
groupid NUMERIC NOT NULL,
CONSTRAINT membergroupid PRIMARY KEY (memberid, groupid)
);
GroupMembersDAOImpl :#
#Override
public List<Integer> returnMemberIdWithMatchingUsername(String memberUsername) {
session = this.sessionFactory.getCurrentSession();
org.hibernate.Query query = session.createQuery("From GroupMembers as " +
"n where n.memberUsername=:memberUsername");
query.setParameter("memberUsername",memberUsername);
List<GroupMembers> memberList = query.list();
List<Integer> memberIdList = new ArrayList<>();
for(GroupMembers members :memberList){
memberIdList.add(members.getMemberid());
}
return memberIdList;
}
GroupAccount model :
#Entity
#Table(name="groupaccount")
public class GroupAccount {
#Id
#Column(name="groupid")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "groupaccount_seq_gen")
#SequenceGenerator(name = "groupaccount_seq_gen",sequenceName = "groupaccount_seq")
private Long groupId;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "memberjunction", joinColumns = {#JoinColumn(name = "groupid")},
inverseJoinColumns = {#JoinColumn(name = "memberid")})
private Set<GroupMembers> groupMembersSet = new HashSet<>();
public void setGroupMembersSet(Set<GroupMembers> groupMembersSet){
this.groupMembersSet = groupMembersSet;
}
}
GroupMembers model class :
#Entity
#Table(name="groupmembers")
public class GroupMembers {
#Id
#Column(name="memberid")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "groupmembers_seq_gen")
#SequenceGenerator(name = "groupmembers_seq_gen",sequenceName = "groupmembers_seq")
private int memberid;
#ManyToMany(mappedBy = "groupMembersSet")
private Set<GroupAccount> groupAccounts = new HashSet<>();
public void setGroupAccounts(Set<GroupAccount> groupAccounts){
this.groupAccounts = groupAccounts;
}
public Set<GroupAccount> getGroupAccounts(){
return this.groupAccounts;
}
}
Query I am using :
#Override
public int getMemberIdForCanvas(String memberUsername, Long groupId) {
session = this.sessionFactory.getCurrentSession();
org.hibernate.Query query = session.createQuery("select distinct m.memberId from GroupMembers m\n" +
"join m.groupAccounts a\n" +
"where a.memberUsername = :userName and m.groupId=:groupId");
query.setParameter(memberUsername,"memberUsername");
query.setParameter(String.valueOf(groupId),"groupId");
int memberid = (Integer)query.uniqueResult();
return memberid;
}
Any help would be nice. Thanks a lot.

Here's the documentation for joins and HQL. Please read it.
The query is as simple as
select distinct m.memberId from GroupMembers m
join m.groupAccounts a
where a.memberUsername = :userName
Please also fix your naming. A GroupMembers instance is a single group member. So the class should be named GroupMember, without s. Repeating the name of the class in the fields of this class is also redundant: member.getId() is more readable and less verbose than member.getMemberId(). Same for the other fields.

Related

#SqlResultSetMapping unknown columns referenced in entity mapping

I am trying to map only certain fields to the entity object using a native SQL query :
#NamedNativeQuery(name = "CustomerEntity.findOnlyNameAndPhoneFromCustomer", query = "select customer_name, customer_email from customer",
resultSetMapping = "nativeMapping")
#SqlResultSetMappings(value = {
#SqlResultSetMapping(name = "nativeMapping",
entities = {
#EntityResult(
entityClass = CustomerEntity.class,
fields = {
#FieldResult(name = "name", column = "customer_name"),
#FieldResult(name = "email", column = "customer_email")
}
)})})
#Entity
class CustomerEntity {
//getter and setter fields
#Column(name="customer_name")
private String name;
#Column(name="customer_email")
private String email;
#Column(name="address")
private String adddress;
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#Access(AccessType.PROPERTY)
#Column(columnDefinition = "VARCHAR(36)", name = "customer_guid")
#Type(type = "uuid-char")
private UUID guid;
#Embedded
private AuditFields audit;
}
Repository:
#Query(nativeQuery = true)
List<CustomerEntity> findOnlyNameAndPhoneFromCustomer();
I am not trying to map all the fields that are present in the customer table to CustomerEntity, I am only projecting certain fields.
This is giving me errors like:
17:44:37.841 [ERROR] o.h.e.j.s.SqlExceptionHelper - The column name address2_6_0_ is not valid.
There is no column called address2_6_0_ in my table, but there is a column called address, why is the address column being renamed and referenced here ?
I am only referencing customer_name and customer_email.
What is going on ?
Thanks.
entities is for "mapping to entities".
#EntityResult:
If this annotation is used, the SQL statement should select all of the columns that are mapped to the entity object.
Therefore, you should use classes and #ConstructorResult for "mapping to DTOs".
#NamedNativeQuery(name = "CustomerEntity.findOnlyNameAndPhoneFromCustomer",
query = "select customer_name, customer_email from customer",
resultSetMapping = "nativeMapping")
#SqlResultSetMappings(value = {
#SqlResultSetMapping(name = "nativeMapping",
classes = #ConstructorResult(columns = { #ColumnResult(name = "customer_name"), #ColumnResult(name = "customer_email") },
targetClass = CustomerEntity.class)) })
#Entity
public class CustomerEntity {
public CustomerEntity() {
}
public CustomerEntity(String name, String email) {
this.name = name;
this.email = email;
}
...
}

Specification API/Criteria API - Group by data returned by existing specification object and findAll(specification,pageable)

Below are my entities:
Product
#Entity
#Table(name = "Product")
public class Product extends ReusableFields
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
Long productId;
#NonNull
#Column(name = "product_name")
String productName;
String measurementUnit;
//more fields and getters setters
}
Inward Outward List related to product:
#Entity
#Table(name = "inward_outward_entries")
public class InwardOutwardList extends ReusableFields
{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
Long entryid;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "productId", nullable = false)
#JsonIgnoreProperties(
{ "hibernateLazyInitializer", "handler" })
Product product;
#JsonSerialize(using = DoubleTwoDigitDecimalSerializer.class)
Double quantity;
//more fields
}
Inward Inventory:
#Entity
#Table(name = "inward_inventory")
public class InwardInventory extends ReusableFields implements Cloneable
{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "inwardid")
Long inwardid;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "inwardinventory_entry", joinColumns =
{ #JoinColumn(name = "inwardid", referencedColumnName = "inwardid") }, inverseJoinColumns =
{ #JoinColumn(name = "entryId", referencedColumnName = "entryId") })
Set<InwardOutwardList> inwardOutwardList = new HashSet<>();
//more fields
}
Inward Inventory Repo:
#Repository
public interface InwardInventoryRepo extends extends JpaRepository<InwardInventory, Long>, JpaSpecificationExecutor<InwardInventory> ,PagingAndSortingRepository<InwardInventory, Long>
{}
Previously the requirement was only to filter data and show as pages based on filters selected by user. So I have a working code to create specification dynamically based on inputs. It is working fine. After creating the specification, I am using:
Page<T> findAll(#Nullable Specification<T> spec, Pageable pageable);
to generate required list of records.
But, now a new requirement has been added to show sum of quantities grouped on product name and measurement unit. i.e. whatever data is returned after filter should be grouped by. Since the filtration logic is already working fine, I do not want to touch it.
Can somehow help how to reuse existing specification object and group the data returned by findall(specification,pageable) method.
What I already tried.
Since specification directly do not support group by, I autowired entity manager and created own criteria query. But this is not giving correct results as all the tables are getting joined twice. Might be because they are joined first during specification object and again during grouping by:
#Service
#Transactional
public class GroupBySpecification {
#Autowired
EntityManager entityManager;
Logger log = LoggerFactory.getLogger(GroupBySpecification.class);
public List<ProductGroupedDAO> findDataByConfiguration(Specification<InwardInventory> spec) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<ProductGroupedDAO> query = builder.createQuery(ProductGroupedDAO.class);
Root<T> root = query.from(InwardInventory.class);
Predicate p = spec.toPredicate(root, query, builder);
query.where(p);
Join< InwardInventory, InwardOutwardList> ioList = root.join(InwardInventory_.INWARD_OUTWARD_LIST);
Join<InwardOutwardList, Product> productList = ioList.join(InwardOutwardList_.PRODUCT);
query.multiselect(productList.get(Product_.PRODUCT_NAME), productList.get(Product_.MEASUREMENT_UNIT),
builder.sum(ioList.get(InwardOutwardList_.QUANTITY)));
query.groupBy(productList.get(Product_.PRODUCT_NAME), productList.get(Product_.MEASUREMENT_UNIT));
List<ProductGroupedDAO> groupedData = fetchData(query);
return groupedData;
}
Generated SQL - all the tables joined twice
SELECT DISTINCT product7_.product_name AS col_0_0_,
product10_.measurementunit AS col_1_0_,
Sum(inwardoutw12_.quantity) AS col_2_0_
FROM inward_inventory inwardinve0_
INNER JOIN inwardinventory_entry inwardoutw1_
ON inwardinve0_.inwardid = inwardoutw1_.inwardid
INNER JOIN inward_outward_entries inwardoutw2_
ON inwardoutw1_.entryid = inwardoutw2_.entryid
AND ( inwardoutw2_.is_deleted = 'false' )
INNER JOIN product product3_
ON inwardoutw2_.productid = product3_.productid
INNER JOIN warehouse warehouse4_
ON inwardinve0_.warehouse_id = warehouse4_.warehouse_id
INNER JOIN inwardinventory_entry inwardoutw5_
ON inwardinve0_.inwardid = inwardoutw5_.inwardid
INNER JOIN inward_outward_entries inwardoutw6_
ON inwardoutw5_.entryid = inwardoutw6_.entryid
AND ( inwardoutw6_.is_deleted = 'false' )
INNER JOIN product product7_
ON inwardoutw6_.productid = product7_.productid
INNER JOIN inwardinventory_entry inwardoutw8_
ON inwardinve0_.inwardid = inwardoutw8_.inwardid
INNER JOIN inward_outward_entries inwardoutw9_
ON inwardoutw8_.entryid = inwardoutw9_.entryid
AND ( inwardoutw9_.is_deleted = 'false' )
INNER JOIN product product10_
ON inwardoutw9_.productid = product10_.productid
INNER JOIN inwardinventory_entry inwardoutw11_
ON inwardinve0_.inwardid = inwardoutw11_.inwardid
INNER JOIN inward_outward_entries inwardoutw12_
ON inwardoutw11_.entryid = inwardoutw12_.entryid
AND ( inwardoutw12_.is_deleted = 'false' )
WHERE ( inwardinve0_.is_deleted = 'false' )
AND ( warehouse4_.warehousename LIKE ? )
AND ( product3_.product_name IN ( ?, ?, ?, ? ) )
GROUP BY product7_.product_name,
product10_.measurementunit
You will have to use the existing joins that are created by the specifications. You will probably have to do something like this:
public List<ProductGroupedDAO> findDataByConfiguration(Specification<InwardInventory> spec) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<ProductGroupedDAO> query = builder.createQuery(ProductGroupedDAO.class);
Root<T> root = query.from(InwardInventory.class);
Predicate p = spec.toPredicate(root, query, builder);
query.where(p);
Join< InwardInventory, InwardOutwardList> ioList = getOrCreateJoin(root, InwardInventory_.INWARD_OUTWARD_LIST);
Join<InwardOutwardList, Product> productList = getOrCreateJoin(ioList, InwardOutwardList_.PRODUCT);
query.multiselect(productList.get(Product_.PRODUCT_NAME), productList.get(Product_.MEASUREMENT_UNIT),
builder.sum(ioList.get(InwardOutwardList_.QUANTITY)));
query.groupBy(productList.get(Product_.PRODUCT_NAME), productList.get(Product_.MEASUREMENT_UNIT));
List<ProductGroupedDAO> groupedData = fetchData(query);
return groupedData;
}
<X, T> Join<X, T> getOrCreateJoin(From<?, X> from, SingularAttribute<X, T> attr) {
return from.getJoins().stream().filter(j -> j.getAttribute() == attr).findFirst().orElse(() -> from.join(attr));
}
<X, T> Join<X, T> getOrCreateJoin(From<?, X> from, PluralAttribute<X, ?, T> attr) {
return from.getJoins().stream().filter(j -> j.getAttribute() == attr).findFirst().orElse(() -> from.join(attr));
}

Spring boot JPA - Insert or update a list of entities

I have a repo with a unique constraint on 2 fields, connection_id and token_type:
#Entity
#Table(
name = "business_api_token",
schema = "public",
uniqueConstraints = {
#UniqueConstraint(
name = "business_api_token_unique_connection_id_and_token_type",
columnNames = {"connection_id", "token_type"}
)
}
)
public class BusinessApiToken {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne
#JoinColumn(
name = "connection_id",
nullable = false,
foreignKey = #ForeignKey(
name = "fk_business_api_token_connection_id"
)
)
private AccountingConnection connection;
#Column(name = "token_type")
#Enumerated(EnumType.STRING)
private ApiTokenType tokenType;
#Column(name = "token_value")
private String tokenValue;
...
}
I saw some posts saying add a custom query, something like this:
#Modifying
#Query("update User u set u.firstname = ?1, u.lastname = ?2 where u.id = ?3")
void setUserInfoById(String firstname, String lastname, Integer userId);
But how would I do this for a list? I was doing this:
businessApiTokenRepository.saveAll(tokens)
Which gives an error.
The tokens are created elsewhere without knowledge of existing ones, I can do another query to check first but that seems inefficient, and I have to do this all over.
Thanks

Spring JPA #JoinColumn(unique = true) not working

My entities are as follows
#Entity
#Table(
name = "t1",
uniqueConstraints = {
#UniqueConstraint(name = "u1", columnNames = {"u1"}),
#UniqueConstraint(name = "u2", columnNames = {"u2_id"}) //This does not enforce uniqueness
}
)
public class t1
{
#Id
#GeneratedValue
private Long id;
#Column(nullable = false)
private String u1; //Uniqueness is enforced here
#OneToOne
#JoinColumn(unique = true) //This does not enforce uniqueness
private U2 u2;
}
#Entity
#Table(
name = "u2",
uniqueConstraints = {
#UniqueConstraint(name = "p1", columnNames = {"p1"})
}
)
public class u2
{
#Id
#GeneratedValue
private Long id;
#Column(nullable = false)
private String p1; //Uniqueness is enforced here
}
Creating and updating the entities by
t1 t1_0 = new t1("t1_0");
t1 t1_1 = new t1("t1_1");
u2 u2_0 = new u2("u2_0");
repository.save(t1_0);
repository.save(t1_1);
repository.save(u2_0);
t1_0 = repository.findOne(t1_0.getId());
t1_1 = repository.findOne(t1_1.getId());
t1_0.setu2(u2_0);
t1_1.setu2(u2_0);
After the transaction finishes t1_0.u2 is tied to u2_0 and t1_1.u2 is tied to u2_0. I expected that it would throw uniqueness constraint violation exception.
Do not understand what is wrong. Other threads on SO suggest that #JoinColumn(unique = true) should do it.
EDIT
This is wrapped in one transaction
t1_0 = repository.findOne(t1_0.getId());
t1_1 = repository.findOne(t1_1.getId());
t1_0.setu2(u2_0);
t1_1.setu2(u2_0);
EDIT2
For some reason I can't access the database either. The console view loads but after adding password and user blank page is shown (some network queries fail).
But I can see from log that foreign keys are created and unique constraint aswell
Hibernate: alter table driver add constraint UK_ssh305wwvomjtn6opolug33nj unique (cardo_id)
Hibernate: alter table car add constraint FKosnia01vhqwmm888uxrg4o6f6 foreign key (manufacturerdo_id) references manufacturer
Hibernate: alter table driver add constraint FK3yb5ci9sr6ieo6n4wwwef3puv foreign key (cardo_id) references car
When adding
My transactions were invalid. As they do not see each others data, I mistakenly created one within another.
Columns names (if more than one) should be introduced with commas.
Your convention is wrong
Disclaimer: not checked on real database
#Table(
name = "t1",
uniqueConstraints = {
#UniqueConstraint(name = "u1", columnNames = {"u1"}),
#UniqueConstraint(name = "u2", columnNames = {"u2", "id"}) // comma
}
)

Hibernate OneToOne bidirectional foreign key on insert

I'm having a problem when calling insert using hibernate. The foreign key is not being passed down to the child on the OneToOne mappings, but it's working fine for the OneToMany mappings.
School.java
private long schoolId;
private set<Student> students;
private Principal principal;
#Id
#Column(name = "SCHOOL_ID", unique = true, nullable = false, precision = 15, scale = 0)
public long getSchoolId() {
return schoolId;
}
public void setSchoolId( long schoolId ) {
this.schoolId = schoolId;
}
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "school")
public set<Student> getStudents() {
return students;
}
public void setStudents( set<Student> students ) {
this.students = students;
}
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "school")
public Principal getPrincipal() {
return principal;
}
public void setPrincipal( Principal principal ) {
this.principal = principal;
}
Student.java
private long studentId;
private School school;
other data....
#Id
#Column(name = "STUDENT_ID", unique = true, nullable = false, precision = 15, scale = 0)
public long getStudentId() {
return studentId;
}
public void setStudentId( long studentId ) {
this.studentId = studentId;
}
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "SCHOOL_ID", nullable = false)
public School getSchool() {
return school;
}
public void setSchool( School school ) {
this.school = school;
}
Principal.java
private long principalId;
private School school;
//other data....
#Id
#Column(name = "PRINCIPAL_ID", unique = true, nullable = false, precision = 15, scale = 0)
public long getPrincipalId() {
return principalId;
}
public void setPrincipalId( long principalId ) {
this.principalId = principalId;
}
#JsonIgnore
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "SCHOOL_ID", nullable = false)
public School getSchool() {
return school;
}
public void setSchool( School school ) {
this.school = school;
}
With this example, when i try to save an School object, the hibernate would call insert on School, then Students, then Principal. When it calls insert on all the students, hibernate calls
insert into STUDENT ( SCHOOL_ID, STUDENT_ID ) values (?, ?)
which is correct. But when it tries to call insert on the principal, hibernate calls
insert into PRINCIPAL ( PRINCIPAL_ID) values (?)
which causes
ORA-01400: cannot insert NULL into ("PRINCIPAL"."SCHOOL_ID")
since the foreign key for the School object is not being inserted. I don't understand why it inserts the foreign key for the OneToMany tables, but not the OneToOne tables. Does anyone know how to fix this? these tables are bi-directional.
Also, I'm have a controller that takes in the School object and save to DB. The object looks like this
{
"name" : "data",
"students" : [ {"name", "data"}],
"principal" : {"name", "data"}
}
when I receive this school object, do I have to loop through the child and set the parent to school? because in this example it's only 2 levels, but I would need to build 4-5 levels, and would not like to loop all the way down to set each of the parents. I don't have to use bi-directional, if uni-directional work, I would do that.
Add mappedBy to the OneToOne annotation in Principal class:
#OneToOne(mappedBy="principal", fetch = FetchType.LAZY)

Resources