ebean unidirectional #OneToOne relation with unique constraint - jpa

I have a User class:
#Entity
public class User extends Model {
#Id
public Long id;
public String email;
public String name;
public String password;
}
and a driver class
#Entity
public class Driver extends Model {
#Id
public Long id;
#OneToOne (cascade = CascadeType.ALL)
#Column(unique = true)
public User user;
}
I want to make sure that the user_id is unique inside the Drivers table. But the code above does not enforce that. (I can create multiple drivers with the same user id).
Ideally, I do not want to add the #OneToOne relations in the User class because there are several different roles inside my app (e.g. driver, teacher, agent etc.) and I don't want to pollute user class with all those relations.
How can I achieve this?

I have tried this code on the model for me, and it worked. One thing to be noted, that you must use #OneToOne annotation to let the ORM knows that you have foreign key reference to other model.
The model look like following:
#Entity
// add unique constraint to user_id column
#Table(name = "driver",
uniqueConstraints = #UniqueConstraint(columnNames = "user_id")
)
public class Driver extends Model {
#Id
public Long id;
#OneToOne
#JoinColumn(name = "user_id")
public User user;
}
It will generate evolution script like this :
create table driver (
id bigint not null,
user_id bigint,
constraint uq_driver_1 unique (user_id), # unique database constraint
constraint pk_driver primary key (id)
);
So, with this method you can make sure that you will have unique user reference on driver table.
Additional Info
Because there is an additional constraint, that is not handled by framework but by the database applied on the model (such as the unique constraint), to validate the input or handling the occurred exception, you can surround Model.save() or form.get().save() expression (saving-the-model) with try-catch block to handle the PersistenceException.

Related

Fetch Entity Reference by Non-Primary Key Column

I have two entities, "User" and "Record", where the Record references a User but not by the User's primary key, but rather another column that is also unique, namely the username:
#Entity
public class User implements Serializable {
#Id
private Long id;
#NaturalId
private String username;
...
}
#Entity
public class Record {
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "username", referencedColumnName = "username")
private User user;
...
}
When creating a new Record, using Hibernate's getReferenceById does not work:
#Transactional
public Record createRecord(Long userId) {
Record record = new Record()
record.setUser(userRepository.getReferenceById(userId));
return recordRepository.save(record);
}
The not-null constraint on the username column of the Record table is violated since the username is not loaded. This does make sense since the getReferenceById method of the JpaRepository interface just returns a proxy and would not return the username. Using the findById method solves this problem, but executes an additional query that I would like to avoid:
#Transactional
public Record createRecord(Long userId) {
Record record = new Record()
record.setUser(userRepository.findById(userId).orElseThrow(RuntimeException::new);
return recordRepository.save(record);
}
Is it possible to fetch an entity reference via a "natural ID" or another unique column?
Additional things to note:
The database schema is managed by Flyway, even in the test context.
Yes, I could just use the numeric ID as the foreign key reference, but I would like to instead use the username.
I know that I could also forgo using the #ManyToOne relationship all together and just use the username in the record class, but I am more interested in the general possibility of leveraging such unique non-primary key columns with Hibernate/Spring Data JPA to the same extent that IDs can be used.

Entity without a constraint required

I have an entity Person with a relation to an other person (mentor). This person can be null. I thought just the Constraints.Required annotation forces my mentor to be set.
If I remove the ManyToOne annotation the mentor wont be connected.
#Entity
public class Person extends Model {
#Id
#Constraints.Required
#Formats.NonEmpty
public Integer id;
#ManyToOne
#Constraints.Required
public User user;
#Constraints.Required
public String firstName;
#Constraints.Required
public String lastName;
#ManyToOne
public Person mentor;
...
How can I have a person without a mentor?
The usage of the #ManyToOne annotation is actually "instructing" your JPA provider th think that the Person table/relation has a foreign key to it (this models the 1:n relation b/n persons to mentors and a foreign key can't be null) but from your question, I see your biz needs doesn't need a 1:n per se so simply remove the #ManyToOne annotation and on case a person does have a mentor , wire this relation manually in the JPA entity constructor or via setter method

Cannot change colum names in JPA Eclipselink

I'm having problems making #Column(name="example") working.
I've got a User class:
#Entity
public class User {
#Id
private String username;
....
}
A Role one:
#Entity
public class Role {
#Id
private String name;
....
}
That are in a ManyToMany relationship. So I created a RoleMembership:
#Entity
#IdClass(UserRolePK.class)
public class RoleMembership {
#Id
#ManyToOne
#PrimaryKeyJoinColumn(name="USERNAME")
private User user;
#Id
#ManyToOne
#PrimaryKeyJoinColumn(name="ROLE")
private Role role;
....
}
As you can see, the primary key is defined in UserRolePK:
public class UserRolePK{
#Id
#Column(name="ROLE")
private String role;
#Id
#Column(name="USERNAME")
private String user;
...
}
In this class I use #Column(name="USERNAME") and #Column(name="ROLE") to force its name to that string, but it's not working: JPA gives it the default names USER_USERNAME and ROLE_NAME (that are in TABLE_ID format).
Can anyone help me finding the mistake?
Thanks,
Andrea
EDIT:
I need to have three tables:
user (username (pk), password ....)
user_role (username, role_name)
role (name (pk), description)
I cannot change User definition in my model.
Remove all the annotations in UserRolePK and change your #PrimaryKeyJoinColumn annotations to #JoinColumn annotations.
Your usage of PrimaryKeyJoinColumn does not seem to make sense. I think you should be using just a #JoinColumn, or are you also mapping the columns as basics?
Perhaps include you complete class, and the DDL that is generated.

EclipseLink: is it possible to add #OneToMany relationship to class on an #EmbeddedId property as foreign key?

I want to do a Jaas login module using JPA to store my AuthUser and AuthUserRole. I'll focus on the JPA side on this question.
Here is what I would do in the Database (not at all legitimate SQL code but hopefully comprehensive):
TABLE AuthUser( INT uid, VARCHAR password )
PRIMARY KEY (uid)
TABLE AuthUserRole( INT uid, INT role )
PRIMARY KEY (uid , role)
FOREIGN KEY (uid) REFERENCE AuthUser.uid
It makes sense to me, one role can only be assigned to a user once.
Here is what I attempted to do in Java, not showing username and email in AuthUser:
#Entity
public class AuthUser {
#Id
private int uid;
private String password;
#OneToMany
private Set<AuthUserRole> roles;
}
#Entity
public class AuthUserRole {
#Embeddedid
private AuthUserRolePK authUserRolePK;
}
#Embeddable
public class AuthUserRolePK {
public int uid;
public int role;
}
Eclipselink does the mapping just fine, which means it works, but not the way I wanted. It makes a third table named *authuser_authuserrole* that holds the (AuthUser_uid , uid, role) columns. No need to mention AuthUser_uid and uid is identical.
The trivial answer would be:
#Entity
public class AuthUser {
#Id
private int uid;
private String password;
#OneToMany
#JoinColumn(name="authUserRolePK.uid", referencedColumnName="uid")
private Set<AuthUserRole> roles;
}
But EclipseLink cryes that when #Embeddedid is used, all primary key columns have to be mentioned.
Q: How do I make Eclipselink map my entities like the database schema I mentioned? Should I rather use #IdClass? I could see the result of a database --> entities mapping but that's too easy :)
Thank you for your answers!
Three tables is the typical way to do this actually, your approach is simply lacking a little bit of JPA finesse.
Typically you have three tables associated as follows:
AuthUser AuthUser_Role (association) Role
frank ---- frank,admin ----- admin
This is in fact what Eclipselink was trying to map for you, but in general, you don't create the AuthUser_Role mapping yourself. Instead, you create a field on AuthUser like:
#ManyToMany
Set<Roles> userRoles
And (optionally) on Role like:
#ManyToMany(mappedBy="userRoles")
Set<AuthUser> usersWithRole;
Then EclipseLink takes care of the join table for you, and all you need to worry about is the userRoles field, which will update the join.
You can extend this to create the join manually for say roles that start and end on a set date, etc, but for all but the most complex projects, that's usually not necessary, and can often be accomplished in a different way. For compliance purposes, it's easier to use one of the ELUG extensions to keep and access a history of changes for example, which is the most common reason I've seen for adding extra meta-data to the join information.
Alternatively, if you REALLY want to do this with only two tables, make the AuthUserRole the primary side, and the AuthUser the passive side.
#Entity
public class AuthUser {
#Id
private int uid;
private String password;
#OneToMany(mappedBy="user")
private Set<AuthUserRole> roles;
}
#Entity
public class AuthUserRole {
#Embeddedid
private AuthUserRolePK authUserRolePK;
}
#Embeddable
public class AuthUserRolePK {
public AuthUser user;
public int role;
}
In this arrangement, you will end up with only two tables, and the "user" field will indeed be equal to the uid of AuthUser in the database, it just shows up as an object on the Java side.

The type of field isn't supported by declared persistence strategy "OneToMany"

We are new to JPA and trying to setup a very simple one to many relationship where a pojo called Message can have a list of integer group id's defined by a join table called GROUP_ASSOC. Here is the DDL:
CREATE TABLE "APP"."MESSAGE" (
"MESSAGE_ID" INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1)
);
ALTER TABLE "APP"."MESSAGE" ADD CONSTRAINT "MESSAGE_PK" PRIMARY KEY ("MESSAGE_ID");
CREATE TABLE "APP"."GROUP_ASSOC" (
"GROUP_ID" INTEGER NOT NULL,
"MESSAGE_ID" INTEGER NOT NULL
);
ALTER TABLE "APP"."GROUP_ASSOC" ADD CONSTRAINT "GROUP_ASSOC_PK" PRIMARY KEY ("MESSAGE_ID", "GROUP_ID");
ALTER TABLE "APP"."GROUP_ASSOC" ADD CONSTRAINT "GROUP_ASSOC_FK" FOREIGN KEY ("MESSAGE_ID")
REFERENCES "APP"."MESSAGE" ("MESSAGE_ID");
Here is the pojo:
#Entity
#Table(name = "MESSAGE")
public class Message {
#Id
#Column(name = "MESSAGE_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int messageId;
#OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
private List groupIds;
public int getMessageId() {
return messageId;
}
public void setMessageId(int messageId) {
this.messageId = messageId;
}
public List getGroupIds() {
return groupIds;
}
public void setGroupIds(List groupIds) {
this.groupIds = groupIds;
}
}
I know this is wrong as there is no #Column mapping to GROUP_ASSOC.GROUP_ID for the groupIds property, but hopefully this illustrates what we are trying to do. When we run the following test code we get <openjpa-1.2.3-SNAPSHOT-r422266:907835 fatal user error> org.apache.openjpa.util.MetaDataException: The type of field "pojo.Message.groupIds" isn't supported by declared persistence strategy "OneToMany". Please choose a different strategy.
Message msg = new Message();
List groups = new ArrayList();
groups.add(101);
groups.add(102);
EntityManager em = Persistence.createEntityManagerFactory("TestDBWeb").createEntityManager();
em.getTransaction().begin();
em.persist(msg);
em.getTransaction().commit();
Help!
When you are working with JPA, you should think Object and relations between Objects and you should map your Object model, not ids, to your relational model (it is possible to map a List of basic values with #ElementCollection in JPA 2.0 though but what I said just before still applies).
Here, (assuming this really is a one-to-many relation between Message and GroupAssoc and not a many-to-many relation between Message and Group entities) you should have something like this:
#Entity
#Table(name = "MESSAGE")
public class Message implements Serializable {
#Id
#Column(name = "MESSAGE_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long messageId;
#OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
private List<GroupAssoc> groupAssocs = new ArrayList<GroupAssoc>();
public Long getMessageId() {
return messageId;
}
public void setMessageId(Long messageId) {
this.messageId = messageId;
}
public List<GroupAssoc> getGroupAssocs() {
return groupAssocs;
}
public void setGroupAssocs(List<GroupAssoc> groupAssocs) {
this.groupAssocs = groupAssocs;
}
// equals() and hashCode()
}
And another entity for GroupAssoc.
PS: Your DDL really looks like a (M:N) relation between MESSAGE and GROUP (or I don't understand the PK constraint of GROUP_ASSOC) but you didn't show any FK constraint on GROUP_ID so I'm not 100% sure. But if that's the case, then you should use an #ManyToMany instead of #OneToMany.