Double-foreign-key in Hibernate when mapping the same entity twice - java

It is common practice to map the same entity twice or even thrice, every time with a subset of columns needed for processing. I have found that with Hibernate 3.5.1, every time a #ManyToOne or a #OneToMany exists in two entities mapping the same table, the foreign key is created twice. This has no impact on MySQL and SQL Server, but Oracle refuses the second creation statement.
Here is an example:
#Entity
#javax.persistence.SequenceGenerator(name = "SEQ_STORE", sequenceName = "SEQ_ENTITY")
#Table(name = "ENTITIES")
class Entity {
//All columns
//And then.....
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "BRIDGE_TABLE", joinColumns = { #JoinColumn(name = "ENTITY_ID") }, inverseJoinColumns = { #JoinColumn(name = "ROLE_ID") })
#OrderBy("id DESC")
private Set<Role> roles = new HashSet<Roles>();
}
#Entity
#javax.persistence.SequenceGenerator(name = "SEQ_STORE", sequenceName = "SEQ_ENTITY")
#Table(name = "ENTITIES")
class EntityListItem {
//Only a subset of the previous columns
//And then.....
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "BRIDGE_TABLE", joinColumns = { #JoinColumn(name = "ENTITY_ID") }, inverseJoinColumns = { #JoinColumn(name = "ROLE_ID") })
#OrderBy("id DESC")
private Set<Role> roles = new HashSet<Roles>();
}
Currently, Role is designed not to be navigable to Entity (otherwise I guess there will be 4 foreign keys).
Here are the statement being issued by Hibernate:
create table BRIDGE_TABLE (ENTITY_ID number(19,0) not null, ROLE_ID varchar2(60 char) not null, primary key (ENTITY_ID, ROLE_ID)); //Creates the table
alter table BRIDGE_TABLE add constraint FK47CFB9F0B068EF3F foreign key (ENTITY_ID) references ENTITIES;
alter table BRIDGE_TABLE add constraint FK47CFB9F0B068EF3F foreign key (ENTITY_ID) references ENTITIES;
I'm not sure whether this is a Hibernate bug. We cannot currently move to Hibernate 4. Can be this fixed via code or does it need a new Hibernate version?

I have made a workaround:
Add a #ForeignKey annotation with the same FK name to both entities (e.g. #ForeignKey(name = "FK_TO_ENTITY", inverseName = "FK_TO_ROLE"))
Extend LocalSessionFactoryBean like the following:
#override
public void createDatabaseSchema() throws DataAccessException
{
logger.info("Creating database schema for Hibernate SessionFactory");
SessionFactory sessionFactory = getSessionFactory();
final Dialect dialect = ((SessionFactoryImplementor) sessionFactory).getDialect();
final LinkedHashSet<String> sql = new LinkedHashSet<String>();
for (String query : getConfiguration().generateSchemaCreationScript(dialect))
sql.add(query);
HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory);
hibernateTemplate.execute(new HibernateCallback<Void>()
{
#Override
public Void doInHibernate(Session session) throws SQLException
{
session.doWork(new Work()
{
#Override
public void execute(Connection conn) throws SQLException
{
PhoenixAnnotationSessionFactoryBean.this.executeSchemaScript(conn, sql.toArray(new String[0]));
}
});
return null;
}
});
}
Reason: the #ForeignKey annotation ensures that the FKs will have the same name, hence the SQL statements will be equal each other. The overriden LSFB will store the SQL queries needed to create the schema in a Set so that no duplicate will be allowed.

Related

Spring Data/Hibernate/JPA join by non-primary keys

There are two related entities Account and AccountInfo. These entities connected by non-primary keys (number_ff, number_ff2). I see that Hibernate fetching three entities(Account, AccountInfo, AccountInfo) by one sql query. But I see that hibernate do additional query to database as well. Why does hibernate do additional call(query2)? How to avoid it?
Also I tried to use JPQL query. It works the same way (2 sql queries).
Database schema(postgres):
CREATE TABLE public.accounts (
user_id serial PRIMARY KEY,
number_ff varchar NULL,
username varchar(50) NULL,
number_ff2 varchar NULL,
CONSTRAINT accounts_pkey PRIMARY KEY (user_id)
);
CREATE TABLE public.account_info (
account_info_id serial4 NOT NULL,
number_ff varchar NULL,
info varchar(1000) NULL,
CONSTRAINT account_info_pkey PRIMARY KEY (account_info_id)
);
Hibernate enitity 1:
#Entity
#Table(name = "accounts")
#Getter
#Setter
#EqualsAndHashCode
public class Account implements Serializable {
#Id
private Long userId;
#Column(name = "number_ff")
private String numberFf;
#Column(name = "number_ff2")
private String numberFf2;
private String username;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "number_ff", referencedColumnName = "number_ff", insertable = false, updatable = false, unique = true)
private AccountInfo accountInfos;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "number_ff2", referencedColumnName = "number_ff", insertable = false, updatable = false, unique = true)
private AccountInfo accountInfo2s;
}
Entity 2:
#Entity
#Table(name = "account_info")
#Getter
#Setter
public class AccountInfo implements Serializable {
#Id
private Long accountInfoId;
#NaturalId
#Column(name = "number_ff")
private String numberFf;
private String info;
}
Service:
#Service
public class ServiceInfo {
#Autowired
private AccountRepository accountRepository;
#PostConstruct
public void init() {
Optional<Account> account = accountRepository.findById(1L);
System.out.println(account);
}
}
SQL queries: Query 1 (Query that retrieves all the necessary data)
select
account0_.user_id as user_id1_1_0_,
account0_.number_ff2 as number_f3_1_0_,
account0_.number_ff as number_f2_1_0_,
account0_.username as username4_1_0_,
accountinf1_.account_info_id as account_1_0_1_,
accountinf1_.info as info2_0_1_,
accountinf1_.number_ff as number_f3_0_1_,
accountinf2_.account_info_id as account_1_0_2_,
accountinf2_.info as info2_0_2_,
accountinf2_.number_ff as number_f3_0_2_
from
accounts account0_
left outer join account_info accountinf1_ on
account0_.number_ff2 = accountinf1_.number_ff
left outer join account_info accountinf2_ on
account0_.number_ff = accountinf2_.number_ff
where
account0_.user_id =?
Query 2: (Additional query, why does it do it?)
select
accountinf0_.account_info_id as account_1_0_0_,
accountinf0_.info as info2_0_0_,
accountinf0_.number_ff as number_f3_0_0_
from
account_info accountinf0_
where
accountinf0_.number_ff =?
I think second call is redundant. Why does hibernate do additional call (query 2)? How to avoid it? It looks like bug for me.
Please see small project on GitHub: github link

How to avoid duplicate insertions of "many" entity in one-to-many relationship when using a unique constraint in eclipselink/JPA

I am working on a large codebase using Spring MVC with EclipseLink 2.5.2 on a mysql database. The database and its structure are created directly, not through any code-first approach. My problem concerns 2 tables in a one-to-many relationship.
CREATE TABLE ROLE (
ID BIGINT(20) PRIMARY KEY,
-- OTHER FIELDS --
);
CREATE TABLE ROLE_DOMAIN (
ID BIGINT(20) PRIMARY KEY,
ROLE_ID BIGINT(20) NOT NULL,
DOMAIN VARCHAR(255) NOT NULL
-- OTHER FIELDS --
);
ALTER TABLE ROLE_DOMAIN ADD CONSTRAINT FK_ROLE_DOMAIN_ROLE_ID FOREIGN KEY (ROLE_ID) REFERENCES ROLE_BASE (ID) ON DELETE CASCADE;
ALTER TABLE ROLE_DOMAIN ADD CONSTRAINT UQ_ROLE_DOMAIN_ROLE_ID_DOMAIN UNIQUE (ROLE_ID, DOMAIN);
And in java, this is how I've got the two entities configured.
#Entity
public class Role {
private Long id;
private Set<RoleDomain> roleDomains = new HashSet<>();
#Id
#TableGenerator(name = "ROLE.ID", allocationSize = 1, initialValue = 1)
#GeneratedValue(strategy = GenerationType.TABLE, generator = "ROLE.ID")
public Long getID() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "ROLE_ID", referencedColumnName = "ID", insertable = false, updatable = false)
public Set<RoleDomain> getRoleDomains() {
return roleDomains;
}
public void setRoleDomains(Set<RoleDomain> roleDomains) {
this.roleDomains = roleDomains;
}
}
#Entity
#Table(name = "ROLE_DOMAIN")
public class RoleDomain {
private Long id;
private Long roleId;
private String domain;
#Id
#TableGenerator(name = "ROLE_DOMAIN.ID", allocationSize = 1, initialValue = 1)
#GeneratedValue(strategy = GenerationType.TABLE, generator = "ROLE_DOMAIN.ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "ROLE_ID", nullable = false)
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
#Column(name = "DOMAIN", length = 255)
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
}
Say that in this table structure, I already have a record in ROLE and a record in ROLE_DOMAIN that references it, translating to a Role object named myRole containing the RoleDomain in roleDomains.
Now, when I add a new RoleDomain and save using a spring data repository like this:
myRole.add(new RoleDomain("some string"));
roleRepository.save(myRole);
I get an exception for a duplicate insert violating my unique constraint on ROLE_ID and DOMAIN in the database.
[EL Warning]: 2020-10-22 14:53:22.405--UnitOfWork(994047815)--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.6.8.v20190620-d6443d8be7): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '198732-some string' for key 'UQ_ROLE_DOMAIN_ROLE_ID_DOMAIN'
Error Code: 1062
Call: INSERT INTO ROLE_DOMAIN (ID, DOMAIN, ROLE_ID) VALUES (?, ?, ?)
bind => [27, some other string, 198732]
The weirdest thing about this problem is that if I remove the unique constraint from the database (Note: keeping the java annotation configuration EXACTLY the same. Literally just "DROP CONSTRAINT..." in the db) then the save call works just fine. It doesn't create duplicates in ROLE_DOMAIN. It does exactly what it's supposed to, just adds the new record to ROLE_DOMAIN.
I don't understand how a unique constraint in the db would cause eclipselink to act this inconsistently. Do I have something configured wrongly? Thanks.
EDIT:
I have just now tried replacing the #Table annotation on the RoleDomain class with this:
#Table(name = "ROLE_DOMAIN", uniqueConstraints =
#UniqueConstraint(columnNames = {"ROLE_ID", "DOMAIN"}))
It didn't change anything.
The issue with your constraint is that EclipseLink orders statements for batching, putting deletes last - this is to give you a chance to clean up other constraints, to modify existing rows before rows get deleted. This can be changed so that deletes are issued first using the setShouldPerformDeletesFirst method on the UnitOfWork. As this is native api, you will have to unwrap the EntityManager to get at it, using
em.unwrap(org.eclipse.persistence.sessions.UnitOfWork.class)
if you are in a transaction. This will only be set for the UnitOfWork within this EntityManager, so if you need it everywhere always, you will want to have a session listener with your own session adaptor class to listen for postAcquireUnitOfWork and call setShouldPerformDeletesFirst on it.

Hibernate : How to Join 3 tables in one join table by Annotation?

I have some roles, users and applications
I want to create a mapping hibernate of this table :
CREATE TABLE role_application_user (
role_identifier INTEGER not null,
application_identifier INTEGER not null,
user_identifier INTEGER not null,
KEY FK_role_identifier (role_identifier),
KEY FK_application_identifier(application_identifier),
KEY FK_user_identifier (user_identifier),
CONSTRAINT FK_role_identifier FOREIGN KEY (role_identifier) REFERENCES role (identifier),
CONSTRAINT FK_application_identifier FOREIGN KEY (application_identifier) REFERENCES application (identifier),
CONSTRAINT FK_user_identifier FOREIGN KEY (user_identifier) REFERENCES users (login)
);
for an application, a role can have many users and a user can of many roles.
I try this mapping :
Application.java
#JoinTable(name = "role_application_user",
joinColumns = #JoinColumn(name = "application_identifier"),
inverseJoinColumns = #JoinColumn(name = "user_identifier"))
#MapKeyJoinColumn(name = "role_identifier")
#ElementCollection
private Map<Role, User> userByRole = new HashMap<>();
Unfortunately, this is not working in my case because in java, the key of a Map must be unique.
With this mapping, we can have only one user for a role and an application.
try this implementation :
#Entity
public class User {
#OneToMany
private List<RoleInApplication> rolesInApplications;
}
#Entity
public class Role {
#OneToMany
private List<RoleInApplication> rolesInApplications;
}
#Entity
public class RoleInApplication {
#ManyToOne
private User user;
#ManyToOne
private Role role;
#ManyToOne
private Application application;
}
#Entity
public class Application {
#OneToMany
private List<RoleInApplication> rolesInApplications;
}

Fetch join by JPQL in openjpa for many to many mapping

I have a device and device_group table, mapping by a device_group_mapping table as below
CREATE TABLE device_group_mapping
(
device_id character varying(64) NOT NULL,
device_group_id bigint NOT NULL,
CONSTRAINT "FK_device_group_mapping_device" FOREIGN KEY (device_id)
REFERENCES device (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "FK_device_group_mapping_device_group" FOREIGN KEY (device_group_id)
REFERENCES device_group (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
OIDS=FALSE
);
The device and deviceGroup entity of openjpa as below
#Entity
#Table(name = "device")
public class Device implements Serializable
{
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "device_group_mapping", joinColumns =
{#JoinColumn(name = "device_id", referencedColumnName = "id", nullable = false)}, inverseJoinColumns =
{#JoinColumn(name = "device_group_id", referencedColumnName = "id", nullable = false)})
private List<DeviceGroup> deviceGroupCollection;
}
#Entity
#Table(name = "device_group")
public class DeviceGroup implements Serializable
{
#ManyToMany(mappedBy = "deviceGroupCollection", fetch = FetchType.EAGER)
#OrderBy()
private List<Device> deviceCollection;
}
Due to the fetch type is lazy, I have to get the deviceGroupCollection as below code
#Override
#Transactional
public List<Device> findAllDevicesWithGroupMapping() throws Exception
{
List<Device> list = new ArrayList<Device>();
list = this.deviceDao.findAll();
for (Device device : list)
{
device.setDeviceGroupCollection(device.getDeviceGroupCollection());
}
return list;
}
However, this will be very slow when list of devices contains amount of devices.
I think maybe I could just find device entity by JPQL with fetch join the device_group, but don't know how to do it. According to openjpa spec., it doesn't support on clause and also nested fetch join.
The openjpa I currently used as below
<dependency>
<groupId>org.apache.openjpa</groupId>
<artifactId>openjpa-all</artifactId>
<version>2.2.2</version>
</dependency>
Any help is appreciated.
You use a fetch join an a ManyToMany like on any other association. You don't need any on clase, since the association mapping already defines how the two entities are linked to each other:
select d from Device d
left join fetch d.deviceGroupCollection
where ...

ManyToMany - delete entries in a Join table

I have two entities and they have many-to-many relationship, what I want to achieve is when I delete one entity, then all entries referencing it in a join table should be removed as well. How do I do that?
So far I have:
#JoinTable(name = "interviewer_technology",
joinColumns = {
#JoinColumn(name = "interviewer_id", referencedColumnName = "id")
},
inverseJoinColumns = {
#JoinColumn(name = "technology_id", referencedColumnName = "id")
})
#ManyToMany(fetch = FetchType.LAZY)
private Collection<Technology> technologies;
in the owning entity and:
#ManyToMany(mappedBy = "technologies", fetch = FetchType.LAZY)
private Collection<Interviewer> interviewers;
in the inverse one, but when I try to delete the inverse one, I get the error that there are fields in join table referencing it. How can I tackle this problem?
first include in your #ManyToMany annotations
cascade=CascadeType.ALL.
After that make sure the calling class has a valid transaction going on and that your target entity is attached to your PersistenceContext at the moment it's being deleted, if not so, you might have to do a merge for the entity, check also if the resulting SQLs are being executed by your DB:
Sample delete method of Business Class:
#TransactionAttribute(TransactionAttributeType.REQUIRED)
public void delete(T entity) throws ServiceException {
try {
if (!entityManager.contains(entity)) {
entity = entityManager.merge(entity);
}
entityManager.remove(entity);
} catch (Exception se) {
se.printStackTrace();
throw new ServiceException("Error while deleting from DB " +
entity.getClass().getSimpleName() + " id: " + entity.getId());
}
}

Categories