I have a problem very similar to this: How do I join tables on non-primary key columns in secondary tables?
But I'm not sure if I can apply the same solution.
I have two tables like these:
CREATE TABLE CUSTOMER
(
CUSTOMER_ID INTEGER NOT NULL,
DETAIL_ID INTEGER NOT NULL,
PRIMARY KEY( CUSTOMER_ID ),
CONSTRAINT cust_fk FOREIGN KEY( DETAIL_ID ) REFERENCES DETAILS( DETAIL_ID )
)
CREATE TABLE DETAILS
(
DETAIL_ID INTEGER NOT NULL,
OTHER INTEGER NOT NULL,
PRIMARY KEY( DETAIL_ID )
)
I'd like to map these tables to a single class called Customer, so I have:
#Entity
#Table(name = "CUSTOMERS")
#SecondaryTable(name = "DETAILS", pkJoinColumns=#PrimaryKeyJoinColumn(name="DETAIL_ID"))
public class Customer {
#Id
#GeneratedValue
#Column(name = "CUSTOMER_ID")
private Integer id;
#Column(table = "DETAILS", name = "OTHER")
private Integer notes;
// ...
}
but this works only if DETAIL_ID matches CUSTOMER_ID in the primary table.
So my question is: how can i use a foreign-key field in my primary table to join on the primary-key of the secondary table?
UPDATE
I tried to set:
#SecondaryTable(name = "DETAILS", pkJoinColumns=#PrimaryKeyJoinColumn(name="DETAIL_ID", referencedColumnName="DETAIL_ID"))
but when I run the application I get this exception:
org.hibernate.MappingException: Unable to find column with logical name: DETAIL_ID in org.hibernate.mapping.Table(CUSTOMERS) and its related supertables and secondary tables
For anyone looking for an answer to this, using #SecondaryTable is not the way to join two tables with non-primary key columns, because Hibernate will try to assosiate the two tables by their primary keys by default; you have to use #OneToMany review http://viralpatel.net/blogs/hibernate-one-to-many-annotation-tutorial/ for a solution, here's a code snippet in case that url stops working:
Customer Class:
#Entity
#Table(name="CUSTOMERS")
public class Customer {
#Id
#GeneratedValue
#Column(name="CUSTOMER_ID")
private Integer id;
#ManyToOne
#JoinColumn(name="DETAIL_ID")
private Details details;
// Getter and Setter methods...
}
Details Class:
#Entity
#Table(name="DETAILS")
public class Details {
#Id
#GeneratedValue
#Column(name="DETAIL_ID")
private int detailId;
#Column(name="OTHER")
private String other;
#OneToMany(mappedBy="details")
private Set<Customer> customers;
// Getter and Setter methods...
}
This is easily accessible through hibernate with the following code:
Session session = HibernateUtil.getSessionFactory().openSession();
Query query = session.createQuery("select id, details.other from Customer");
I hope this helps anyone out there spending hours searching for a way to achieve this like I did.
You can use the referenceColumnName attribute of the #PrimaryKeyJoinColumn annotation to define the join column to the referenced table. In fact, by combining use of name/referencedColumnName you can join on arbitrary on both sides, with the constraint that if duplicates are found your ORM provider will throw an exception.
Related
I am working on a Spring Boot application using Spring Data JPA and Hibernate mapping and I have the following problem.
I have this network table:
CREATE TABLE IF NOT EXISTS public.network
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
name character varying(50) COLLATE pg_catalog."default" NOT NULL,
description text COLLATE pg_catalog."default",
CONSTRAINT network_pkey PRIMARY KEY (id)
)
and this chain table:
CREATE TABLE IF NOT EXISTS public.chain
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
name character varying(50) COLLATE pg_catalog."default" NOT NULL,
fk_chain_type bigint NOT NULL,
description text COLLATE pg_catalog."default",
CONSTRAINT chain_pkey PRIMARY KEY (id),
CONSTRAINT "chain_to_chain_type_FK" FOREIGN KEY (fk_chain_type)
REFERENCES public.chain_type (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
These 2 tables are related to each other by a MANY TO MANY relationship, implmemented by this network_chain table:
CREATE TABLE IF NOT EXISTS public.network_chain
(
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
fk_network_id bigint NOT NULL,
fk_chain_id bigint NOT NULL,
CONSTRAINT network_chain_pkey PRIMARY KEY (id),
CONSTRAINT chain_id_fk FOREIGN KEY (fk_chain_id)
REFERENCES public.chain (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID,
CONSTRAINT network_id_fk FOREIGN KEY (fk_network_id)
REFERENCES public.network (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
Basically the fk_network_id field represents the id of a specific record into the network table while the fk_chain_id represents the id of a specific record into the chain table.
I mapped these DB tables with the following Hibernate entity classes, first of all I created this Network class mapping the network table:
#Entity
#Table(name = "network")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Network implements Serializable {
private static final long serialVersionUID = -5341425320975462596L;
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(name = "name")
private String name;
#Column(name = "description")
private String description;
#ManyToMany(cascade = { CascadeType.MERGE })
#JoinTable(
name = "network_chain",
joinColumns = { #JoinColumn(name = "fk_network_id") },
inverseJoinColumns = { #JoinColumn(name = "fk_chain_id") }
)
Set<Chain> chainList;
}
As you can see it contains the #ManyToMany annotation using the #JoinTable annotation in order to join my network table with my chain table (mapped by the Chain entity class) using the previous network_chain table (implementing the MANY TO MANY relationship).
So in this #JoinTable annotation I am specifying:
The merging table implementing the MANY TO MANY relationship: network_chain.
the two FK on this table that are fk_network_id and fk_chain_id.
Then I have this Spring Data JPA repository class named NetworkRepository:
public interface NetworkRepository extends JpaRepository<Network, Integer> {
/**
* Retrieve a Network object by its ID
* #param id of the network
* #return the retrieve Network object
*/
Network findById(String id);
/**
* Retrieve a Network object by its name
* #param name of the network
* #return a Network object
*/
Network findByName(String name);
/**
* Retrieve the list of all the possible networks
* #return a List<Network> object: the list of the all the networks
*/
List<Network> findAll();
}
Finally I created a JUnit test class containing a method in order to test the previous repository findAll() method, this one:
#SpringBootTest()
#ContextConfiguration(classes = GetUserWsApplication.class)
#TestMethodOrder(OrderAnnotation.class)
public class NetworkTest {
#Autowired
private NetworkRepository networkRepository;
/**
* Retrieve the networks list
*/
#Test
#Order(1)
public void findAllTest() {
List<Network> networksList = this.networkRepository.findAll();
assertTrue(networksList.size() == 5, "It retrieved 5 networks");
Set<Chain> networkChain = networksList.get(0).getChainList();
assertTrue(networkChain != null, "The network chains list are not null");
}
}
The problem is that the findAll() method execute this SQL statement( I can see it into my stacktrace):
Hibernate:
select
network0_.id as id1_6_,
network0_.description as descript2_6_,
network0_.name as name3_6_
from
network network0_
and retrieve the expected List object but as you can see in the following printscreen my chainList field give an error:
It seems that it have not retrieved the chainList from my MANY TO MANY table (infact thre previous Hibernate statement seems not perform any join on the network_chain and then chain tables).
I also tried to directly access to this field by this line (my idea was that maybe Hibernate performed this join when the access to this field is explicitly performed:
Set<Chain> networkChain = networksList.get(0).getChainList();
It seems that this com.sun.jdi.InvocationException exceptions happens when I try to retrieve this field. Basically it seems that the JOINS between the MANY TO MANY table and the on the chain table is never performed.
Why? What is wrong with my code? What am I missing? How can I try to fix it?
This is happening because you have an extra column (id) in the table (network_chain) which is responsible for the many-to-many relationship.
The #JoinTable annotation will support the definition of only 2 fields, usually the ids of the 2 tables. The 2 ids together, will be the primary key for the join table, also known as a composite primary key.
In your case, you will have to create an entity to represent the table network_chain.
#Entity
#Table(name = "network_chain")
public class NetworkChain {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#ManyToOne
private Network network;
#ManyToOne
private Chain chain;
}
public class Network implements Serializable {
//somewhere in the code
#OneToMany
List<NetworkChain> networkChains;
}
public class Chain implements Serializable {
//somewhere in the code
#OneToMany
List<NetworkChain> networkChains;
}
Now, the entity NetworkChain has the extra column id as the primary key and hibernate will do the proper joins to fetch the data.
Out of topic but not sure why there is a javadoc for the findAll() method which is usually part of the JpaRepository and weird findById as well..
I have three tables
CREATE TABLE "ingredient" (
"id" INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) PRIMARY KEY,
"ingredient" VARCHAR(50) NOT NULL
);
CREATE TABLE "pizza" (
"id" INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) PRIMARY KEY,
"pizza" VARCHAR(50) NOT NULL
);
CREATE TABLE "pizza_structure" (
"pizza_id" INT NOT NULL,
"ingredient_id" INT NOT NULL,
"amount" INT NOT NULL
);
how to join them, to get Pizzas structure as a Map
#Entity
#Table(name = "ingredient")
public class Ingredient{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
public Ingredient() {
}
}
#Entity
#Table(name = "pizza")
public class Pizza {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany ????
private Map<Ingredient, Integer> pizzaStructure;
public Pizza() {
}
public Pizza(String name, Map<Long, Integer> pizzaStructure) {
this.name = name;
this.pizzaStructure = pizzaStructure;
}
}
do I need to create #Embeddable class PizzaStructure, if yes when how to use it?
now I'm getting an error
Invocation of init method failed; nested exception is org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class:
how to join them, to get Pizzas structure as a Map
It seems to look like this:
#ElementCollection
#CollectionTable(name = "pizza_structure", joinColumns = {#JoinColumn(name = "pizza_id")})
#Column(name = "amount")
#MapKeyJoinColumn(name = "ingredient_id")
private Map<Ingredient, Integer> pizzaStructure;
do I need to create #Embeddable class PizzaStructure
No.
More info is here: Hibernate User Guide - Maps.
Note that table pizza_structure should have foreign keys to pizza and ingredient tables and also unique constrain of pizza_id and ingredient_id, like this (it's postgresql dialect):
create table pizza_structure
(
pizza_id ... constraint fk_structure_pizza references pizza,
ingredient_id ... constraint fk_structure_ingredient references ingredient,
amount ...,
constraint pizza_structure_pkey primary key (pizza_id, ingredient_id)
);
You have a manyToMany relationship between pizza and ingredient and an additional column in your relationship.
I found a similar question here: JPA 2.0 many-to-many with extra column
(I would comment, but i do not have enough reputation.)
I'm new with JPA, and want to create a Database with this relation :
|Participant|
|id : INT (PK) | id_event : INT (PK, FK) |
|Event|
|id : INT (PK) |
I'm totally lost and barely figure the syntax of the examples I found :/
But I understood I need to create an other class to contain the two pieces of the PK, which leads to another question : can this class be an inner-class (for optimisation purposes) ?
I hope I'm not asking too much but I really want to get it.
Your entities might be like this:
#Entity
public class Participant {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(fetch = FetchType.LAZY) // or any other relation
private List<Event> events;
// fields, constructors, getters, setters
}
#Entity
public class Event {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// fields, constructors, getters, setters
}
In this case JPA will create 3 tables with the following queries (SQL dialect will vary from DB to DB, in this case I used H2 database):
CREATE TABLE Event (
id bigint GENERATED BY DEFAULT AS IDENTITY,
PRIMARY KEY (id)
);
CREATE TABLE Participant (
id bigint GENERATED BY DEFAULT AS IDENTITY,
PRIMARY KEY (id)
);
CREATE TABLE Participant_Event (
Participant_id bigint NOT NULL,
events_id bigint NOT NULL
)
Participant_Event is automatically created join table to link participants and events.
Here is a good example of understanding JPA entity relations.
For a OneToMany relation you need the below entities and tables:
Participant
Event
The Event entity is simple:
#Entity
public class Event {
#Id
private Long id;
// fields, constructors, getters, setters
}
The entity Participant has to hold the composite key (aka two pieces of the PK), so, every Participant is only linked once with an Event.
#Entity
public class Participant {
#EmbeddedId
private EventParticipantPK id;
#OneToMany(fetch = FetchType.LAZY)
private List<Event> events;
// fields, constructors, getters, setters
}
The composite key is declared as an EmbeddedId.
The EventParticipantPK should be like:
#Embeddable
public class EventParticipantPK {
#Column (name = "PARTICIPANT_ID")
private Long participantId;
#Column (name = "EVENT_ID")
private Long eventId;
// fields, constructors, getters, setters
}
I hope this helps.
It`s possible to create one map with hibernate #ManyToOne just like this:
public class IndicadorAtos {
#JsonIgnore
#Id
#Column(name="cod_ato_praticado")
private Integer codAtoPraticado;
#Column(name="descricao_ato")
private String ato;
#JoinColumn(name = "cod_ato", referencedColumnName = "cod_ato")
#ManyToOne
#Fetch(FetchMode.SUBSELECT)
private Atos atos;
}
But in some cases I dont have association or in my table IndicadorAtos have one code, that don`t existis in table Atos
this is my tables:
create table IndicadorAtos (
codAtoPraticado integer primary key,
ato varchar(250),
cod_ato integer
);
create table Atos(
cod_ato integer primary key.
name varchar(250)
)
I try to create this join:
Select t FROM IndicadorAtos t , Atos a where t.cod_ato = a.cod_ato, but I need to return all records from my IndicadorAtos, and with this select he only return all itens that have one item in Atos.
tks
It`s possible to create one map with hibernate #ManyToOne
Yes; it is called unidirectional relationship.
If I understood your question properly, you want to select all entries from IndicadorAtos with possibly associated entries from Atos. You can achieve this by using left join as follows:
SELECT t FROM IndicadorAtos t LEFT JOIN t.atos at
provided that you have an entity Atos defined like:
#Entity
public class Atos {
#Id #GeneratedValue
private int cod_ato;
private String name;
// getters and setters
}
i am trying on many to many relationship, Team member can work on multiple projects and a project can have multiple team member , the table structure is as follows,
create table TBL_PROJECT_ONE(
id integer primary key generated always as identity(start with 12,increment by 3),
name varchar(50)
)
create table TBL_TEAM_MEMBER_ONE(
id integer primary key generated always as identity(start with 7,increment by 5),
name varchar(50),
salary integer
)
create table EMP_PRJ_CADRE(
MEMBER_ID integer references TBL_TEAM_MEMBER_ONE,
PRJ_ID integer references TBL_PROJECT_ONE,
CADRE varchar(10),
constraint PK_001_EMP_TEAM primary key (MEMBER_ID,PRJ_ID)
)
Here i have created a new table just to store the relationship,
Now please follow the Employee entity,
#Entity
#Table(name="TBL_TEAM_MEMBER_ONE")
public class EmployeeEntityFour implements Serializable{
public EmployeeEntityFour(){}
public EmployeeEntityFour(String empName,Integer salary){
...
..
}
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="ID")
private Integer empId;
#Column(name="NAME")
private String empName;
#Column(name="SALARY")
private Integer empSal;
#ElementCollection(fetch= FetchType.LAZY)
#CollectionTable(name="EMP_PRJ_CADRE")
#MapKeyJoinColumn(name="PRJ_ID")
#Column(name="CADRE")
private Map<ProjectEntityOne,String> employeeCadre;
...
..
.
}
Please follow the mapping for Project Entity,
#Entity
#Table(name="TBL_PROJECT_ONE")
public class ProjectEntityOne implements Serializable{
public ProjectEntityOne(){}
public ProjectEntityOne(String name){
this.projectName = name;
}
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="ID")
private Integer projectId;
#Column(name="NAME")
private String projectName;
#ElementCollection(fetch= FetchType.LAZY)
#CollectionTable(name="EMP_PRJ_CADRE")
#MapKeyJoinColumn(name="MEMBER_ID")
#Column(name="CADRE")
private Map<EmployeeEntityFour,String> employeeCadre;
....
..
.
}
In main method testing the code written is as follows,
ProjectEntityOne proj = new ProjectEntityOne("Citi Grand Central");
Map<EmployeeEntityFour,String> cadreMap = new HashMap<EmployeeEntityFour,String>();
cadreMap.put(new EmployeeEntityFour("Murlinarayan Muthu",34000), "Senior Software Engineer");
cadreMap.put(new EmployeeEntityFour("Gopalkrishna Rajnathan",64000), "Software Engineer");
cadreMap.put(new EmployeeEntityFour("Premanna Swaminathan",94000), "Project Manager");
proj.setEmployeeCadre(cadreMap);
em.persist(proj);
but i am getting an error which is
ERROR: 'PROJECTENTITYONE_ID' is not a column in table or VTI 'APP.EMP_PRJ_CADRE'.
When in both the entities i have specified #MapKeyJoinColumn than too i am getting an error as improper column for the third table.
Where i am missing
It somehow worked, i had to do some changes in the code,
first, the edited code in Entity ProjectEntityOne is as follows,
#ElementCollection(fetch= FetchType.LAZY)
#CollectionTable(name="EMP_PRJ_CADRE",joinColumns=#JoinColumn(name="PRJ_ID"))
#MapKeyJoinColumn(name="MEMBER_ID")
#Column(name="CADRE")
private Map<EmployeeEntityFour,String> employeeCadre;
What i have done here is i added #JoinedColumn in #CollectionTable,
Second change i did in Entity EmployeeEntityFour, the change is I removed Map of PorjectEntityOne from it,
in test,
i can save Project with Employee mapping but here all the employees should be already saved one.
i.e. the key of map
Map<EmployeeEntityFour,String> employeeCadre;
should be already persisted
and than we can persist project entity.
On employeeCadre in EmployeeEntityFour you need a #JoinColumn(name="MEMBER_ID") and you would also need a #JoinColumn(name="PRJ_ID") in the ProjectEntityOne employeeCadre.
But, I would not model it this way. First of all you cannot have a bi-directional ElementCollection mapping, and ElementCollection can only be owned by one side. The best solution would be to define an Cadre entity mapping to EMP_PRJ_CADRE table and have a OneToMany to it from both sides, and have it have a ManyToOne to each.
Alternatively you may use a ManyToMany with a MapKeyColumn, but I think you would be better off having an entity.