JPA prevent entity from being saved - spring

I've the following db schema:
account -< account_role >- role
Summing up: an account can be tied to multiple roles and account_role is a join table. roles is predefined and roles are inserted to DB in a migration.
Unfortunately on new account creating hibernate tries to insert the role to the table as well which results in the following exception:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "role_role_name_key"
Detail: Key (role_name)=(CUSTOMER) already exists.
How can I configure the relation to prevent the insertion?
Below mentioned classes:
Role
import javax.persistence.*;
import static javax.persistence.EnumType.STRING;
#Entity
#Table(name = "ROLE")
class Role {
#Id
#Column(name = "ROLE_NAME")
#Enumerated(STRING)
private RoleName role;
//getters, setters, constructors
}
and
Account
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.FetchType.EAGER;
import static javax.persistence.GenerationType.SEQUENCE;
#Entity
#Table(name = "ACCOUNT")
public class Account {
#Id
#SequenceGenerator(allocationSize = 1, sequenceName = "ACCOUNT_PK_SEQ", name = "ACCOUNT_PK_SEQ")
#GeneratedValue(generator = "ACCOUNT_PK_SEQ", strategy = SEQUENCE)
#Column(name = "ID")
private Long id;
#Column(name = "USERNAME")
private String username;
#ManyToMany(fetch = EAGER)
#JoinTable(
name = "ACCOUNT_ROLE",
joinColumns = #JoinColumn(name = "USERNAME", referencedColumnName = "USERNAME"),
inverseJoinColumns = #JoinColumn(name = "ROLE_NAME", referencedColumnName = "ROLE_NAME")
)
private Set<Role> roles = new HashSet<>();
//getters, setters, constructor
}
Here is the code responsible for saving new account:
#Transactional
public Account createNewAccount(Account account, String password) {
validator.validateNewAccount(account, password);
String email = account.getUsername().toLowerCase();
checkIfEmailAlreadyTaken(email);
LOG.info("Creating new account for username: '{}'.", account.getUsername());
account.setPassword(encodePassword(password));
account = repository.save(account);
return account;
}
Object of an Account class is created automatically from the request.
The full exception stacktrace can be found here.

The problem is caused by
#Id
#Column(name = "ROLE_NAME")
#Enumerated(STRING)
The JPA spec says :
2.1.4 Primary Keys and Entity Identity
The primary key (or field or property of a composite primary key) should be one of the following types: any Java primitive type; any primitive wrapper type; java.lang.String; java.util.Date; java.sql.Date. In general, however, approximate numeric types (e.g., floating point types) should never be used in primary keys. Entities whose primary keys use types other than these will not be portable.
So the combination of #Id and #Enumeration seems not allowed as you are using an enum as id.
With your current code you can write only as many roles as the enum RoleName has elements. These are the predifined roles of your system.
You should do that once on startup (or otherwise make sure they exist)
Add the annotation
#Column(insertable=false, updatable=false)
to Account.roles, this tells the JPA provider, to not insert or update the roles, when an Account is inserted or updated.
This should work.

Related

Why Value is not getting assigned in JPA for insert statement

Hi I have couple of Entity classes as below, using lombok for getter and setters
Parent Entity Class have
#Table(name = "PARTY")
#Entity
public class Party {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "PARTY_ID")
#MapKey(name="dvpParticipantName")
#LazyCollection(LazyCollectionOption.FALSE)
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
}
Child Entity Class have
#Table(name = "DV_LOAN_PARTICIPANTS")
#Entity
public class DVLoanParticipants implements Serializable {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
}
In service class i am calling save operation as
repository.save(parentEntityObject);
I am able to execute update statements ,but when i try to insert new row for child entity class i am getting an error saying
cannot insert NULL into ("ABC"."DV_LOAN_PARTICIPANTS"."PARTY_ID")
But if i print the parentEntityObject just before the save operation i see the values like
(partyId=12345678, dvpParticipantName=XYZ, dvpParticipantType=VKP)
I see the query formed as
insert
into
DV_LOAN_PARTICIPANTS
(DVP_PARTICIPANT_TYPE, PARTY_ID, DVP_PARTICIPANT_NAME)
values
(?, ?, ?)
Just before te save i am seeing valules in the Object
Builder=DVLoanParticipants(partyId=123456, dvpParticipantName=Builder,
dvpParticipantType=Individual)
Update
This is the setting part for values
DVLoanParticipants dvLoanParticipants = new
DVLoanParticipants();
dvLoanParticipants.setPartyId(Long.valueOf(partyId));
dvLoanParticipants.setDvpParticipantName("Builder");
dvLoanParticipants.setDvpParticipantType("Individual");
Party party = new Party();
Map<String, DVLoanParticipants> dvLoanParticipantsMap = new
java.util.HashMap<>();
dvLoanParticipantsMap.put("Builder", dvLoanParticipants);
party.setPartyId(Long.valueOf(partyId));
party.setDvLoanParticipantsMap(dvLoanParticipantsMap);
repository.save(party);
What is the mistake i am doing ?
The root cause of your problem in this part:
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "LOAN_ID")
#MapKey(name="dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
actually for your case the column name in the #JoinColumn means:
If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.
So, assuming for the clarity that you want to map the following schema:
create table PARTY
(
PARTY_ID int,
-- ...
primary key (PARTY_ID)
);
create table DV_LOAN_PARTICIPANTS
(
PARTY_ID int,
DVP_PARTICIPANT_NAME varchar(50),
DVP_PARTICIPANT_TYPE varchar(10),
-- ...
primary key (PARTY_ID, DVP_PARTICIPANT_NAME),
foreign key (PARTY_ID) references PARTY(PARTY_ID)
);
You can use the following mapping:
#Entity
#Table(name = "PARTY")
public class Party
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
// I use fetch = FetchType.EAGER instead of deprecated #LazyCollection(LazyCollectionOption.FALSE)
// targetEntity = DVLoanParticipants.class is redundant here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "PARTY_ID") // this is DV_LOAN_PARTICIPANTS.PARTY_ID column
#MapKey(name = "dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
public Party()
{
dvLoanParticipantsMap = new HashMap<>();
}
// getters / setters
public void addParticipant(DVLoanParticipants p)
{
this.dvLoanParticipantsMap.put(p.getDvpParticipantName(), p);
p.setPartyId(getPartyId());
}
}
#Entity
#Table(name = "DV_LOAN_PARTICIPANTS")
public class DVLoanParticipants implements Serializable
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
// getters / setters
}
and example how to save:
Party party = new Party();
party.setPartyId(2L);
// ...
DVLoanParticipants part1 = new DVLoanParticipants();
part1.setDvpParticipantName("Name 3");
part1.setDvpParticipantType("T1");
DVLoanParticipants part2 = new DVLoanParticipants();
part2.setDvpParticipantName("Name 4");
part2.setDvpParticipantType("T1");
party.addParticipant(part1);
party.addParticipant(part2);
repository.save(party);
and several notes:
The LazyCollectionOption.TRUE and LazyCollectionOption.FALSE values are deprecated since you should be using the JPA FetchType attribute of the #OneToMany association.
You use hibernate specific approach for mapping сomposite identifiers. As it's mentioned in the hibernate documentation:
The restriction that a composite identifier has to be represented by a primary key class (e.g. #EmbeddedId or #IdClass) is only JPA-specific.
Hibernate does allow composite identifiers to be defined without a primary key class via multiple #Id attributes.
But if you want to achieve more portability you should prefer one of the jpa allowed approaches.

Two tables connected via Primary Key

I have read about the use of #MapsId and #PrimaryKeyJoinColumn annotations, which sounds like a great options. I have two tables (UserList and UserInformation) which have a child, parent relationship, respectively; both classes below are abbreviated to just include the relevant columns. UserInformation's primary key value is always null and does not take the value of its parent column.
User Class
#Entity
#Data
#Table(name = "user_list")
public class UserList {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// List of foreign keys connecting different entities
#OneToOne(mappedBy = "user")
#MapsId("id")
private UserInformation userInfo;
}
UserInformation Class
#Entity
#Data
#Table(name = "user_information")
public class UserInformation implements Serializable {
#Id
private Integer userId;
#OneToOne
private UserList user;
}
I would prefer to not use an intermediary class if possible. I'm not tied to MapsId or even this implementation if there is a better solution.
Thanks!
The question is not very clear to me, but I think you could improve the following in the modeling of the entity:
The #column annotation can only be omitted when the class parameter is called exactly the same as the database column, taking into account the table name nomenclature, could it be that the column is user_id ?, if so the id parameter should be :
#Id
#column(name="USER_ID")
private Integer userId;
In the user entity being id, it will match the DB ID field so the #column annotation is not necessary

Jpa OneToOne shared primary key half works

I have SpringBoot 2.1.3 and Java 8 application. Building DB with JPA I have 3 table in one to one relationship. Suppose the tables is the follows:
#Entity
#Data //lombok
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private Address address;
}
And then:
#Entity
#Data
#Table(name = "address")
public class Address {
#Id
#Column(name = "id")
private Long id;
#OneToOne
#MapsId
private User user;
}
That's works.. and it is the best way to do (this exactly example is taken from documentation).
If I start the application the DB is created and if I tried to add entities all works well. The model created follows:
Now I want to add a Country object to my address Entities (for example) and I modified the Entities as follows:
#Entity
#Data
#Table(name = "address")
public class Address {
#Id
#Column(name = "id")
private Long id;
#OneToOne
#MapsId
private User user;
#OneToOne
#MapsId
private Country country;
}
And Country Entities:
#Entity
#Data
#Table(name = "country")
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#OneToOne(mappedBy = "country", cascade = CascadeType.ALL)
private Address address;
}
The application still starts, the DB is created and the model follows:
But if I try to save a User as follows:
User user = new User();
Address address = new Address();
Country country = new Country();
user.setAddress(address);
address.setUser(user);
address.setCountry(country);
country.setAddress(address);
userRepository.save(user);
I obtain the error:
java.sql.SQLException: Field 'country_id' doesn't have a default value
Anyway I solve the issue removing #MapsId and added #JoinColumn but I would like to understand what's wrong.
P.S.: I'm using MySQL 5.7 with InnoDB dialect (setting on application.properties)
Thanks all
It works only with one #MapsId annotation. Using two is causing that country id is not inserted:
insert into Country (id) values (?)
insert into Users (id) values (?)
insert into Address (user_id) values (?)

How to map a relationship between two entities using Redis as a database in Spring Boot 2.0?

I have two entities namely User and AuthenticationToken. For each User I generate a JWT Token which I want to store in a Redis cache for session management. How do I map that, shall I user user_id as a key and Token Object as value?
I'll also have multiple tokens(at max 2) for one user, one for web and one for mobile, so keeping user_id as a key is not a good solution.
What is the correct way to map this in Redis?
I feel, as per your requirement, you can use combination of user_id and type of token as key and put AuthenticationToken object as value.
To generate key you can append the user_id and token type like below:
user_id ==> 10
token_type ==> web/mobile
So key will be 10_web/10_mobile.
I do not have much idea about, what you are having in AuthenticationToken object. If you need any thing related to User entity, you can add that detail in AuthenticationToken object while saving in Redis by attaching a property or the whole User object as property with annotaion #Transient to it (if you do not have User entity linked to AuthenticationToken object)
Updated: I am adding a sample entity relationship for User and AuthenticationToken, it may help you to understand what I am trying to say.
User Entity
import javax.persistence.*;
import java.io.Serializable;
#Entity(name = "user")
public class User implements Serializable {
#Id
#GeneratedValue
private Long userID;
//Getters, Setters and other properties
}
Token Type Enum (To differentiate between web/mobile token)
public enum TokenType {
MOBILE, WEB
}
AuthenticationToken Entity
1. If you want to store the link of User and AuthenticationToken in database also, then you can use the below entity:
import javax.persistence.*;
import java.io.Serializable;
#Entity(name = "authenticationtoken")
public class AuthenticationToken implements Serializable {
#Id
#GeneratedValue
private Long id;
#Column(unique = true)
private String jwtToken;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user", referencedColumnName = "userID")
private User user;
#Column
#Enumerated(EnumType.STRING)
private TokenType tokenType;
// Getters, Setters and other properties
}
2. If you do not want to store the link of User and AuthenticationToken in database and prefer to have details in JWT token, then you can use the below entity:
import javax.persistence.*;
import java.io.Serializable;
#Entity(name = "authenticationtoken")
public class AuthenticationToken implements Serializable {
#Id
#GeneratedValue
private Long id;
#Column(unique = true)
private String jwtToken;
#Transient
private User user;
#Column
#Enumerated(EnumType.STRING)
private TokenType tokenType;
// Getters, Setters and other properties
}
So if you want to maintain max 2 tokens(one for web and one for mobile) for a User, you can fetch by user and token type in Redis/Database before generating a new token and at the time of validation also.
Hope it helps you :)

Saving entity with existing children with CRUD Repository fails because of duplicate key

A User can have multiple Authorities. By default, when I create a new User I want to add him ROLE_USER authority.
This is the User entity:
#Entity
#Table(name = "Users")
public class User
{
#Id
private String username;
private String password;
private String email;
#OneToMany
#JoinTable(
name = "UserAuthority",
joinColumns = #JoinColumn(name = "username"),
inverseJoinColumns = #JoinColumn(name = "authority"))
private List<Authority> authorities = new ArrayList<>();
and Authority entity:
#Entity
public class Authority
{
#Id
private String authority;
I am using basic UserRepository and AuthorityReposiroty both extending CrudRepository.
This is method for registering new user:
public String register(User user)
{
if (userRepository.findOne(user.getUsername()) != null)
{
return "User with given username already exists!";
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.getAuthorities().add(authRepository.findOne("ROLE_USER"));
userRepository.save(user);
return "User successfully registered!";
}
When I try to register new user it fails with this error:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "uk_c1gaghspdr9qx2yt45moye10y"
Detail: Key (authority)=(ROLE_USER) already exists.
I can see in logs that Hibernate is trying to insert ROLE_USER authority which is not possible because it already exists, but I do not understand why. I haven't set cascade property on #OneToManyannotation.
How to I achieve what I want?
This is my configuration:
compile('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('org.postgresql:postgresql:9.4-1206-jdbc42')
Thanks in advance!
Problem is in relations. You have OneToMany - means that on other site is relation ManyToOne... If you used some Authority in one User you can't use this entity (object) for other user.
The solution of your issue is changing relation to #ManyToMany insted #OneToMany in User class. It's little streng, but in fact it is relation ManyToMany, because User can has many authorities and any athority can be used in many users.

Resources