Need some light on #oneToMany using Map and #MapKeyColumn - java

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;

Related

How to create composite key objects with existing object using hibernate

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 }
}

How to use a foreign key within a composite primary key?

I'd like to create a composite primary key with hibernate. Usually I'd go for #IdClass.
But this time I want to use a foreign key also inside the composite primary key.
Question: is that possible at all?
Example:
#Entity
class Person {
long id;
}
class CarPK implements Serializable {
private int code;
private String name;
public CarPK(int code, String name) {
this.code = code;
this.name = name;
}
}
#Entity
#IdClass(CarPK.class)
class Car {
#Id
private int code;
#Id
private String name;
//can I also mark "person.id" with #Id?
#ManyToOne
#JoinColumn(name = "fk_person_id", foreignKey = #ForeignKey(name = "fk_person"))
private Person person; //assume car is shared
}
The person reference will show in database as fk_person_id. Is it possible to also add this column to the primary key of the car table?
So I'd be getting similar to: CONSTRAINT car_pkey PRIMARY KEY (code, name, fk_person_id)?
Update:
#ManyToOne
#JoinColumn(name = "id")
private Person person;
Results in: Property of #IdClass not found in entity path.to$Car: id
Yes, you can add the #Id to the join column, but you must use the key type in your IdClass. I'm doing exactly the same thing in my current project.
#Entity
#IdClass(MyIdClass.class)
public class MyObject {
#Id
private String key;
#Column
#Lob
private String value;
#ManyToOne(cascade = CascadeType.PERSIST)
#Id
#JoinColumn(name = "id")
private MyOtherObject otherObject;
...
and
public class MyIdClass implements Serializable {
private long otherObject;
private String key;
...
MyOtherObject.id is a long in this scenario.

Spring JPA Repository query filter by a relationship table

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.

How to implement a complex many to many relationship in JPA?

Here the db schema
CREATE TABLE Products
(
id INT NOT NULL AUTO_INCREMENT,
category_id INT NOT NULL,
description VARCHAR(100),
price DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (category_id) REFERENCES Categories(id)
) ENGINE = INNODB;
CREATE TABLE Orders
(
id INT NOT NULL AUTO_INCREMENT,
customer_id INT NOT NULL,
status VARCHAR(20) NOT NULL,
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES Customers(id)
) ENGINE = INNODB;
CREATE TABLE OrderDetails
(
product_id INT NOT NULL,
order_id INT NOT NULL,
quantity INT NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (product_id, order_id),
FOREIGN KEY (product_id) REFERENCES Products(id),
FOREIGN KEY (order_id) REFERENCES Orders(id)
) ENGINE = INNODB;
The models
#Embeddable
public class OrderDetailPK
{
private Product product;
private Order order;
public OrderDetailPK() {}
public OrderDetailPK(Product product, Order order)
{
this.product = product;
this.order = order;
}
}
public class OrderDetail {
#EmbeddedId
private OrderDetailPK id;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="product_id", insertable=false, updatable=false)
private Product product;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="order_id", insertable=false, updatable=false)
private Order order;
private int quantity;
private double subtotal;
public OrderDetail() {}
public OrderDetail(OrderDetailPK id, int quantity, double subtotal)
{
this.product = id.getProduct();
this.order = id.getOrder();
this.quantity = quantity;
this.subtotal = subtotal;
}
// getters, setters
}
public class Product {
#Id
private int id;
private String description;
private double price;
#ManyToOne
#JoinColumn(name="category_id")
private Category category;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "Products")
private List<OrderDetail> orderDetail;
}
public class Order {
#Id
private int id;
#ManyToOne
#JoinColumn(name="customer_id")
private Customer customer;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "Orders")
private List<OrderDetail> orderDetail;
}
And for some reasons I keep getting the error
Concrete type "class models.OrderDetail" with application identity does not declare any primary key fields.
Could anyone point me out where the problem is ? Thanks
When i did this before (as detailed in this question and answer), i made the fields in the embeddable ID primitives (corresponding to the ID fields of the entities referred to), and then used #MapsId in the entity. I believe this is the simplest (and dare i say correct) of meeting all the requirements: that the fields in the entity are relationships, that the fields in the ID class are primitive, that every column is mapped exactly once (the #MapsId fields not really being mappings, but sort of aliases).
Applying that to your case, the ID class looks like:
#Embeddable
public class OrderDetailPK {
private final int productId;
private final int orderId;
public OrderDetailPK(int productId, int orderId) {
this.productId = productId;
this.orderId = orderId;
}
}
And the entity class looks like:
public class OrderDetail {
#EmbeddedId
private OrderDetailPK id;
#ManyToOne(cascade = CascadeType.ALL)
#MapsId("productId")
private Product product;
#ManyToOne(cascade = CascadeType.ALL)
#MapsId("orderId")
private Order order;
private int quantity;
private double subtotal;
public OrderDetail(Product product, Order order, int quantity, double subtotal) {
this.id = new OrderDetailPK(product.getId(), order.getId());
this.product = product;
this.order = order;
this.quantity = quantity;
this.subtotal = subtotal;
}
protected OrderDetail() {}
}
First of all OrderDetailPK has to implement Serializable.
For second please specify which ID's you are going to use, because you has specified columns product_id and order_id as insertable=false, updatable=false (read-only).
So you need to try something like the following:
#EmbeddedId
#AttributeOverrides({
#AttributeOverride(name = "product_id",column = #Column(name = "product_id")),
#AttributeOverride(name = "listingId",column= #Column(name = "order_id"))
})
private OrderDetailPK id;
More information you may find here:
http://docs.oracle.com/javaee/6/api/javax/persistence/EmbeddedId.html
http://docs.oracle.com/javaee/6/api/javax/persistence/AttributeOverride.html
From the EmbeddedId javadoc:
Relationship mappings defined within an embedded id class are not supported.
So you cannot do it this way. I don't think JPA 1 specifies a standard way to implement this (in JPA 2 there is #MapsId but I never tried), but this is what I usually do and most implementations (I think at least Hibernate, EclipseLink and OpenJPA) support it:
Declare your primary key class using primitive types:
#Embeddable
public class OrderDetailPK implements Serializable
{
private int product;
private int order;
public OrderDetailPK() {}
...
}
Annotate your entity with #IdClass and declare the fields using the same name but the desired types:
#Entity
#IdClass(OrderDetailPK.class)
public class OrderDetail {
#Id
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="product_id", insertable=false, updatable=false)
private Product product;
#Id
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="order_id", insertable=false, updatable=false)
private Order order;
...
}
(I have always kept the #Id on the fields in the entity but I didn't recheck if they are mandatory)

JPA ManyToMany Join Table has all attributes as PK

I'm using Hibernate 3.3.1 and am following along in modelling this sample table structure, but I'm having trouble creating a join table with extra attributes.
It's the many-to-many relationship between the Order and Product table. The join table is the Order Detail table. I followed the approach mentioned here.
Now I have the entities
#Entity
#Table(name = "Orders")
public class Order {
#OneToMany(mappedBy="order")
private List<OrderDetail> orderItems;
}
and
#Entity
#Table(name="PRODUCTS")
public class Product {
#OneToMany(mappedBy="product")
private List<OrderDetail> orderItems;
}
and
#Entity
#IdClass(OrderDetail.class)
#Table(name = "ORDER_DETAIL")
public class OrderDetail implements Serializable {
#Id
#Column(name="ORDER_ID")
private Long orderId;
#Id
#Column(name="PRODUCT_ID")
private Long productId;
#Column(name = "PRICE")
private double price;
#Column(name = "LAST_UPDATED_TIME")
private Date lastUpdatedTime;
#ManyToOne
#JoinColumn(name = "ORDER_ID")
private Order order;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID")
private Product product;
}
and
public class OrderDetailId implements Serializable {
private Long orderId;
private Long productId;
}
I used Apache Derby to do the test, but I'm having trouble with the generated table structure.
CREATE TABLE ORDER_DETAIL (
PRODUCT_ID BIGINT NOT NULL,
ORDER_ID BIGINT NOT NULL,
LAST_UPDATED_TIME TIMESTAMP NOT NULL,
PRICE DOUBLE NOT NULL
);
CREATE INDEX SQL120323142938020 ON ORDER_DETAIL (PRODUCT_ID ASC);
CREATE UNIQUE INDEX SQL120323142937810 ON ORDER_DETAIL (PRODUCT_ID ASC, ORDER_ID ASC, LAST_UPDATED_TIME ASC, PRICE ASC);
ALTER TABLE ORDER_DETAIL ADD CONSTRAINT SQL120323142937810 PRIMARY KEY (PRODUCT_ID, ORDER_ID, LAST_UPDATED_TIME, PRICE);
ALTER TABLE ORDER_DETAIL ADD CONSTRAINT FK4A94AA82CC6D989A FOREIGN KEY (PRODUCT_ID)
REFERENCES PRODUCTS (PROD_ID);
It seems that it has created all of my columns as the primary key. Why is this so?
You use class of your entity as an argument to IdClass. That is not correct. Class of Id should be used. Additionally separate fields for id in join entity are not needed.
Go for something like code below. I cannot guarantee that it works in such a old version of Hibernate, but works for sure in never ones. Worth of trying anyway. It would not hurt to update to at least 3.5.X version (or rather even fresher one) if you want to use JPA 2.0 features. Constructors/equals etc. are stripped away to save space.
#Entity
#Table(name = "Orders")
public class Order {
#Id Long id;
#OneToMany(mappedBy="order")
private List<OrderDetail> orderItems;
}
#Entity
#Table(name="PRODUCTS")
public class Product {
#Id Long id;
#OneToMany(mappedBy="product")
private List<OrderDetail> orderItems;
}
#Entity
#IdClass(OrderDetailId.class)
#Table(name = "ORDER_DETAIL")
public class OrderDetail implements Serializable {
#Id #ManyToOne #JoinColumn(name = "ORDER_ID")
private Order order;
#Id #ManyToOne #JoinColumn(name = "PRODUCT_ID")
private Product product;
#Column(name = "PRICE") private double price;
//Maybe you also want to use #TemporalType here
#Column(name = "LAST_UPDATED_TIME") private Date lastUpdatedTime;
}
public class OrderDetailId implements Serializable {
private Long order;
private Long product;
}
UPDATE 15/08/2017
In JPA 2.1 and above you don't need to add a class for the composite Id and you can do it like this:
#Entity
#Table(name = "ORDER_DETAIL")
public class OrderDetail implements Serializable {
#Id #ManyToOne #JoinColumn(name = "ORDER_ID")
private Order order;
#Id #ManyToOne #JoinColumn(name = "PRODUCT_ID")
private Product product;
#Column(name = "PRICE") private double price;
//Maybe you also want to use #TemporalType here
#Column(name = "LAST_UPDATED_TIME") private Date lastUpdatedTime;
}
The code below seems to generate tables as desired, I have tested it on MySQL (just the table creation, not CRUD):
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "orderDetailId.order")
private List<OrderDetail> orderItems;
//get set …..
}
#Entity
#Table(name="products")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "orderDetailId.product")
private List<OrderDetail> orderItems;
//get set ……
}
#Entity
#Table(name = "order_detail")
public class OrderDetail {
#Id
private OrderDetailId orderDetailId;
private double price;
#Temporal(TemporalType.TIMESTAMP)
private Date lastUpdatedTime;
//get set ….
}
#Embeddable
public class OrderDetailId implements Serializable{
private Order order;
private Product product;
#ManyToOne(fetch=FetchType.LAZY)
#Access(AccessType.PROPERTY)
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
#ManyToOne(fetch=FetchType.LAZY)
#Access(AccessType.PROPERTY)
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
//hash code equals override
}
Hibernate DEBUG details as below
DEBUG: org.hibernate.tool.hbm2ddl.SchemaUpdate - create table order_detail (lastUpdatedTime datetime, price double precision not null, product_id bigint, order_id bigint, primary key (order_id, product_id)) ENGINE=InnoDB
DEBUG: org.hibernate.tool.hbm2ddl.SchemaUpdate - create table orders (id bigint not null auto_increment, primary key (id)) ENGINE=InnoDB
DEBUG: org.hibernate.tool.hbm2ddl.SchemaUpdate - create table products (id bigint not null auto_increment, primary key (id)) ENGINE=InnoDB
DEBUG: org.hibernate.tool.hbm2ddl.SchemaUpdate - alter table order_detail add index FK23AE5A622128CF91 (order_id), add constraint FK23AE5A622128CF91 foreign key (order_id) references orders (id)
DEBUG: org.hibernate.tool.hbm2ddl.SchemaUpdate - alter table order_detail add index FK23AE5A62EB201631 (product_id), add constraint FK23AE5A62EB201631 foreign key (product_id) references products (id)

Categories