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.
Related
I have a Project and Employee entities, which has ManyToMany relationship like below.
#Entity
public class Project {
#Id #GeneratedValue
private int projectId;
private String projectName;
// has some additional columns
#ManyToMany(mappedBy = "projects")
private List<Employee> emp = new ArrayList<Employee> ();
....
.....
}
#Entity
public class Employee {
#Id #GeneratedValue
private int id;
private String firstName;
private String lastName;
#ManyToMany(cascade=CascadeType.ALL)
List<Project> projects = new ArrayList<Project> ();
....
....
}
When I use above entities, JPA create a mpping table 'Employee_Project' like below.
create table Employee_Project (emp_id integer not null, projects_projectId integer not null)
My question is, whenever new employee is added, I want to update both employee table and Employee_Project mapping table only, assume I know project id that I would like to map this employee to. (without touching project table/entity, I mean why should I provide complete project object, while saving employee entity alone, how can I do this via jpa?)
You don't need to provide the entire Project object. Use EntityManager.getReference(projectId) or JpaRepository.getOne(projectId).
Those methods will create a proxy object with the appropriate id, rather than loading the entire Project entity from the data store.
EDIT Your service method should look pretty much like the following:
#Transactional
public void createEmployee(Employee employee, Long projectId) {
employee.setProjects(List.of(projectRepository.getOne(projectId));
employeeRepository.save(employee);
}
As a side note, CascadeType.ALL (in particular, because it includes CascadeType.MERGE and CascadeType.REMOVE) doesn't make sense for #ManyToMany. Unless you're planning to create a Project by creating an Employee, CascadeType.PERSIST makes no sense, either.
I am reprograming an application.
The current database has table names like “User” which is a reserved word in the new db so I changed the table name to “NewUser”. I also had to change a few column names. I would like to code it so it imports the new name but changes them immediately in the app back to the reserved word so I don’t have to spend a lot of time re-programming:
Example Code:
#Entity
// NewUser is the new table name but still User below. I would like to keep the user
//as the class name but go after NewUser in the db
public class User implements java.io.Serializable {
private static final long serialVersionUID = -6091824661950209090L;
/** Primary key */
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
// uid is now newuid in the table but again I want to keep uid in the app
//but reference newuid from the db
protected int uid;
public int getUid() {
return this.uid;
}
public void setUid(int uid) {
this.uid = uid;
}
Just add #Table(name = "NewUser") to your entity. It will remap the entity to new table name, but keep User as entity name which is what is used in queries. You will only have to rewrite native queries if you have them, since that is pure SQL. Also, for renaming column names use #Column(name = "newuid").
I am using Spring JPA and want to set value to a foreign key column. Here is my entities and repository.
#Entity
public class Device {
#NotEmpty
#Id
private String deviceId;
#ManyToOne
#JoinColumn(name="userId", referencedColumnName="userId", insertable=false, updatable=false)
#NotFound(action=NotFoundAction.IGNORE)
private User user;
//Getters and setters
}
#Entity
public class User(){
#Id
private String userId;
private String userName;
//Getters and setters
}
public interface DeviceRepository extends PagingAndSortingRepository {
}
public class DeviceServiceImpl implements DeviceService {
#Autowired
private DeviceRepository devRepos;
#Autowired
private UserRepository userRepos;
#Override
public void saveDevice(Device device, String userId) {
User user = null;
if (userId!=null) {
user = userRepos.findOne(userid);
device.setUser(user);
}
deviceRepos.save(device);
}
}
The user exists in Device table but userId column in the table does not set the value. Please help me to fix the problem.
EDIT:
I removed insertable and updatable from the annotation and now it works.
#JoinColumn(name="userId", referencedColumnName="userId")
Then, this means I have to get user of the device from the User table whenever I save a device?
Because you set insertable and updatable to false for the user property in your Device class , it will cause the persistence provider to ignore this column (Device.userId) when generating SQL INSERT and UPDATE statement.
Just change them to true or remove them as their default values are already true.
Update :
this means I have to get user of the device from the User table
whenever I save a device?
In pure JPA , if you know the ID of the user , you can use EntityManager#getReference(User.class , aUserId) to get an User instance without actually querying from DB . But in Spring Data JPA , it seems that this method is not supported out of the box.
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.
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.