spring-data-jpa 3-way-intersection table - java

I'll try to illustrate what I'm trying to achieve shortly...
Let's suppose I have a users table:
USER_INFO
USER_ID [PK]
USER_NAME
PASSWORD
an intersection table to define connections for each user (N:M - ManyToMany)
CONNECTION_INFO
CONNECTION_ID [PK]
USER_A_ID [FK - references USER_INFO(USER_ID)]
USER_B_ID [FK - references USER_INFO(USER_ID)]
CONNECTION_TYPE_ID [FK - references CONNECTION_TYPE(CONNECTION_TYPE_ID)]
The CONNECTION_TYPE is simple as:
CONNECTION_TYPE
CONNECTION_TYPE_ID [PK]
CONNECTION_TYPE_NAME [CHECK allowed values are: FRIEND, FAMILY, ...]
On Spring side I defined my User entity as:
#Entity
#Table(name = "USER_INFO")
public class User implements Serializable {
#Id
#NotNull
#Column(name = "USER_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer userId;
#Column(name = "USER_NAME)
private String userName;
#Column(name = "PASSWORD)
private char[] password;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "CONNECTION_INFO",
joinColumns = { #JoinColumn(name = "USER_A_ID") },
inverseJoinColumns = { #JoinColumn(name = "USER_B_ID") })
private List<User> connections;
// ctor, getters, setters, toString, ...
}
I have a UserRepository interface that extends JpaRepository etc etc. Now, this works perfectly and I can retrieve all connections be it FRIEND, FAMILY, MOST_HATED_PERSONS, BLOCKED, DEMON, etc...
I tried to integrate the ConnectionType too in the picture however...
#Entity
#Table(name = "CONNECTION_TYPE")
public class Connection implements Serializable {
public static enum Types {
FRIEND, FAMILY, BLOCKED, ...
}
#Id
#NotNull
#Column(name = "CONNECTION_TYPE_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer connectionTypeId;
#Column(name = "CONNECTION_TYPE_NAME")
private ConnectionType connectionType;
// ctor, getters, setter, etc
}
Now, my question is, how can I get only specific connections for a given user, based on Connection.Types? For example I want to find only FRIENDs, or only FAMILY I think you get my point. This 3 way intersection table gives me one of a headache.
#Clarification:
What I want is a #ManyToMany relation defined on my User entity that happen to have extra column. I know in that case there are proposed solutions like LINK. In my case this extra column is a foreign key to a third table (USER_INFO(Holds the users), CONNECTION_INFO(Holds the connections between users N:M + an info on the type of connection), CONNECTION_TYPE. If I can model it with spring-data-jpa from what I understand I only need a magic named method under UserRepository, something like (totally incorrect):
public interface UserRepository extends JpaRepository<User, Integer> {
List<User> findUserFriendsByConnectionType(User userWhoseFriendsWeAreSearching, String connectionTypeFromTheThirdTable);
}
That's all I want. I know it's simple with a normal extra column by creating an entity for the intersection table too and break the ManyToMany to OneToMany and ManyToOne, it just happens I have a third table and a possibly ManyToOne (1 connection can have 1 associated type, while a type can be linked to any number of connections) on the intersection entity with the connection_type table.
I hope it clears everything up. The above are just a sample I never imagined we'd hang up on an enum because I wanted to make it look simple I possibly made it way too simple perhaps :).

I managed to solve the problem but I'm not sure if this is the right way to do it. Anyway here's my solution. Consider the following 3 tables:
create table USER_INFO (
USER_ID int not null primary key,
USER_NAME varchar(16),
PASSWORD varchar(64)
);
create table CONNECTION_TYPE (
CONNECTION_TYPE_ID int not null primary key,
CONNECTION_TYPE_NAME varchar(16) not null,
CONNECTION_TYPE_DESCRIPTION varchar(128),
unique (CONNECTION_TYPE_NAME)
);
create table CONNECTION (
CONNECTION_ID int not null primary key,
CONNECTION_TYPE_ID int,
RELATED_USER_ID int,
RELATING_USER_ID int,
foreign key (CONNECTION_TYPE_ID) references CONNECTION_TYPE(CONNECTION_TYPE_ID),
foreign key (RELATED_USER_ID) references USER_INFO(USER_ID),
foreign key (RELATING_USER_ID) references USER_INFO(USER_ID)
With the above 3 tables, I want to provide a functionality to get connections for any given user based on the connection's type. For this I created 3 entities as follows:
#Entity
#Table(name = "CONNECTION_TYPE")
public class ConnectionType implements Serializable {
#Id
#NotNull
#Column(name = "CONNECTION_TYPE_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer connectionTypeId;
#NotNull
#Column(name = "CONNECTION_TYPE_NAME", unique = true)
private String connectionTypeName;
#Column(name = "CONNECTION_TYPE_DESCRIPTION")
private String connectionTypeDescription;
...
}
Nothing particularly interesting in here, I omitted the constructor, getters, setters etc and from the ConnectionType I don't want to have a mapping for all connections for this type so that direction is not present.
#Entity
#Table(name = "CONNECTION")
public class Connection implements Serializable {
#Id
#NotNull
#Column(name = "CONNECTION_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer connectionId;
#NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CONNECTION_TYPE_ID", referencedColumnName = "CONNECTION_TYPE_ID")
private ConnectionType connectionType;
#NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "RELATED_USER_ID", referencedColumnName = "USER_ID")
private User relatedUser;
#NotNull
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "RELATING_USER_ID", referencedColumnName = "USER_ID")
private User relatingUser;
...
}
This one is more interesting if for noone else at least for me. This would be my intersection table entity. There's the uni-directional mapping for the used ConnectionType with ManyToOne as one Connection can have exactly one ConnectionType while the same ConnectionType can be reused for an arbitrary number of Connections.
The other 2 User mappings I'm sure I've messed up but before that here's the User entity:
#Entity
#Table(name = "USER_INFO")
public class User implements Serializable {
#Id
#NotNull
#Column(name = "USER_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer userId;
#NotNull
#Column(name = "USER_NAME")
private String userName;
#NotNull
#Column(name = "PASSWORD")
private char[] password;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "relatedUser", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Connection> connections;
}
Now here I'm even more sure I completely messed up, but I'll show the actual error. My repository is simple as a brick:
#Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}
And I have a UserService with a simplified findAllConnectionsForUserById function:
#Service
public interface UserService {
List<User> findAllConnectionsForUserById(Integer userId);
}
The method implementation is simple enough:
#Override
#Transactional
public List<User> findAllConnectionsForUserById(Integer userId) {
Optional<User> _user = userRepository.findById(userId);
// omitted exception handling...
User user = _user.get();
List<Connection> connections = user.getConnections();
return connections.strea.map(Connection::getRelatingUser).collect(Collectors.toList());
This way it seem to work fine for the simple case and in case I take the ConnectionType too:
connections.stream().filter(c -> c.getConnectionType().getConnectionTypeName().equals("FRIEND")).map(Connection::getRelatingUser).collect(Collectors.toList());
it seem to work as well. Again, not sure if this is the right way but at least it does the job.

Related

Java hibernate OneToOne: which side should be the owner

So i'm learning from these simple examples, there're 2 tables, USERS and USER_DETAILS, simple enough, each user has user_details and it's 1-to-1 relationship. So this sample is like this,
#Entity
#Table(name = "USERS")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "USR_ID")
private long id;
#Column(name = "USERNAME", nullable = false, unique = true)
private String username;
#Column(name = "PASSWORD")
private String password;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "user", fetch = FetchType.LAZY)
private UserDetail userDetail;
//Setter and getter methods
}
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "USR_DET_ID")
private long id;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "EMAIL")
private String email;
#Column(name = "DBO")
private LocalDate dob;
#OneToOne(fetch=FetchType.LAZY)
#JoinColumn(name = "USR_ID")
private User user;
//Setter and Getter methods
}
If you look at mappedBy, it's in the User not UserDetails.
Q1: so USER is the owner, if it calls save(),
USER_DETAILS table will be updated as well ?
Q2: same examples put mappedBy in the USER_DETAILS side,
why people want to do this ?
How to determine which side to put mappedBy ?
Thanks for your help !
Q2: same examples put mappedBy in the USER_DETAILS side,
why people want to do this ?
How to determine which side to put mappedBy ?
In a bidirectional relationship, each entity has a relationship field
or property that refers to the other entity. Through the relationship
field or property, an entity class’s code can access its related
object. If an entity has a related field, the entity is said to “know”
about its related object.
There is a bidirectional one-to-one relationship in your example. Both User and UserDetail entities have a relationship field. #OneToOne annotation specified on both the entities.
For one-to-one bidirectional relationships, the owning side
corresponds to the side that contains the corresponding foreign key.
The owner of the relationship is UserDetail entity. The owner has #JoinColumn annotation to specify foreign key (USR_ID).
Inverse side of relationship (User) has mappedBy attribute.
Q1: so USER is the owner, if it calls save(),
USER_DETAILS table will be updated as well ?
In your example UserDetail is the owner. Therefore the saving process:
User user = new User(); // Ignoring the constructor parameters...
UserDetail userDetail = new UserDetail();
user.setUserDetail(userDetail);
userDetail.setUser(user);
userRepository.save(user);
You only need to save the parent. It will save the child as well.

JPA: How to handle versioned entities?

I have a versioning on an entity as part of its primary key. The versioning is done via a timestamp of the last modification:
#Entity
#Table(name = "USERS")
#IdClass(CompositeKey.class)
public class User {
#Column(nullable = false)
private String name;
#Id
#Column(name = "ID", nullable = false)
private UUID id;
#Id
#Column(name = "LAST_MODIFIED", nullable = false)
private LocalDateTime lastModified;
// Constructors, Getters, Setters, ...
}
/**
* This class is needed for using the composite key.
*/
public class CompositeKey {
private UUID id;
private LocalDateTime lastModified;
}
The UUID is translated automatically into a String for the database and back for the model. The same goes for the LocalDateTime. It gets automatically translated to a Timestamp and back.
A key requirement of my application is: The data may never update or be deleted, therefore any update will result in a new entry with a younger lastModified. This requirement is satisfied with the above code and works fine until this point.
Now comes the problematic part: I want another object to reference on a User. Due to versioning, that would include the lastModified field, because it is part of the primary key. This yields a problem, because the reference might obsolete pretty fast.
A way to go might be depending on the id of the User. But if I try this, JPA tells me, that I like to access a field, which is not an Entity:
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#Column(nullable = false)
private UUID id;
#OneToOne(optional = false)
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private UUID userId;
#Column(nullable = false)
private boolean married;
// Constructors, Getter, Setter, ...
}
What would be the proper way of solving my dilemma?
Edit
I got a suggestion by JimmyB which I tried and failed too. I added the failing code here:
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#Column(nullable = false)
private UUID id;
#OneToMany
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private List<User> users;
#Column(nullable = false)
private boolean married;
public User getUser() {
return users.stream().reduce((a, b) -> {
if (a.getLastModified().isAfter(b.getLastModified())) {
return a;
}
return b;
}).orElseThrow(() -> new IllegalStateException("User detail is detached from a User."));
}
// Constructors, Getter, Setter, ...
}
What you seem to require seems to be on the lines of a history table, to keep track of the changes. See https://wiki.eclipse.org/EclipseLink/Examples/JPA/History on how EclipseLink can handle this for you while using normal/traditional JPA mappings and usage.
What you have here is a logical 1:1 relationship which, due to versioning, becomes a technical 1:n relationship.
You have basically three options:
Clean JPA way: Declare an 'inverse' #ManyToOne relationship from user to the "other object" and make sure you always handle it whenever a new User record is created.
'Hack-ish' way: Declare a #OneToMany relationship in the "other object" and force it to use a specific set of columns for the join using #JoinColumn. The problem with this is that JPA always expects unique reference over the join columns so that reading the UserDetail plus referenced User records should work, whereas writing UserDetail should not cascade onto User to avoid unwanted/undocumented effects.
Just store the user's UUID in the "other object" and resolve the reference yourself whenever you need it.
The added code in your question is wrong:
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private UUID userId;
More correct, albeit not with the result you want, would be
#JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private User user;
This won't work though, because, as I said above, you may have more than one user record per UserDetail, so you'd need a #OneToMany relationship here, represented by a Collection<User>.
Another 'clean' solution is to introduce an artificial entity with a 1:1 cardinality w.r.t. to the logical User to which you can refer, like
#Entity
public class UserId {
#Id
private UUID id;
#OneToMany(mappedBy="userId")
private List<User> users;
#OneToOne(mappedBy="userId")
private UserDetail detail;
}
#Entity
public class User {
#Id
private Long _id;
#ManyToOne
private UserId userId;
}
#Entity
public class UserDetail {
#OneToOne
private UserId userId;
}
This way, you can somewhat easily navigate from users to details and back.
I came to a solution, that is not really satisfying, but works. I created a UUID field userId, which is not bound to an Entity and made sure, it is set only in the constructor.
#Entity
#Table(name = "USER_DETAILS")
public class UserDetail {
#Id
#Column(nullable = false)
private UUID id;
#Column(nullable = false)
// no setter for this field
private UUID userId;
#Column(nullable = false)
private boolean married;
public UserDetail(User user, boolean isMarried) {
this.id = UUID.randomUUID();
this.userId = user.getId();
this.married = isMarried;
}
// Constructors, Getters, Setters, ...
}
I dislike the fact, that I cannot rely on the database, to synchronize the userId, but as long as I stick to the no setter policy, it should work pretty well.

Many to one Hibernate mapping

So, here'e the situation:
Table ComputerInventory with {computerInventoryID (Primary key), TagID(unique), Name etc}
Table reviewStatus with {reviewStatusID(Primary key), computerInventoryID (ForeignKey), status }
I've written the hibernate Entity for ReviewStatus:
public class ReviewStatus implements Serializable {
public enum reviewStatus {
To_Be_Reviewed,
Reviewed
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long reviewStatusId;
#Column(name = "lastModifiedTime")
private Date lastModifiedTime;
#Column(name = "Comments")
private String comments;
#Enumerated(EnumType.STRING)
#Column(name = "status")
//all above params have gettetrs and setters
private reviewStatus status;
//TODO: Mapping to computerinventory
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "computerInventoryId")
//For one ComputerInventoryID, there can be many review Statuses.
private Set<ReviewStatus> reviewStatusSet;
public long getreviewStatusId() {
return reviewStatusId;
}
My Doubts:
For one ComputerInventoryID, there can be many review Statuses, so do I have a
Set<ReviewStatus> reviewStatusSet
where I return the list of entries in reviewstatus? Sorry, but I don't understand how I can write a get/set for returning and setting the reviews status of a bunch of records.
Your reference from ReviewStatus should be to a ComputerInventory, not to its ID. Hibernate lets you abstract out the details of the primary key (the ID), letting you directly reference from one object to another. You should use an #ManyToOne annotation on your private ComputerInventory computerInventory;.

Hibernate database mapping

Firstly, I am somewhat new with Hibernate. To get to know the technology I am using it in a project. I am trying to map the following database:
Campaign
campaignId(+)
name
Promotion
campaignId(+)
discount(+)
product
message
I've indicated the primary key in both cases with a (+). The 'campaignId' in Promotion is a foreign key to Campaign to model the 1:m mapping (A Campaign has many Promotions). Using annotations I am stuck on how to do this.
I do not really want to add a promotionId in the Promotion table as it makes working with the data cumbersome. This of course, makes the bridging table a bit tricky. I also have problems working with a foreign key that is also part of the primary key.
Is a mapping for this possible at all?
Ok, I got it working. Sort of. Have to check if persistence actually work. I did the following:
#Entity
#Table(name = "CAMPAIGNS")
#Audited
public class CampaignEntity {
private int campaignId;
private String name;
private List<PromotionEntity> promotions;
public CampaignEntity(int campaignId, String name) {
this.campaignId = campaignId;
this.name = name;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "cmp_id")
public int getCampaignId() {
return campaignId;
}
public void setCampaignId(int campaignId) {
this.campaignId = campaignId;
}
// Campaign name here... left out to save space
#OneToMany
#JoinColumn(name = "cmp_id")
public List<PromotionEntity> getPromotions() {
return promotions;
}
public void setPromotions(List<PromotionEntity> promotions) {
this.promotions = promotions;
}
}
Promotion is a vanilla mapping (not using embedded after all), with the fields: campaignId, discount, message. (It also does not have a #ManyToOne annotation.)
Does that make sense?
Lastly, and this will be first prize: as you can see I'm using Envers to audit the whole thing. The above creates a rather ugly "CampaignEntity_PromotionEntity_AUD" table. I understand that it is needed, but how can I rename it to CAMPAIGN_PROMOTION_AUD rather?
Thanks guys!
I got an answer on a lonely website deeply hidden away in far-corners of the Hibernate's Jira error tracking website: https://hibernate.onjira.com/browse/HHH-3729.
The answer is to use #AuditJoinTable(name = "CAMPAIGN_PROMOTION_AUD") of course.
This is a basic example of a one-to-many relationship and its inverse.
public class Campaign
{
#OneToMany(mappedBy = "campaign)
private List<Promotion> promotions;
}
public class Promotion
{
#ManyToOne
private Campaign campaign;
}
You can use an EmbeddedId to create a multi-field PK.
Remove the PK fields from Promotion
Create a separate entity, say PromotionPK, without any annotations except for #Column on the PK fields
In Promotion, include that PK class as field, annotating it using #EmbeddedId, with getters and setters
The FK mapping is as Wouter indicated.
This is what I am now using. It works well and Hibernate handles the PKs of the Promotions for me. Thanks again.
#Entity
#Table(name = "CAMPAIGNS")
#Audited
public class CampaignEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer campaignId;
#Column(name = "name", nullable = false, unique = true)
private String campaignName;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinTable(name = "CAMPAIGN_PROMOTIONS",
joinColumns = { #JoinColumn(name = "campaign_id") },
inverseJoinColumns = { #JoinColumn(name = "promotion_id") })
private Set<PromotionEntity> promotions;
...
}
and then, PromotionEntity:
#Entity
#Table(name = "PROMOTIONS")
#Audited
public class PromotionEntity implements Comparable<PromotionEntity> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "discount", nullable = false)
private Integer discount;
#Column(name = "message", nullable = false)
private String message;
...
}
I also prefer annotating the fields rather than the getters as it is more compact and reads easier.

JPA Compound key with #EmbeddedId

In a legacy database, I have three tables: Users, Workgroups, and UsersWorkgroup. UsersWorkgroup stores what role a user has in a workgroup.
Here are the relevant code snippets:
#Entity
#Table(name = "users_workgroup")
public class UsersWorkgroup implements Serializable {
private static final long serialVersionUID = 1L;
#EmbeddedId
protected UsersWorkgroupPK usersWorkgroupPK;
#JoinColumn(name = "idworkgroup", referencedColumnName = "idworkgroup")
#ManyToOne(optional = false)
private Workgroup workgroup;
#JoinColumn(name = "user_name", referencedColumnName = "user_name")
#ManyToOne(optional = false)
private Users users;
#Column(name = "role")
private Integer role;
#Embeddable
public class UsersWorkgroupPK implements Serializable {
#Basic(optional = false)
#Column(name = "idworkgroup", insertable=false, updatable=false)
private int idworkgroup;
#Basic(optional = false)
#Column(name = "user_name", insertable=false, updatable=false)
private String userName;
#Entity
#Table(name = "workgroup")
public class Workgroup implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "idworkgroup")
private Integer idworkgroup;
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "idworkgroup")
private Collection<UsersWorkgroup> usersWorkgroupCollection;
And of course, problem is, it doesn't work.
Currently I get this exception:
Exception Description: An incompatible
mapping has been encountered between
[class entity.Workgroup] and [class
entity.UsersWorkgroup]. This usually
occurs when the cardinality of a
mapping does not correspond with the
cardinality of its backpointer.
Which I don't understand since OneToMany should match ManyToOne... Or is it a ManyToMany relationship? If I switch to #ManyToMany, I get this:
Exception Description: The target
entity of the relationship attribute
[workgroup] on the class [class
com.ericsson.rsg.ejb.entity.UsersWorkgroup]
cannot be determined. When not using
generics, ensure the target entity is
defined on the relationship mapping.
I'm trying to understand compound keys (embedded), but all the examples I could find have only simple columns that are not foreign keys (but that's the whole point of a compound key, isn't it?). Can the UsersWorkgroup table secretly be a join table?
Should I declare the PK class as a strict POJO class? Or should I put the #JoinColumn annotations in the PK class? How do I refer to the columns within the compound key from another table? Should I initialize the PK object in the refering class constructor, or is it not necessary?
I feel stuck completely.
First of all, I think your relation is a Many To Many, as a user can be in many groups, and a group can have many users (or I would assume so).
Second, as far as I know you have to reference both id_workgroup and user_name as JoinColumns, because they are part of the PK and a unit, so both should be referenced.
Also, I see the "equals" and "hashCode" methods missing from your embedded PK, as well as the getters/setters. I believe they are mandatory.
Your mapping looks fine except for mappedBy - it should be a property name, not a column name:
#OneToMany(cascade = CascadeType.ALL, mappedBy = "workgroup")
private Collection<UsersWorkgroup> usersWorkgroupCollection;

Categories