I have these tables:
CREATE TABLE company (
id VARCHAR(256) NOT NULL,
tenantId VARCHAR(256) NOT NULL,
fieldName VARCHAR(256) NOT NULL,
PRIMARY KEY (id, tenantId, fieldName)
);
CREATE TABLE employee (
tenantId VARCHAR(256) NOT NULL,
companyFieldName VARCHAR(256) NOT NULL,
companyId VARCHAR(256) NOT NULL,
fieldName VARCHAR(256) NOT NULL,
PRIMARY KEY (tenantId, companyFieldName, companyId, fieldName),
CONSTRAINT fkCompany FOREIGN KEY (tenantId, companyFieldName, companyId) REFERENCES employee (tenantId, fieldName, id)
);
One company can have many employees
A company's primary key is a composite key consisting of 3 fields
An employees' primary key consists of the company's composite key (i.e the foreign key), plus another field specific to company.
Based on this related question:
JPA how to make composite Foreign Key part of composite Primary Key
I have created the following entities:
#Embeddable
public class CompanyIdentity implements Serializable
{
#NotBlank
private String tenantId;
#NotBlank
private String fieldName;
#NotBlank
private String id;
//getters, setters, equals and hashcode ommited
}
#Entity
public class Company implements Serializable
{
#EmbeddedId
private CompanyIdentity companyIdentity;
#OneToMany(mappedBy = "company")
private Set<Employee> employees;
//getters, setters, equals and hashcode ommited
}
#Embeddable
public class EmployeeIdentity implements Serializable
{
#NotNull
private CompanyIdentity companyIdentity;
// This *not* the fieldName in the CompanyIdentity, this is a property
// specific to an employee, it just happens to have the same name
#NotBlank
private String fieldName;
//getters, setters, equals and hashcode ommited
}
public class Employee implements Serializable
{
#EmbeddedId
private EmployeeIdentity employeeIdentity;
#MapsId("companyIdentity")
#JoinColumns({
#JoinColumn(name = "tenantId", referencedColumnName = "tenantId"),
#JoinColumn(name = "companyFieldName", referencedColumnName = "fieldName"),
#JoinColumn(name = "companyId", referencedColumnName = "id")
})
#ManyToOne
#Cascade(value={org.hibernate.annotations.CascadeType.ALL})
private Company company;
//getters, setters, equals and hashcode ommited
}
I want to save a company with a single employee, and have it write a
row to the Company table and Employee table, but whenever I run the
following, I only ever see a row getting written to the Company table
and never the Employee table?
I'm not sure if below is the right approach or not, or maybe the entities
above are not correct?
public interface CompanyRepository extends CrudRepository<Company, String> {}
final Company company = new Company();
final CompanyIdentity companyIdentity = new CompanyIdentity("company-tenant-id", "company-field-name", "company-id");
company.setCompanyIdentity(companyIdentity);
final Employee employee = new Employee();
final EmployeeIdentity employeeIdentity = new EmployeeIdentity();
employeeIdentity.setFieldName("employee-field-name");
employeeIdentity.setCompanyIdentity(companyIdentity);
employee.setEmployeeIdentity(employeeIdentity);
employee.setCompany(company);
final Set<Employee> employees = new HashSet<>();
employees.add(employee);
company.setEmployees(employees);
companyRepository.save(company); //only saves company, not employee?
Many thanks!
Your are saving the company with the company repository but the company doens´t have a cascade annotation.
#OneToMany(mappedBy = "company")
private Set<Employee> employees;
Should be:
#OneToMany(mappedBy = "company")
#Cascade(value={org.hibernate.annotations.CascadeType.ALL})
private Set<Employee> employees;
Related
Is it possible to define a composite primary key in my Entity, but 2 IDs are #OneToOne fields? I'm trying to do it with #PrimaryKeyJoinColumn annotation, unsuccessfully
public class EmployeeEntity {
// my composite ID must be person_id + department_id
#OneToOne
//#PrimaryKeyJoinColumn(name = "person_id")
private PersonEntity person;
#OneToOne
//#PrimaryKeyJoinColumn(name = "department_id")
private DepartmentEntity department;
// other fields
I definitely can do it in a classic way with two numeric fields, but I want to have a OneToOne relationship to be set.
Actually, DB schema should be like that:
create table EMPLOYEE
(
PERSON_ID INT not null,
DEPARTMENT_ID INT not null,
constraint EMPLOYEE_PK
primary key (PERSON_ID, DEPARTMENT_ID),
constraint EMPLOYEE_PERSON_ID_FK
foreign key (PERSON_ID) references PERSON (ID),
);
I believe an embeddable composite key is what you need:
#Entity
public class EmployeeEntity extends BaseEntity {
#Embeddable
static class Pk implements Serializable {
#OneToOne
#JoinColumn(name = "PERSON_ID")
private PersonEntity person;
#OneToOne
#JoinColumn(name = "DEPARTMENT_ID")
private DepartmentEntity department;
}
#Id
private final Pk id;
public EmployeeEntity(DepartmentEntity department, PersonEntity person) {
this.id = new Pk();
this.id.person = person;
this.id.department = department;
}
}
You should create a new class which contains both fields, that class will become your composite key for your EmployeeEntity.
#Embeddable
public class EmployeeId implements Serializable {
private PersonEntity person;
private DepartmentEntity department;
public EmployeeId() {}
public EmployeeId(PersonEntity person, DepartmentEntity department) {
this.person = person;
this.department = department;
}
// equals() and hashCode() methods should also be implemented here
}
public class EmployeeEntity implements Serializable {
#EmbeddedId
private EmployeeId id;
}
Lets say I have the following database schema
CREATE TABLE employee(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
hrid VARCHAR (50)
);
CREATE TABLE territory(
id BIGINT PRIMARY KEY,
name varchar (50)
);
CREATE TABLE transcode(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR (10) NOT NULL
);
create table employee_territory_function(
employee_id BIGINT NOT NULL,
territory_id BIGINT NOT NULL,
transcode_id BIGINT NOT NULL,
PRIMARY KEY (employee_id,territory_id),
CONSTRAINT employeeref FOREIGN KEY (employee_id) REFERENCES employee (id),
CONSTRAINT territoryref FOREIGN KEY (territory_id) REFERENCES territory (id) ,
CONSTRAINT transcoderef FOREIGN KEY (transcode_id) REFERENCES transcode (id)
);
Now I have the following JPA mapped entities
Employee entity
#Entity
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String hrid;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "employee", cascade = CascadeType.ALL)
private Set<EmployeeTerritoryFunction> employeeTerritoryFunctionList = new HashSet<>();
//getters and setters
}
Territory entity:
#Entity
public class Territory implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
// getters and setters for all field
}
Transcode Entity:
#Entity
public class Territory implements Serializable {
#Id
private long id;
private String name;
//getters and setters
}
EmployeeTerritoryFunction entity (composite key table)
#Entity
#IdClass(value = EmployeeTerritoryFunctionPK.class)
public class EmployeeTerritoryFunction implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#ManyToOne
private Employee employee;
#Id
#ManyToOne
private Territory territory;
#ManyToOne
#JoinColumn(name = "transcode_id", referencedColumnName = "id")
private Transcode transcode;
//getters and setters
}
EmployeeTerritoryFunction pk
public class EmployeeTerritoryFunctionPK implements Serializable {
private static final long serialVersionUID = 1L;
private Long employee;
private Long territory;
//getters and setters, no args constructor, equals and hashcode
}
Below sample insertion
Employee employee = this.employeeRepository.findByHrid("111");
if (employee == null) {
employee = new Employee();
employee.setName("Marie");
employee.setHrid("333");
}
Territory territory = new Territory();
territory.setId(2L);
territory.setName("T2");
Territory territory2 = new Territory();
territory2.setId(3L);
territory2.setName("T3");
Transcode transcode = this.transcodeRepository.findByCode("ASC");
Transcode transcode2 = this.transcodeRepository.findByCode("CC");
EmployeeTerritoryFunction employeeTerritoryFunction1 = new EmployeeTerritoryFunction();
employeeTerritoryFunction1.setTranscode(transcode);
employeeTerritoryFunction1.setTerritory(territory);
employeeTerritoryFunction1.setEmployee(employee);
employee.getEmployeeTerritoryFunctionList().add(employeeTerritoryFunction1);
EmployeeTerritoryFunction employeeTerritoryFunction2 = new EmployeeTerritoryFunction();
employeeTerritoryFunction2.setTranscode(transcode2);
employeeTerritoryFunction2.setTerritory(territory2);
employeeTerritoryFunction2.setEmployee(employee);
employee.getEmployeeTerritoryFunctionList().add(employeeTerritoryFunction2);
employeeRepository.save(employee);
when I run above code with only new objects, I have no issue because hibernate automatically insert the employee, the territory and the list of employee_territory_function but when I first delete all existing territory, employee_territory_function and try to insert using an existing employee, hibernate is not able auto insert or update employee, auto insert in territory, employee_territory_function.
Below the error
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.demo.Employee.employeeTerritoryFunctionList, could not initialize proxy - no Session
When I replace oneToMany fetch type to EAGER, I got below error
Caused by: javax.persistence.EntityNotFoundException: Unable to find com.example.demo.Territory with id 3
It seams that hibernate try to query Territory table but I do not when him to do that because I remove all data on Territory and EmployeeTerritoryFunction table and only employee existing data is not removed.
How to fixe please ?
Fields in both classes EmployeeTerritoryFunction and EmployeeTerritoryFunctionPK should be named exactly same and have same types which you don't have. Try like this:
#Entity
#IdClass(EmployeeTerritoryFunctionPK.class)
public class EmployeeTerritoryFunction implements Serializable {
#Id
#ManyToOne
private Employee employee;
#Id
#ManyToOne
private Territory territory;
}
public class EmployeeTerritoryFunctionPK implements Serializable {
private Employee employee;
private Territory territory;
public int hashCode() { //TODO }
public boolean equals(Object obj) { //TODO }
}
I want to persist an #Entity having a #ManyToOne #JoinColumn reference, which should be optional.
But when I try to persist the following Person class where locationId is set, but address rerference is not set, I'm getting a PSQLException: voilates foreign key constraintsexception.
My class looks as follows:
#Entity
public class Person {
#Id
private Long id;
private String name;
private int locationId;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DETACH})
#JoinColumn(name = "location_id", foreignKey = #ForeignKey(name="fk_address"), nullable = true)
private AddressEntity address;
}
#Entity
public class AddressEnity {
#Id
private int locationId;
//street, zip, town etc
#OneToMany(mappedBy = "address")
private Set<Person> persons;
}
The schema generated by hibernate:
CREATE TABLE person(
id bigint NOT NULL,
name character varying(255),
location_id interger NOT NULL,
CONSTRAINT fk_address FOREIGN KEY (location_id)
REFERENCES addresses (location_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE TABLE address(
location_id integer NOT NULL,
//street, zip, town, etc
CONSTRAINT location_id_pk PRIMARY KEY (location_id)
);
Question: isn't is possible to save the locationId explicit, but omitting the address entity? (which might come in later, but not known at this stage).
If I have a many-to-many relationship between JPA entities as below, how can I retrieve a list of Person (I am interested in the person attributes) that are employees of a specific company?
The relationship between Person and Company is many-to-many. The relationship table Employee has the FK to Person and Company, and a start_date and end_date to indicate when the employment started and finished.
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
}
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
}
#Entity
public class CompanyEmployee {
//note this is to model a relationship table. Am I doing this wrong?
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "start_date", nullable = false)
private LocalDate startDate;
#Column(name = "end_date", nullable = false)
private LocalDate endDate;
#ManyToOne
private Company company;
#ManyToOne
private Person person;
}
Do I use a #Query on the CompanyEmployeeJPARepository? How should I tackle it?
public interface CompanyEmployeeRepository extends JpaRepository<CompanyEmployee,Long> {
//
}
Pablo,
Our company is in the process of converting our existing Spring / MyBatis code to Spring Data JPA, so I have been learning Spring Data JPA for a few weeks. I'm clearly not an expert, but I worked out an example similar to yours which may help you.
I have Person and Company classes that are similar to yours, but (as Jens mentioned), you need lists with OneToMany annotations. I used a separate join table (named company_person) which only has companyId, personId columns to maintain the many-to-many relationship. See the code below.
I did not see a way to put the start/end dates in the company_person join table, so I made a separate (4th table) for that. I called it employment_record with Java class entity EmploymentRecord. It has the combo primary key (companyId, personId) and the start/end dates.
You need repositories for Person, Company, and EmploymentRecord. I extended CrudRepository instead of JpaRepository. But, you don't need an entity or repository for the join table (company_record).
I made a Spring Boot Application class to test it out. I used CascadeType.ALL on Person's OneToMany. In my Application test, I tested that I can change the companies assigned to a person and Spring Data propagates all the changes needed to the Company entities and join table.
However, I had to manually update the EmploymentRecord entities, via its repository. For example, I had to add a start_date each time I added a company to a person. Then, add an end_date when I removed that company from that person. There is probably some way to automate this. The Spring / JPA audit feature is a possibility, so check that out.
The answer to your question:
how can I retrieve a list of Person (I am interested in the person
attributes) that are employees of a specific company?
You simply use companyRepository's findOne(Long id) method followed by getPersonList() method.
snippet from Application.java:
PersonRepository pRep = context.getBean(PersonRepository.class);
CompanyRepository cRep = context.getBean(CompanyRepository.class);
EmploymentRecordRepository emplRep = context.getBean(EmploymentRecordRepository.class);
...
// fetch a Company by Id and get its list of employees
Company comp = cRep.findOne(5L);
System.out.println("Found a company using findOne(5L), company= " + comp.getName());
System.out.println("People who work at " + comp.getName());
for (Person p : comp.getPersonList()) {
System.out.println(p);
}
Here are some references that I found to be useful:
Spring Data JPA tutorial
Join Table example
Person.java:
#Entity
public class Person {
// no-arg constructor
Person() { }
// normal use constructor
public Person(String name, String address) {
this.name = name;
this.address = address;
}
#Id
#GeneratedValue
private Long id;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
#Version
private int versionId;
#OneToMany(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name="company_person",
joinColumns={#JoinColumn(name="person_id", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="company_id", referencedColumnName="id")})
private List<Company> companyList;
// Getters / setters
}
Company.java:
#Entity
public class Company {
// no-arg constructor
Company() { }
// normal use constructor
public Company(String name, String address) {
this.name = name;
this.address = address;
}
#Id
#GeneratedValue
private Long id;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
#Version
private int versionId;
//#OneToMany(cascade=CascadeType.ALL)
#OneToMany(fetch = FetchType.EAGER)
#JoinTable(name="company_person",
joinColumns={#JoinColumn(name="company_id", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="person_id", referencedColumnName="id")})
private List<Person> personList;
// Getters / Setters
}
EmploymentRecord.java:
#Entity
#IdClass(EmploymentRecordKey.class)
public class EmploymentRecord {
// no-arg constructor
EmploymentRecord() { }
// normal use constructor
public EmploymentRecord(Long personId, Long companyId, Date startDate, Date endDate) {
this.startDate = startDate;
this.endDate = endDate;
this.companyId = companyId;
this.personId = personId;
}
// composite key
#Id
#Column(name = "company_id", nullable = false)
private Long companyId;
#Id
#Column(name = "person_id", nullable = false)
private Long personId;
#Column(name = "start_date")
private Date startDate;
#Column(name = "end_date")
private Date endDate;
#Version
private int versionId;
#Override
public String toString() {
return
" companyId=" + companyId +
" personId=" + personId +
" startDate=" + startDate +
" endDate=" + endDate +
" versionId=" + versionId;
}
// Getters/Setters
}
// Class to wrap the composite key
class EmploymentRecordKey implements Serializable {
private long companyId;
private long personId;
// no arg constructor
EmploymentRecordKey() { }
#Override
public int hashCode() {
return (int) ((int) companyId + personId);
}
#Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (!(obj instanceof EmploymentRecordKey)) return false;
EmploymentRecordKey pk = (EmploymentRecordKey) obj;
return pk.companyId == companyId && pk.personId == personId;
}
// Getters/Setters
}
MySql script, createTables.sql:
DROP TABLE IF EXISTS `test`.`company_person`;
DROP TABLE IF EXISTS `test`.`employment_record`;
DROP TABLE IF EXISTS `test`.`company`;
DROP TABLE IF EXISTS `test`.`person`;
CREATE TABLE `company` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL DEFAULT '',
`address` varchar(500) DEFAULT '',
`version_id` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `person` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL DEFAULT '',
`address` varchar(500) DEFAULT '',
`version_id` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/* Join table */
CREATE TABLE `company_person` (
`company_id` int NOT NULL,
`person_id` int NOT NULL,
PRIMARY KEY (`person_id`,`company_id`),
KEY `company_idx` (`company_id`),
KEY `person_idx` (`person_id`),
CONSTRAINT `fk_person` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_company` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/* Employment records */
CREATE TABLE `employment_record` (
`company_id` int NOT NULL,
`person_id` int NOT NULL,
`start_date` datetime,
`end_date` datetime,
`version_id` int NOT NULL,
PRIMARY KEY (`person_id`,`company_id`),
KEY `empl_company_idx` (`company_id`),
KEY `empl_person_idx` (`person_id`),
CONSTRAINT `fk_empl_person` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_empl_company` FOREIGN KEY (`company_id`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
I have previous experience in hibernate JPA but not spring JPA. From that knowledge following query might be useful:
select cp.person from CompanyEmployee cp where cp.company.id = ?
You shouldn't need to make a separate entity for the relationship table.
The relationship can be maintained within the two entities,
so if A and B are in a many-to-many relationship,
#Entity
class A {
#Id
Long id;
...
#ManyToMany(fetch=FetchType.LAZY)
#JoinTable(name="a_b",
joinColumns={#JoinColumn(name="id_a", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="id_b", referencedColumnName="id")})
List<B> bList;
...
}
#Entity
class B {
#Id
Long id;
...
#ManyToMany(fetch=FetchType.LAZY)
#JoinTable(name="a_b",
joinColumns={#JoinColumn(name="id_b", referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="id_a", referencedColumnName="id")})
List<A> aList;
...
}
You can now use the repository queries on either of the entity repositories or if you have a query with params on both, you can create a custom query in the repository of one.
This is my sql table structure,
create table TBL_DEPARTMENT_ONE(
ID integer primary key generated always as identity (start with 50, increment by 1),
name varchar(100)
)
create table TBL_EMPLOYEE_THREE(
ID integer primary key generated always as identity (start with 100, increment by 1),
name varchar(100),
dept_ID integer references TBL_DEPARTMENT_ONE
)
Here we i'v done a structure of one to many relation between Employee and Department where many employees can belong to one Department,
Now, here is the JPA mapping code as follows,
For Employee,
#Entity
#Table(name="TBL_EMPLOYEE_THREE")
public class EmployeeEntityThree implements Serializable{
public EmployeeEntityThree(){}
public EmployeeEntityThree(String name,String mobileNo,DepartmentEntityOne dept){
this.empName = name;
this.department = dept;
this.mobileNo = mobileNo;
}
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="ID")
private Integer employeeId;
#Column(name="MOBILE_NO")
private String mobileNo;
#ManyToOne(cascade={CascadeType.PERSIST,
CascadeType.MERGE},
fetch= FetchType.LAZY,targetEntity=DepartmentEntityOne.class)
#JoinColumn(name="DEPT_ID")
private DepartmentEntityOne department;
.....
...
}
the code below is of Department Entity,
#Entity
#Table(name="TBL_DEPARTMENT_ONE")
public class DepartmentEntityOne implements Serializable{
public DepartmentEntityOne(){ }
public DepartmentEntityOne(String name){
this.deptName = name;
}
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="ID")
private Integer deptId;
#Column(name="NAME")
private String deptName;
#OneToMany(cascade= { CascadeType.MERGE,
CascadeType.PERSIST},
fetch= FetchType.LAZY,mappedBy="department")
#MapKeyColumn(name="xxxxx")
private Map<String,EmployeeEntityThree> employees;
...
..
}
This is the code in my main method for testing,
DepartmentEntityOne deptOne = new DepartmentEntityOne("Mechanical Engineering");
Map<String,EmployeeEntityThree> empMap = new HashMap<String,EmployeeEntityThree>();
EmployeeEntityThree[] array = new EmployeeEntityThree[]{
new EmployeeEntityThree("Amar","9000000001",deptOne),
new EmployeeEntityThree("Akbar","9000000002",deptOne),
new EmployeeEntityThree("Anthony","9000000003",deptOne)
};
empMap.put(array[0].getMobileNo(),array[0]);
empMap.put(array[1].getMobileNo(),array[1]);
empMap.put(array[2].getMobileNo(),array[2]);
deptOne.setEmployees(empMap);
em = emf.createEntityManager();
em.persist(deptOne);
The code works fine with all the inserts done successfully
Now my Question is for the Entity Department
where is have used an #MapKeyColumn(name="xxxx"), where "xxxx" is some garbage value,
Here what should be the name = ?
because prior to this it was name = "mobileNo" which is the property in Employee entity.
This worked too.
So what shoud be the actualy vaue for #MapKetColumn(name= ?)
You should refer to the name column of the Employee table.
#MapKeyColumn(name="NAME")
private Map<String,EmployeeEntityThree> employees;