Hibernate OneToOne between PK's with lazy behaviour - java

I'm trying to achieve to have an entity called MyEntity along with another entity called MyEntityInfo using Hibernate 5.3.13.Final with annotations under Wildfly 18.
The idea is to have MyEntity store some commonly requested fields, and MyEntityInfo store some rarely requested fields. Both share the same primary key called SID (Long), and there is a FK from Info's SID to Entity's SID. There can be entities without info.
Normally you will not require the additional info. For example, I don't want the info entity to be fetched when I query my entity like this:
MyEntityImpl entity = em.find(MyEntityImpl.class, 1L);
However, when I run this code, I find that there's a second query, fetching the Info entity along the main one, as in an EAGER behaviour.
I'm mapping the relationship using #OneToOne. I've tried several combinations of FetchType, optional and #LazyToOne, but so far without success.
Here is the code for both MyEntity and MyEntityInfo classes (additional getters and setters removed):
MyEntity (ID generator is a custom sequence generator):
#Entity
#Table(name = MyEntityImpl.TABLE_NAME)
public class MyEntityImpl {
public static final String TABLE_NAME = "TMP_MY_ENTITY";
#Id
#GeneratedValue(strategy = GenerationType.TABLE, generator = "GEN_" +
TABLE_NAME)
#GenericGenerator(name = "GEN_" +
TABLE_NAME, strategy = CoreIdGenerator.ID_GENERATOR, parameters = {
#Parameter(name = "tableName", value = TABLE_NAME) })
#Column(name = "sid", nullable = false, unique = true)
private Long sid;
#OneToOne(mappedBy = "myEntity", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#LazyToOne(LazyToOneOption.NO_PROXY)
private MyEntityInfoImpl info;
#Column
private String field;
MyEntityInfo:
#Entity
#Table(name = MyEntityInfoImpl.TABLE_NAME)
public class MyEntityInfoImpl {
public static final String TABLE_NAME = "TMP_MY_ENTITY_INFO";
#Id
#Column(name = "SID", nullable = false, unique = true)
private Long sid;
#OneToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "SID", referencedColumnName = "SID", insertable = false, updatable = false, nullable = false)
private MyEntityImpl myEntity;
#Column(name = "INFO_FIELD")
private String infoField;
I've tried this solution, but as I said, it didn't work for me:
Hibernate lazy loading for reverse one to one workaround - how does this work?
I've managed to do something somewhat similar using #OneToMany and managing data manually, but that's not what I'd like to do. However, another alternatives and information on whether this can be achieved or not using #OneToOne, or the right design pattern to do this are also welcome.
PS: Database tables creation for SQL Server, in case you want to try it:
create table TMP_MY_ENTITY (SID NUMERIC(19,0) NOT NULL, FIELD VARCHAR(100));
go
ALTER TABLE TMP_MY_ENTITY ADD CONSTRAINT PK_TMP_MY_ENTITY PRIMARY KEY CLUSTERED (SID);
go
create table TMP_MY_ENTITY_INFO (SID NUMERIC(19,0) NOT NULL, INFO_FIELD VARCHAR(100));
go
ALTER TABLE TMP_MY_ENTITY_INFO ADD CONSTRAINT PK_TMP_MY_ENTITY_INFO PRIMARY KEY CLUSTERED (SID);
go
CREATE SEQUENCE SEQ_TMP_MY_ENTITY START WITH 1 INCREMENT BY 1 MINVALUE 1 CACHE 20;
alter table TMP_MY_ENTITY_INFO add constraint FK_TMP_MY_ENT_INFO_MY_ENT FOREIGN KEY (SID) references TMP_MY_ENTITY(SID);
go
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 1');
insert into TMP_MY_ENTITY_INFO(SID, INFO_FIELD) VALUES ((SELECT MAX(SID) FROM TMP_MY_ENTITY), 'Info 1');
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 2');
insert into TMP_MY_ENTITY_INFO(SID, INFO_FIELD) VALUES ((SELECT MAX(SID) FROM TMP_MY_ENTITY), 'Info 2');
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 3 no info');
-- DELETE ALL
drop table TMP_MY_ENTITY_INFO;
drop table TMP_MY_ENTITY;
drop sequence SEQ_TMP_MY_ENTITY;

After following #SternK link, and upgrading to Wildfly 19 and Hibernate 5.4.14, it finally worked by using #MapsId.
The right mapping to use is this:
MyEntity:
public class MyEntityImpl {
#OneToOne(mappedBy = "myEntity", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "SID")
private MyEntityInfoImpl info;
MyEntityInfo:
public class MyEntityInfoImpl {
#OneToOne(fetch = FetchType.EAGER, optional = false)
#MapsId
#JoinColumn(name = "SID", referencedColumnName = "SID", insertable = false, updatable = false, nullable = false)
private MyEntityImpl myEntity;

Related

How to use derived columns in Spring Data JPA associations

I have an entity TeamActivity:
#Entity
#Table(name = "teams_to_activities")
public class TeamActivity {
#Column(name = "scope_id", nullable = false)
private String scopeId;
#Column(name = "team_id", nullable = false)
private String teamId;
#Column(name = "activity_set_id", nullable = false)
private String activitySetId;
#Id
#Column(name = "scoped_team_activity_id", nullable = false)
private String scopedTeamActivityId;
}
And another entity ActivitySet:
#Entity
#Table(name = "activity_sets")
public class ActivitySet {
#Column(name = "scope_id", nullable = false)
private String scopeId;
#Column(name = "name", nullable = false)
private String name;
#Column(name = "description", nullable = false)
private String description;
#Id
#Column(name = "scoped_activity_set_id", nullable = false)
private String scopedActivitySetId;
}
There's no index on any other column besides the PK in both tables.
There's no FK constraint creating a relationship between these tables whatsoever. I have no idea why as this is a legacy system.
Technically, if I fetch a TeamActivity record, I can pick the scope_id and activity_set_id from it and combine them to form a scoped_activity_set_id which would be a valid PK to fetch the corresponding ActivitySet.
I know that TeamActivity -> ActivitySet is a N -> 1 association
I would like to leverage Spring Data JPA features to create an association from TeamActivity to ActivitySet such that when I fetch a TeamActivity from TeamActivityRepository, the corresponding ActivitySet is also returned.
I have created an association like this before using a combination of #JoinColumn and #MapsId but there was actually a single FK to use which is different here where source table has 2 columns I can combine to get the target's key.
If you are fully in control of the database, I may propose you create a Materialized View with the contents you desire from both tables and handle it as any other table with JPA, i.e, create #Entity model and CrudRepository<MVTeamActivitySet, String>.
If you are not fully in control of the database, one easy way to achieve it is to simply create a method that internally executes two lookup queries and retrieves the expected model you want. You will still be using using JPA correctly.
Querying two tables and joining desired fields in the code layer is quite common with denormalized DBs, sometimes you want to avoid the overhead of a Materialized View.
#Override
public TeamActivitySetDto findById(String scopedTeamActivityId) throws DemoCustomException {
Optional<TeamActivity> teamActivityEntity = teamActivityDao.getById(scopedTeamActivityId);
if(teamActivityEntity.isEmpty()) {
throw new DemoCustomException("teamActivity record not found");
}
String scopedActivitySetId =
teamActivityEntity.get().getScopeId() + ":" + teamActivityEntity.get().getActivitySetId();
Optional<ActivitySet> activitySetEntity = activitySetDao.getById(scopedActivitySetId);
if(activitySetEntity.isEmpty()) {
throw new DemoCustomException("activitySet record not found");
}
return TeamActivitySetDto.builder()
.description(activitySetEntity.get().getDescription())
.name(activitySetEntity.get().getName())
.scopedActivitySetId(activitySetEntity.get().getScopedActivitySetId())
.activitySetId(teamActivityEntity.get().getActivitySetId())
.scopedTeamActivityId(teamActivityEntity.get().getScopedTeamActivityId())
.scopeId(teamActivityEntity.get().getScopeId())
.teamId(teamActivityEntity.get().getTeamId())
.build();
}

JPA ManyToMany to use grouping & crosswalks to join data together

Building a Spring Boot REST service backed by MySQL here. I'm adding a super-simple chat feature to an app and this service will handle its backend/enndpoints. I'm new to JPA and have two concerns: (1) that my primordial data model itself may be a little awry; and (2) that I'm not wrapping that model correctly using JPA conventions/best practices.
So first: an overview of the simple problem I'm trying to solve: Users can send Messages to 1+ other Users. This creates a Conversation, which is really just a container of 1+ Messages. If the Conversation is only between 2 Users, it's considered (by the app) to be a Direct Message (DM). Otherwise its considered to be a Group Chat.
My tables (pseudo-schema):
[users]
=======
id PRIMARY KEY AUTO_INC INT NOT NULL,
username VARCHAR(255) NOT NULL
[conversations]
===============
id PRIMARY KEY AUTO_INC INT NOT NULL,
created_on DATETIME NOT NULL
[messages]
==========
id PRIMARY KEY AUTO_INC INT NOT NULL,
conversation_id FOREIGN KEY INT NOT NULL, # on conversations table
sender_id FOREIGN KEY INT NOT NULL, # on users table
text VARCHAR(2000) NOT NULL,
sent_at DATETIME
[users_x_conversations]
=======================
id PRIMARY KEY AUTO_INC INT NOT NULL,
conversation_id FOREIGN KEY INT NOT NULL, # on conversations table
user_id FOREIGN KEY INT NOT NULL, # on users table
So in my design above, you can see I'm really just using the [conversations] table as a placeholder and as a way of grouping messages to a single conversation_id, and then [users_x_conversations] is crosswalk (many-to-many) table where I'm actually storing who is a "member of" which conversation.
Is this the right approach to take or is there a better way to relate the tables here? That's Concern #1.
Assumning I'm modeling the problem at the database correctly, then I have the following JPA/entity classes:
#MappedSuperclass
abstract public class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Ctors, getters & setters down here...
}
#Entity(name = 'messages')
#AttributeOverrides({
#AttributeOverride(name = 'id', column=#Column(name='message_id'))
})
public class Message extends BaseEntity {
#OneToOne(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinColumn(name = 'conversation_id', referencedColumnName = 'conversation_id')
#NotNull
#Valid
private Conversation conversation;
#OneToOne(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinColumn(name = 'user_id', referencedColumnName = 'user_id')
#NotNull
#Valid
private User sender;
#Column(name = 'message_text')
#NotEmpty
private String text;
#Column(name = 'message_sent_at')
#NotNull
private Date sentAt;
// Ctors, getters & setters down here...
}
#Entity(name = 'conversations')
#AttributeOverrides({
#AttributeOverride(name = 'id', column=#Column(name='conversation_id'))
})
public class Conversation extends BaseEntity {
#Column(name = 'conversation_created_on')
#NotNull
private Date createdOn;
// Ctors, getters & setters down here...
}
What I'm stuck on now is: how should I model my [users_x_conversations] table at the JPA layer? Should I create something like this:
#Entity(name = 'users_x_conversations')
#AttributeOverrides({
#AttributeOverride(name = 'id', column=#Column(name='users_x_conversations_id'))
})
public class UserConversations extends BaseEntity {
#ManyToMany(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinTable(
name="users_x_conversations",
joinColumns=[
#JoinColumn(name="user_id")
],
inverseJoinColumns=[
#JoinColumn(name="conversation_id")
]
)
private Map<User,Conversation> userConversations;
// Ctors, getters & setters down here...
}
Basically my service will want to be able to do queries like:
Given a conversationId, who are all the users that are members of that conversation?; and
Given a userId, what are all the conversations that user is a member of (DM and Group Chat alike)?
Is this the right approach to take or is there a better way to relate the tables here?
Your approach seems OK at the DB layer, except that if users_x_conversations serves only as a join table (i.e. if there are no extra properties associated with the (user, conversation) associations represented within), then I would use (conversation_id, user_id) as its PK instead of giving it a surrogate key. If you don't do that, then you should at least put a uniqueness constraint on that pair.
What I'm stuck on now is: how should I model my [users_x_conversations] table at the JPA layer?
I take you to be asking whether you should model that table as an entity. If you insist on giving it a surrogate key as you have done, then that implies "yes". But as I already discussed, I don't think that's needful. Nor much useful, for that matter. I would recommend instead modeling a direct many-to-many relationship between Conversation and User entities, with this table (less its id column) serving as the join table:
#Entity
#Table(name = "converations")
public class Conversation extends BaseEntity {
#Column(name = 'conversation_created_on')
#NotNull
private Date createdOn;
#ManyToMany(mappedBy = "conversations")
#JoinTable(name = "users_x_conversations",
joinColumns = #JoinColumn(name="conversation_id", nullable = false, updateable = false),
inverseJoinColumns = #JoinColumn(name = "user_id", nullable = false, updateable = false)
)
private Set<User> users;
// Ctors, getters & setters down here...
}
#Entity
#Table(name = "users")
public class User extends BaseEntity {
#NotNull
private String username;
#ManyToMany(mappedBy = "users")
// this is the non-owning side of the relationship; the join table mapping
// is declared on the other side
private Set<Conversation> conversations;
// Ctors, getters & setters down here...
}
Note in that case that User and Conversation entities are directly associated in the object model.
On the other hand, if you did choose to model users_x_conversations via an entity of its own, then the code you present for it is all wrong. It would look more like this:
#Entity
#Table(name = "users_x_converations", uniqueConstraints =
#UniqueConstraint(columnNames={"converation_id", "user_id"}))
public class UserConversation extends BaseEntity {
#ManyToOne(optional = false)
#JoinColumn(name = "conversation_id", nullable = false, updatable = false)
Conversation conversation;
#ManyToOne(optional = false)
#JoinColumn(name = "user_id", nullable = false, updatable = false)
User user;
// Ctors, getters & setters down here...
}
Note well that:
This makes the object-level association between Conversations and Users indirect, via UserConversation entities. If the relationships are navigable from the other side, then they would be modelled via #OneToMany relationship fields of type Set<UserConversation> or List<UserConversation>.
It requires more code, and more objects in the system at runtime.
On the other hand, it does have the minor advantage of saving you from making a somewhat arbitrary choice of which side of a direct #ManyToMany relationship is the owning side.

Hibernate reloading entities in a fetch join

I am having a problem with Hibernate reloading the entities in a query even though they are being fetched as part of the main query.
The entities are as follows (simplified)
class Data {
#Id
String guid;
#ManyToOne(fetch = FetchType.LAZY)
#NotFound(action = NotFoundAction.IGNORE)
DataContents contents;
}
class DataClosure {
#Id
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "ancestor_id", nullable = false)
private Data ancestor;
#Id
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "descendant_id", nullable = false)
private Data descendant;
private int length;
}
This is modelling a closure table of parent / child relationships.
I have set up some criteria as follows
final Criteria criteria = getSession()
.createCriteria(DataClosure.class, "dc");
criteria.createAlias("dc", "a");
criteria.createAlias("dc.descendant", "d");
criteria.setFetchMode("a", FetchMode.JOIN);
criteria.setFetchMode("d", FetchMode.JOIN);
criteria.add(Restrictions.eq("d.metadataGuid",guidParameter));
criteria.add(Restrictions.ne("a.metadataGuid",guidParameter));
This results in the following SQL query
select
this_.descendant_id as descenda2_21_2_,
this_.ancestor_id as ancestor3_21_2_,
this_.length as length1_21_2_,
d2_.guid as metadata1_20_0_,
d2_.name as name5_20_0_,
a1_.guid as metadata1_20_1_,
a1_.name as name6_20_1_
from
data_closure this_
inner join
data d2_
on this_.descendant_id=d2_.metadata_guid
inner join
data a1_
on this_.ancestor_id=a1_.metadata_guid
where
d2_.guid=?
and a1_.guid<>?
which looks like it is correctly implementing the join fetch. However when I execute
List list = criteria.list();
I see one of these entries in the SQL log per row in the result set
Result set row: 0
DEBUG Loader - Loading entity: [Data#testGuid19]
DEBUG SQL -
select
data0_.guid as guid1_20_0_,
data0_.title as title5_20_0_,
from
data data0_
where
data0_.guid=?
Hibernate:
(omitted)
DEBUG Loader - Result set row: 0
DEBUG Loader - Result row: EntityKey[Data#testGuid19]
DEBUG TwoPhaseLoad - Resolving associations for [Data#testGuid19]
DEBUG Loader - Loading entity: [DataContents#7F1134F890A446BBB47F3EB64C1CF668]
DEBUG SQL -
select
dataContents0_.guid as guid_12_0_,
dataContents0_.isoCreationDate as isoCreat2_12_0_,
from
dataContents dataContents0_
where
dataContents0_.guid=?
Hibernate:
(omitted)
It is looks like even though the DataContents is marked as lazily loaded, it's being loaded eagerly.
So I either want some way in my query to fetch join DataClosure and Data and lazily fetch DataContents, or to fetch join the DataContents if that is not possible.
Edit:
Modelling the closure table like this
class DataClosure {
#Id
#Column(name = "ancestor_id", nullable = false, length =36 )
private String ancestorId;
#Id
#Column(name = "descendant_id", nullable = false, length =36 )
private String descendantId;
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "ancestor_id", nullable = false)
private Data ancestor;
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
#JoinColumn(name = "descendant_id", nullable = false)
private Data descendant;
private int length;
}
fixed the problem. In other words, having #Id annotation on entities from other tables seemed to cause the issue, even though there was nothing wrong with the queries generated.
I think your problem here might be this
#NotFound(action = NotFoundAction.IGNORE)
There are plenty of google results where using that causes the lazy loading to become eager. I think it is a bug in Hibernate.
Adding this to the list of annotations should fix the problem
#LazyToOne(value=LazyToOneOption.NO_PROXY)
Since that informs Hibernate that you will not try to use that property later on so no proxy is required.

Java hibernate: Update foreign key in manytoone relationship automatically

I have a problem with the auto update of foreign keys which appears as following:
I have a two tables HamKeyword and HamKeywordAlias. One entry in the hamKeyword has 0…n entries in HamKeywordAlias. This relationship is reflected with a foreign key field in the HamKeywordAlias table. Both tables have their own primary keys. I defined the two tables using reverse engineering of hibernate eclipse tools as follows:
#Entity
#Table(name = "HAM_KEYWORDS")
public class HamKeywords implements java.io.Serializable {
private long keywordid;
private String keyword;
…
#Id
#GenericGenerator(name="gen",strategy="increment")
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "KEYWORDID", unique = true)
public long getKeywordid() {
return this.keywordid;
}
…
#OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL, mappedBy = "hamKeywords")
public Set<HamKeywordsAlias> getHamKeywordsAliases() {
return this.hamKeywordsAliases;
}
#Entity
#Table(name = "HAM_KEYWORDS_ALIAS", schema = "dbo", catalog = "ham")
public class HamKeywordsAlias implements java.io.Serializable {
#Id
#GenericGenerator(name="gen",strategy="increment")
#GeneratedValue(generator="gen")
#Column(name = "ALIASID", unique = true, nullable = false)
public long getAliasid() {
return this.aliasid;
}
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "KEYWORDID", nullable = false, updatable = false, insertable = true)
public HamKeywords getHamKeywords() {
return this.hamKeywords;
}
Now to my problem. I try to add a new entry to HamKeyword with 1 new related HamKeywordAlias:
HamKeywords hkw = new HamKeywords();
HamKeywordsAlias hka = new HamKeywordsAlias();
hka.setAlias("new alias");
hkw.setHamKeywordsAliases(new HashSet<HamKeywordsAlias>());
Set<HamKeywordsAlias> hkaS = hkw.getHamKeywordsAliases();
hkaS.add(hka);
hkw.setHamKeywordsAliases(hkaS);
session.flush();
session.save(hkw);
session.getTransaction().commit();
This code fails with the error message:
ERROR: The value NULL can not be inserted in table 'KEYWORDID'-Spalte, 'ham.dbo.HAM_KEYWORDS'. No NULL allowed for INSERT. Exception in thread "main" org.hibernate.exception.ConstraintViolationException: could not execute statement
(Please note that I translated the error message into english, it might be a bit different languagewise)
Obviously, the foreign key in field KEYWORDID of the HamKeywordAlias table is not be updated. I double checked this by removing the NOT NULL constraint. What happens is, that the enty into the ALIAS table is inserted but with a NULL in the field keywordid.
I tested furthermore adding manually rows into the HamKeywordAlias table. Retrieving an entry of the HamKeyword table and retrieving the related Aliases with following code works great:
HamKeywords hamCurrentKeyword = (HamKeywords) session.get(HamKeywords.class, (long)1);
hamCurrentKeyword.getHamKeywordsAliases();
Thus I assume that I defined the many to one relation correctly. However, the foreign key is not updated automatically.
Can you assist me why this is not be done?
Thanks
Felix
You have a bidirectional OneToMany association. The owner of the association is the Many side: HamKeywordsAlias.hamKeywords. That's the side that Hibernate cares about. But you didn't initialize it. You added an alias to the keywords' collection of aliases, but failed to set the keywords of the alias:
hka.setHamKeywords(hkw);

Hibernate Spring: #ManyToMany DataIntegrityViolationException ConstraintViolationException

I am building a sample for ManyToMany relationship between: User(1) - ()AccessLevel() - (1)Role
I have implemented 3 classes in Java with hibernate implementation as follow:
Class User
#Entity
#Table(name="user")
public class User {
#Id
#GeneratedValue
#Column(name="USER_ID")
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "access_level", joinColumns = {
#JoinColumn(name = "user_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "role_id", nullable = false, updatable = false) })
private Set<Role> roles = new HashSet<Role>(0);
Class Role
#Entity
#Table(name="role")
public class Role {
#Id
#GeneratedValue
#Column(name="role_id")
private Integer id;
#Column(name="role_name")
private String roleName;
Class AccessLevel
#Entity
#Table(name="access_level")
public class AccessLevel {
#Id
#GeneratedValue
private Integer id;
#Column(name="role_id")
private Integer roleId;
#Column(name="user_id")
private Integer userId;
Problem:
When I am persisting the User bean using merge method then an exception arise:
#Service
public class UserServiceImpl implements UserService {
#PersistenceContext
private EntityManager em;
#Override
#Transactional
public void save(User user) {
em.merge(user);
}
Exception
org.springframework.web.util.NestedServletException: Request process
ing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: Could not execute JDBC batch update; SQL [insert into access_level (user_id, role_id) values (?, ?)]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:894)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
javax.servlet.http.HttpServlet.service(HttpServlet.java:641)
javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
As you can see hibernate is trying to run this query:
insert into access_level (user_id, role_id) values (?, ?)
From my point of view it seems like hibernate is not generating the primary key for AccessLevel even though I have added the #GeneratedValue to the id attribute.
Note:
I am working on production environment with Postgresql and evelopment environment with HSQL database that creates all schemas from the begining based on the entities description. Both environments generate same issue.
Regards,
Cristian Colorado
Reason:
It seems for ManyToMany relationships you do not need to define a class for the "Joining Table". Therefore if I eliminate AccessLevel entity the logic would work perfectly fine. I explain further:
Explanation:
When I defined the User class I also described the relationship with Role:
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "access_level", joinColumns = {
#JoinColumn(name = "user_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "role_id", nullable = false, updatable = false) })
private Set<Role> roles = new HashSet<Role>(0);
Important thing here is I have told hibernate that User entity will relate to Role entity through a table known as "access_level" and such table will have user_id and role_id columns in order to join previous entities.
So far this is all hibernate needs in order to work the many to many relationship, therefore when mergin it uses that information to create and tun this script:
insert into access_level (user_id, role_id) values (?, ?)
Now, the problem cames when I defined a new entity for AccessLevel:
#Entity
#Table(name="access_level")
public class AccessLevel {
#Id
#GeneratedValue
private Integer id;
#Column(name="role_id")
private Integer roleId;
#Column(name="user_id")
private Integer userId;
Now I am telling hibernate that there is a table "access_level" related to AccessLevel entity and it has 3 columns, the most important would be Id which is primary key.
So I defined "access_level" twice!
Solution:
I eliminated the Entity for access_level table.
I re-write my production script in order to have "access_level" with
user_id/role_id columns only.
Note: It would be good to know how to add a primary key to the joining table without generating issues. An alternative would be adding a composed primary key in database(user_id/role_id) which would be independient from hibernate.
Why do you need a PK column in the join table? There will be a composite PK composed of user_id and role_id. Now, as you have discovered a JoinTable for #ManyToMany will only ever have two columns and at some point you may require additional data about this relationship.
e.g.
user_id
role_id
date_granted
You may then want to use your AccessLevel entity however you replace the #ManyToMany with #OneToMany from User to AccessLevel and optionally from Role > AccessLevel.
The Hibernate docs themselves advise against #ManyToMany:
Do not use exotic association mappings:
Practical test cases for real many-to-many associations are rare. Most
of the time you need additional information stored in the "link
table". In this case, it is much better to use two one-to-many
associations to an intermediate link class. In fact, most associations
are one-to-many and many-to-one. For this reason, you should proceed
cautiously when using any other association style.

Categories

Resources