Many to one relationship without a join table in spring - spring-boot

I'm trying to build the relationship between two tables using spring-data jpa. I have read many SO articles like 1, 2 but they are pretty old and don't seem to apply to my specific use case. Hence this question:
There are 2 tables user_client_scopes and scopes listed below.
user_client_scopes:
user_id (long),
client_id (string)
last_updated (timestamp)
scope_id (Foreign key to scopes table),
primary key (user_id, client_id, scope_id)
scopes:
id (int, primary key)
name (string)
A <user_id, client_id> can have multiple scopes. Similarly, the same scope can be held by many <user_id, client_id>s. Hence the many-to-many relationship. The join table (as defined by spring-data-jpa) is kind of embedded within user_client_scope table.
Here is a half-written-code:
#Entity
#Table(name = "user_client_scopes")
#RequiredArgsConstructor
#IdClass(UserClientScopesPK.class)
public class UserClientScopes implements Serializable {
#Id
#Column(name = "user_id")
private long userId;
#Id
#Column(name = "client_id")
private String clientId;
#Column(name = "last_updated")
private Timestamp lastUpdated;
#Id
#Column(name = "scope_id")
private int scopeId;
#ManyToMany // <- how to complete this definition?
private Set<Scope> scopes;
getters and setters.
Here are 2 other classes (for the sake of completion).
#Data
#RequiredArgsConstructor
public class UserClientScopesPK implements Serializable {
private long userId;
private String clientId;
private int scopeId;
}
#Entity
#Table(name = "scopes")
#RequiredArgsConstructor
public class Scope implements Serializable {
#Id
#GeneratedValue
private long id;
private String name;
}
How do I complete the user_client_scopes entity such that we can:
Find all scopes for a given <user_id, client_id>. i.e. execute the following SQL:
select user_id, client_id, scope
from scopes
join user_client_scopes ucs on ucs.scope_id = scopes.id
where ucs.user_id = ? and ucs.client_id = ?
Save new scopes for a given <user_id, client_id>. i.e. execute the following SQL:
insert into user_client_scopes (user_id, client_id, scope_id, last_updated)
select ?, ?, id, now()
from scopes
where scopes.name = ?
UPDATE 1:
Changing title to Many to one instead of Many to many relationship.

That's not a many-to-many because the association scope is mapped by the column scope_id in user_client_scopes. This means that if I take a single row in the table user_client_scopes, it will be associated to only a single row in the table scopes. Therefore, this is a many-to-one.
If the three columns <user_id, client_id, scope_id> form the key for user_client_scopes, then the mapping for the table should look like:
Entity
#Table(name = "user_client_scopes")
#RequiredArgsConstructor
#IdClass(UserClientScopesPK.class)
public class UserClientScopes implements Serializable {
#Id
#Column(name = "user_id")
private long userId;
#Id
#Column(name = "client_id")
private String clientId;
#Column(name = "last_updated")
private Timestamp lastUpdated;
#Id
#ManyToOne
#JoinedColumn(name = "scope_id")
private Scope scope;
getters and setters.
}
class UserClientScopesPK implements Serializable {
private long userId;
private String clientId;
private Scope scope;
// getters,setters, equals and hascode
}
With this mapping you can run the following HQL:
select ucs
from UserClientScopes ucs join ucs.scope
where ucs.userId = :userId and ucs.clientId = :clientId
It will return all UserClientScopes entities matching the selected pair <userId, clientId>. Each one with a different scope.
Or, if you only care about the scope:
select s
from UserClientScopes ucs join ucs.scope s
where ucs.userId = :userId and ucs.clientId = :clientId
With Spring Data JPA, it will look like this:
#Query("select s from UserClientScopes ucs join ucs.scope swhere ucs.userId = ?1 and ucs.clientId = ?2")
public List<Scope> findScopesByUserIdAndClientId(long userId, String clientId);
or
#Query("select s.name from UserClientScopes ucs join ucs.scope swhere ucs.userId = ?1 and ucs.clientId = ?2")
public List<String> findScopesNameByUserIdAndClientId(long userId, String clientId);
You can also run the insert query as native SQL (you can probably run something similar as HQL, but I don't remember the right syntax now. I will update the answer later).
One last thing, to keep track of the last updated time, you could use Spring Entity callback listener:
#Entity
...
#EntityListeners(AuditingEntityListener.class)
public class UserClientScopes implements Serializable {
#LastModifiedDate
#Column(name = "last_updated")
private Date lastUpdated;
}

Related

Is that possible in spring boot that join column (foreign key) with id

I want to join column without object reference. is that possible?
I want to do foreign key without object reference like that
#Data
#Entity
#Table(name = "HRM_EMPLOYEE_SALARY_INCREMENT")
public class EmployeeSalaryIncrement implements Serializable {
private static final long serialVersionUID = 9132875688068247271L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="ID")
private Integer id;
#Column(name = "REFERENCE_NO")
private String referenceNo;
#ManyToOne
#JoinColumn(name = "AUTHORITY", referencedColumnName = "id")
private Integer authority;
#ManyToOne
#JoinColumn(name = "PART_TWO_REGISTER_ID")
private Integer partTwoRegisterId;
#Column(name = "PART_TWO_ORDER_NO")
private String partTwoOrderNo;
#Column(name = "REMARKS")
private String remarks;
#Column(name = "HRM_TYPE")
private Integer hrmType;
}
If I found solve this problem, it will helpful for me.
Joining is not needed in this case. If you only need the foreign key value, then simply add the column as a #Column like any other:
#Data
#Entity
#Table(name = "HRM_EMPLOYEE_SALARY_INCREMENT")
public class EmployeeSalaryIncrement implements Serializable {
private static final long serialVersionUID = 9132875688068247271L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="ID")
private Integer id;
#Column(name = "AUTHORITY")
private Integer authority;
// other fields
// ...
}
No, I don't think that you can join columns between two entities without adding the reference of one to the related entity. You will have to create one entity class corresponding to each of your relational database table and add the reference of one to the other to establish relation.
However, I understand that you may not need all the attributes from your related table based upon your use case, and only wish to select one column from it. You can do that either by only adding required attributes in your joined table entity class (if you are sure you won't need other attributes for that table anywhere else).
Or you can use custom queries using JPQL in your repository class which selects only the required attributes from the tables that you have joined.
I will show you an example of the second way:
//Say, this is your entity class where you wish to join other table to fetch only one attribute from the joined table-
#Entity
#Table(name = "TABLE1", schema = "SCHEMA1")
public class Table1 {
#Id
#Column(name = "ID")
private String id;
#Column(name = "TABLE2_COLUMN")
private String table2Column;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "TABLE2_COLUMN1")
private Table2 table2; //refrence of the joined table entity object
}
// And this is the joined table entity class
#Entity
#Table(name = "TABLE2", schema = "SCHEMA1")
public class Table2 {
#Id
#Column(name = "ID")
private String id;
#Column(name = "TABLE2_COLUMN1")
private String table2Column1;
#Column(name = "TABLE2_COLUMN2")
private String table2Column2; // The column which we want to select from the joined table
}
In your repository class -
#Repository
public interface Table1Repository extends JpaRepository<Table1, String> {
#Query("SELECT t1 FROM Table1 t1 WHERE t1.id = :id")
public List<Table1> getTable1Rows(#Param("id") String id);
#Query("SELECT t1.table2.table2Column2 FROM Table1 t1 WHERE t1.id = :id")
public String getTable2Column2(#Param("id") String id);
}
Based upon the response from Markus Pscheidt below, I agree when he said there's no need to join the entities if you only need the attribute which is a foreign key. As foreign key is already present as an attribute in your entity (or table) you are working with.
If you need to fetch any other column apart from foreign key, then you may use JPQL to fetch the exact column that you wish to select.

Why Value is not getting assigned in JPA for insert statement

Hi I have couple of Entity classes as below, using lombok for getter and setters
Parent Entity Class have
#Table(name = "PARTY")
#Entity
public class Party {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "PARTY_ID")
#MapKey(name="dvpParticipantName")
#LazyCollection(LazyCollectionOption.FALSE)
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
}
Child Entity Class have
#Table(name = "DV_LOAN_PARTICIPANTS")
#Entity
public class DVLoanParticipants implements Serializable {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
}
In service class i am calling save operation as
repository.save(parentEntityObject);
I am able to execute update statements ,but when i try to insert new row for child entity class i am getting an error saying
cannot insert NULL into ("ABC"."DV_LOAN_PARTICIPANTS"."PARTY_ID")
But if i print the parentEntityObject just before the save operation i see the values like
(partyId=12345678, dvpParticipantName=XYZ, dvpParticipantType=VKP)
I see the query formed as
insert
into
DV_LOAN_PARTICIPANTS
(DVP_PARTICIPANT_TYPE, PARTY_ID, DVP_PARTICIPANT_NAME)
values
(?, ?, ?)
Just before te save i am seeing valules in the Object
Builder=DVLoanParticipants(partyId=123456, dvpParticipantName=Builder,
dvpParticipantType=Individual)
Update
This is the setting part for values
DVLoanParticipants dvLoanParticipants = new
DVLoanParticipants();
dvLoanParticipants.setPartyId(Long.valueOf(partyId));
dvLoanParticipants.setDvpParticipantName("Builder");
dvLoanParticipants.setDvpParticipantType("Individual");
Party party = new Party();
Map<String, DVLoanParticipants> dvLoanParticipantsMap = new
java.util.HashMap<>();
dvLoanParticipantsMap.put("Builder", dvLoanParticipants);
party.setPartyId(Long.valueOf(partyId));
party.setDvLoanParticipantsMap(dvLoanParticipantsMap);
repository.save(party);
What is the mistake i am doing ?
The root cause of your problem in this part:
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "LOAN_ID")
#MapKey(name="dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
actually for your case the column name in the #JoinColumn means:
If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.
So, assuming for the clarity that you want to map the following schema:
create table PARTY
(
PARTY_ID int,
-- ...
primary key (PARTY_ID)
);
create table DV_LOAN_PARTICIPANTS
(
PARTY_ID int,
DVP_PARTICIPANT_NAME varchar(50),
DVP_PARTICIPANT_TYPE varchar(10),
-- ...
primary key (PARTY_ID, DVP_PARTICIPANT_NAME),
foreign key (PARTY_ID) references PARTY(PARTY_ID)
);
You can use the following mapping:
#Entity
#Table(name = "PARTY")
public class Party
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
// I use fetch = FetchType.EAGER instead of deprecated #LazyCollection(LazyCollectionOption.FALSE)
// targetEntity = DVLoanParticipants.class is redundant here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "PARTY_ID") // this is DV_LOAN_PARTICIPANTS.PARTY_ID column
#MapKey(name = "dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
public Party()
{
dvLoanParticipantsMap = new HashMap<>();
}
// getters / setters
public void addParticipant(DVLoanParticipants p)
{
this.dvLoanParticipantsMap.put(p.getDvpParticipantName(), p);
p.setPartyId(getPartyId());
}
}
#Entity
#Table(name = "DV_LOAN_PARTICIPANTS")
public class DVLoanParticipants implements Serializable
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
// getters / setters
}
and example how to save:
Party party = new Party();
party.setPartyId(2L);
// ...
DVLoanParticipants part1 = new DVLoanParticipants();
part1.setDvpParticipantName("Name 3");
part1.setDvpParticipantType("T1");
DVLoanParticipants part2 = new DVLoanParticipants();
part2.setDvpParticipantName("Name 4");
part2.setDvpParticipantType("T1");
party.addParticipant(part1);
party.addParticipant(part2);
repository.save(party);
and several notes:
The LazyCollectionOption.TRUE and LazyCollectionOption.FALSE values are deprecated since you should be using the JPA FetchType attribute of the #OneToMany association.
You use hibernate specific approach for mapping сomposite identifiers. As it's mentioned in the hibernate documentation:
The restriction that a composite identifier has to be represented by a primary key class (e.g. #EmbeddedId or #IdClass) is only JPA-specific.
Hibernate does allow composite identifiers to be defined without a primary key class via multiple #Id attributes.
But if you want to achieve more portability you should prefer one of the jpa allowed approaches.

How to retrieve only a specific field from child entity on #OneToOne relationship, not all fields?

When I use jpa's #OneToOne annotation, I want to get the userName field from the table, not all fields. What should I do instead?
#Setter
#Getter
#Entity
public class Menu implements Serializable {
private static final long serialVersionUID = 4462798713783196961L;
/**
* id
*/
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
#OneToOne
#JoinColumn(name = "createUserId",referencedColumnName = "userId")
private User createUser;
#Column(nullable = false)
private LocalDateTime createTime;
}
What do I need to do, can I get the userName field in the User object, but not all of it? Thank you in advance.
You can create a POJO with required fields. e.g. You only want id from Menu and userName from User:
public class CustomMenu {
private Long menuId;
private String userName;
public CustomMenu(Long menuId, String userName) {
this.menuId = menuId;
this.userName = userName;
}
// getters, setters
}
Then you can write a query with hql using the constructor in the CustomMenu with parameters new com.yourpackage.CustomMenu(m.id, m.createUser.userName) and join User entity (join m.createUser) :
TypedQuery<CustomMenu> query = entityManager.createQuery("select new com.yourpackage.CustomMenu(m.id, m.createUser.userName)"
+ "from com.yourpackage.Menu m join m.createUser", CustomMenu.class);
List<CustomMenu> menus = query.getResultList();
This generates one sql query with inner join fetching only required fields :
select menu0_.id as col_0_0_, user1_.user_name as col_1_0_ from menu menu0_ inner join user user1_ on menu0_.create_user_id=user1_.user_id

Two tables connected via Primary Key

I have read about the use of #MapsId and #PrimaryKeyJoinColumn annotations, which sounds like a great options. I have two tables (UserList and UserInformation) which have a child, parent relationship, respectively; both classes below are abbreviated to just include the relevant columns. UserInformation's primary key value is always null and does not take the value of its parent column.
User Class
#Entity
#Data
#Table(name = "user_list")
public class UserList {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// List of foreign keys connecting different entities
#OneToOne(mappedBy = "user")
#MapsId("id")
private UserInformation userInfo;
}
UserInformation Class
#Entity
#Data
#Table(name = "user_information")
public class UserInformation implements Serializable {
#Id
private Integer userId;
#OneToOne
private UserList user;
}
I would prefer to not use an intermediary class if possible. I'm not tied to MapsId or even this implementation if there is a better solution.
Thanks!
The question is not very clear to me, but I think you could improve the following in the modeling of the entity:
The #column annotation can only be omitted when the class parameter is called exactly the same as the database column, taking into account the table name nomenclature, could it be that the column is user_id ?, if so the id parameter should be :
#Id
#column(name="USER_ID")
private Integer userId;
In the user entity being id, it will match the DB ID field so the #column annotation is not necessary

EntityNotFoundException in Hibernate Many To One mapping however data exist

I'm getting an error
Caused by: javax.persistence.EntityNotFoundException: Unable to find tn.entities.AgenceBnq with id 01
when I get AgenceBnq through Employee
Employee class:
#Table(name = "EMPLOYEE")
#NamedQuery(name = "Employee.findById", query = "SELECT e FROM Employee e WHERE e.employeMat = ?1"),
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "EMPLOYEE_MAT", unique = true, nullable = false, length = 15)
private String employeeMat;
...
#ManyToOne
#JoinColumn(name = "AGENCE_COD")
private AgenceBnq agenceBnq;
}
#Entity
#Table(name="AGENCEBNQ")
public class AgenceBnq implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="AGENCE_COD", unique=true, nullable=false, length=10)
private String agenceCod;
...
//bi-directional many-to-one association to Employee
#OneToMany(mappedBy="agenceBnq")
private Set<Employee> employees;
}
I'm calling namedQuery Employee.findById in DAO to retrieve data and I have to get AgenceBnq from Employee but get this error while calling query.getResultList()
#NotFound( action = NotFoundAction.IGNORE) isn't useful for me because data exist in AGENCEBNQ table and I have to retrieve date through Employee.
Is this a bug in hibernate ? I'm using hibernate version 3.6.7.Final
Firstly, You dont need query for it, the EnityManger.find(Employee.class, YOUR_ID) will do the job.
Secondly dont use ? in your queries but names (e.employeMat = :id) as it is easier to debug and less error prones for complicated queries.
Finally, check your DB table if the AGENCE_COD column in Employee table really contains the valid ID for your entitity that crashes (and that it length matches the ID length of AgenceBnq). It should work, the typical reason why it doesnt will be that your Employe.AGENCE_COD has defualt value and when creatubg the new EMploye you add it only to the Agence but you did not set Agence in the Employ.

Resources