Suppose you have two resources, User and Account. They are stored in separate tables but have a one-to-one relationship, and all API calls should work with them both together. For example a POST request to create a User with an Account would send this data:
{ "name" : "Joe Bloggs", "account" : { "title" : "My Account" }}
to /users rather than have multiple controllers with separate routes like users/1/account. This is because I need the User object to be just one, regardless of how it is stored internally.
Let's say I create these Entity classes
#Table(name = "user")
public class User {
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#NotNull
Account account;
#Column(name = "name")
String name;
}
#Table(name = "account")
public class Account {
#OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", nullable = false)
#NotNull
User user;
#Column(name = "title")
String title;
}
The problem is when I make that POST request above, it throws an error because user_id is missing, since that's required for the join, but I cannot send the user_id because the User has not yet been created.
Is there a way to create both entities in a single API call?
Since it is a bi-directional relation, and one-to-one is a mandatory in this case, you should persist a user entity and only then persist an account. And one more thing isn't clear here is db schema. What are the pk's of entities? I coukd offer to use user.id as a single identity for both of tables. If so, entities would be as:
User(id, name), Account(user_id, title) and its entities are:
#Table(name = "account")
#Entity
public class Account {
#Id
#Column(name = "user_id", insertable = false, updatable = false)
private Long id;
#OneToOne(mappedBy = "account", fetch = FetchType.LAZY, optional = false)
#MapsId
private User user;
#Column(name = "title")
private String title;
}
#Table(name = "user")
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "id", referencedColumnName = "user_id")
private Account account;
#Column(name = "name")
private String name;
}
at the service layer you must save them consistently:
#Transactional
public void save(User userModel) {
Account account = user.getAccount();
user.setAccount(null);
userRepository.save(user);
account.setUser(user);
accountRepository.save(account);
}
it will be done within a single transaction. But you must save the user first, coz the user_id is a PK of the account table. #MapsId shows that user's id is used as an account's identity
Another case is when account's id is stored in the user table:
User(id, name, account_id), Account(id, title) and entities are:
#Table(name = "account")
#Entity
public class Account {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "account")
private User user;
#Column(name = "title")
private String title;
}
#Table(name = "user")
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "account_id", insertable = false, updatable = false)
private Long accountId;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "account_id", referencedColumnName = "id", unique = true)
private Account account;
#Column(name = "name")
private String name;
}
in this case an Account entity will be implisitly persisted while User entity saving:
#Transactional
public void save(User userModel) {
userRepository.save(user);
}
will cause an insertion into the both of tables. Since cascade and orphane are declared, for deletion would be enough to set null for the account reference:
user.setAccount(null);
userRepository.save(user);
Related
I have two java entity classes :
#Table(name = "user")
public class UserEntity
{
#Id
#Column(name = "id", unique = true, nullable = false)
private Long id;
#OneToOne(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER)
#JoinColumn(name = "opportunity_id")
private OpportunityEntity opportunity;
}
and
#Table(name = "opportunity")
public class OpportunityEntity
{
#Id
#Column(name = "id", unique = true, nullable = false)
private Long id;
#OneToMany
#JoinColumn(name = "opportunity_id")
private List<UserEntity> users;
#OneToOne
#JoinColumn(name = "mainuser_id")
private UserEntity mainUser;
}
When i search for a list of Users [find users], i've got a "stackoverflow" when mapping User.opportunity.
the bug was clear that the opportunity.mainUser refer to User which itself refer to the same opportunity.
Is there another way to design my models ?
For example create a boolean isMain in User Model ?
Try to specify relationship to UserEntity by adding mappedBy to annotatation
#Table(name = "opportunity")
public class OpportunityEntity
{
#Id
#Column(name = "id", unique = true, nullable = false)
private Long id;
#OneToMany
#JoinColumn(name = "opportunity_id")
private List<UserEntity> users;
#OneToOne(mappedBy="opportunity")
#JoinColumn(name = "mainuser_id")
private UserEntity mainUser;
}
I would be very grateful to you for help.
I use Spring Boot 2.5.2.
DB: H2 with Liquibase
I need to change ticket History when attachment is removed.
This is my entities:
Ticket:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Ticket {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "user_owner_id", nullable = false)
private User owner;
#OneToMany(mappedBy = "ticket", orphanRemoval = true)
private List<Attachment> attachments;
#OneToMany(mappedBy = "ticket", orphanRemoval = true)
private List<History> history;
// other fields and relationships
}
User:
#Entity
#Table(name = "users")
#Data
#AllArgsConstructor
#NoArgsConstructor
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "owner", orphanRemoval = true)
private List<Ticket> ownerTickets;
// other fields and relationships
}
Attachment:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Attachment {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String filename;
#Column(columnDefinition = "bytea")
private byte[] file;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "user_id", nullable = false)
private User user;
}
History:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class History {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#CreationTimestamp
#Column(name = "changed_date", nullable = false, updatable = false)
private LocalDateTime changedDate;
private String action;
private String description;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "ticket_id")
private Ticket ticket;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "user_id", nullable = false)
private User user;
}
Services:
JpaAttachmentService:
#Service
#AllArgsConstructor
public class JpaAttachmentService implements AttachmentService {
private final AttachmentRepository attachmentRepository;
private final HistoryService historyService;
#Transactional
#Override
public void delete(Long id, Long ticketId, Long userId) {
var attachment = attachmentRepository.getByIdAndTicketIdAndTicketOwnerId(id, ticketId, userId);
var action = "File is removed";
var description = "File is removed: " + attachment.getFilename();
var ticket = attachment.getTicket();
var user = attachment.getUser();
var history = new History(null, LocalDateTime.now(), action, description, ticket, user);
historyService.save(history);
attachmentRepository.deleteByIdAndTicketIdAndTicketOwnerId(id, ticketId, userId);
}
}
When I try to 'historyService.save(history)' I catch:
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: NULL not allowed for column "USER_ID"; SQL statement:
update history set action=?, description=?, ticket_id=?, user_id=? where id=? [23502-200]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:459)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:429)
at org.h2.message.DbException.get(DbException.java:205)
at org.h2.message.DbException.get(DbException.java:181)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:374)
at org.h2.table.Table.validateConvertUpdateSequence(Table.java:845)
at org.h2.command.dml.Update.update(Update.java:176)
at org.h2.command.CommandContainer.update(CommandContainer.java:198)
at org.h2.command.Command.executeUpdate(Command.java:251)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:191)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:152)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
... 144 more
This is 'var user' debug:
picture
I can't understand why I get this Hibernate Interceptor but not entity. I confused when I see inside interceptor required UserID and when I see 'null' outside of it. Could you help me with this problem, please?
Thanks in advance for any help.
The exception message is quite informative regarding the error you are experiencing. Take a look at the following part:
NULL not allowed for column "USER_ID"; SQL statement: update history
set action=?, description=?, ticket_id=?, user_id=? where id=?
What you attempt to do is to save an instance of a History entity which without passing in a reference to a User object. Since your relation dictates that the user reference cannot be null:
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "user_id", nullable = false)
To fix this, either make sure that a non-null reference to a User object is passed in when inserting/updating the History reference, or modify your database constraint design to allow for null user references at the History entity.
My problem was related to test, but in the further development I faced it again. So I want to share my solution.
Maybe it will help you.
Pay attention to CascadeType! Cascading operations must be specified above the link to the child relation. In my case, I have to remove this relation or leave Ticket with CascadeType.PERSIST
Fixed class Attachment:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Attachment {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String filename;
#Column(columnDefinition = "bytea")
private byte[] file;
#ManyToOne(fetch = FetchType.LAZY) // or cascade = CascadeType.PERSIST
#JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", nullable = false)
private User user;
}
I currently have a problem with this Relationship, I have tried everything I saw on the internet. Still, I get this error: ERROR: column roles0_.user_id does not exist.
I have a boot app that has spring security, and I need to login using users from PostgreSQL database.
But I just can't get the relation between the user and the Role to work.
Here are Entity classes:
#Data
#Entity
#Table(name="user",schema = "public")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Integer id;
#Column(unique = true)
private String username;
private String password;
private boolean enabled;
#OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Role> roles;
}
#Data
#Entity
#Table(name="role",schema = "public")
public class Role {
#Id
#Column(name="role_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
#ManyToOne()
#JoinColumn(name ="user_id")
private User user;
}
The database looks fine, I looked at the column names, etc. I don't know what to do to get rid of this error. I have the user table, and another table named roles, which include id and name, 2 inputs, USER and ADMIN...
It seems that the #JoinColumn annotation requires one additional column in the roles table the one with #ManytoOne relation, because when I add the column the error disappears, but when I'm trying to get the role from each user, I get an empty List. The foreign key is set as well, from the roles column to the role_id column from role table.
worked for me this way:
#Entity
#Data
#Table(name = "users")
public class User{
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_roles", joinColumns = {#JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "role_id", referencedColumnName = "id")})
private List<Role> roles;
}
and then in roles just:
#Entity
#Table(name = "roles")
public class Role{
#ManyToMany(mappedBy = "roles", fetch = LAZY)
private List<User> users;
}
that's if you are ok with third table user_roles (user_id, role_id) which manages the many to many relation
User table :
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user", fetch = FetchType.EAGER)
private List<Role> roles;
Role table :
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
private User user;
I have a User Class
#Entity(name = "users")
#Table(name = "users")
public class User implements UserDetails {
static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id", nullable = false)
private Long id;
#Column(name = "username", nullable = false, unique = true)
private String username;
#Column(name = "password", nullable = false)
private String password;
}
Tied to a simple Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
}
And I have an Instructor Class that has a nested User object
#Entity
#Table(name = "instructors")
public class Instructor {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "instructor_id", nullable = false, updatable = false)
private Long id;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "user_id")
private User user;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "course_id")
private List<Course> courses;
}
It is saved with the following repository
public interface InstructorRepository extends PagingAndSortingRepository<Instructor, Long> {
}
The JSON I am posting
{
"user": {
"id": 1
}
}
When I try to do a POST to /instructors . User is coming in null. Is there something I am missing to get JPA to tie the two together? I have tried adding CascadeType.ALL onto the field and that only throws a detached persist exception.
Leave the CascadeType.ALL to Instructor like you already tried:
#ManyToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
In addition add the following to User. Seems to work with me. It provides the mapping information and makes JPA treat User managed
#OneToMany(mappedBy="user")//, cascade=CascadeType.ALL)
private List<Instructor> instructors = new ArrayList<>();
I have commented out the cascadeType in the above but it might be useful if you want to persist User wit all of its Instructors.
There is a unidirectional ManyToMany mapping between Role and Privilege with Role as the owning entity like so
Role
#Entity
public class Role extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "role_id")
private Integer roleId;
#Size(max = 45)
#Column(name = "role")
private String role;
#JoinTable(name = "role_privilege", joinColumns = {
#JoinColumn(name = "role_role_id", referencedColumnName = "role_id")}, inverseJoinColumns = {
#JoinColumn(name = "privilege_privilege_id", referencedColumnName = "privilege_id")})
#ManyToMany(
cascade = {
CascadeType.DETACH,
CascadeType.MERGE,
CascadeType.REFRESH,
CascadeType.PERSIST }, fetch = FetchType.EAGER, targetEntity = Privilege.class)
private Collection<Privilege> privilegeCollection;
#Transient
private Collection<Privilege> parentPrivilegeCollection;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "roleId")
#JsonIgnore
private Collection<User> userCollection;
public Role() {
}
//getters,setter,hashcode,equals removed for brevity
}
Privilege
#Entity
public class Privilege extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "privilege_id")
private Integer privilegeId;
#Size(max = 45)
#Column(name = "name")
private String name;
#Size(max = 150)
#Column(name = "description")
private String description;
#Size(max = 45)
#Column(name = "friendly_name")
private String friendlyName;
#JoinTable(name = "privilege_hierachy", joinColumns = {
#JoinColumn(name = "parent_privilege", referencedColumnName = "privilege_id")}, inverseJoinColumns = {
#JoinColumn(name = "child_privilege", referencedColumnName = "privilege_id")})
#ManyToMany
private Collection<Privilege> privilegeCollection;
public Privilege() {
}
}
The Problem
Whenever i set updated list of privileges in a role and update, the join table is successfully updated without removing either target or owning entity, and that is desired result. The problem is on update it also affect another self join table in Privilege called privilege_hierachy which is not what is expect.
Is it possible for hibernate to only update the Role-Privilege mant-to-many relationship and let other relation unchanged.
Spring Data Jpa is used for data persistence
It sounds like you are updating the privileges by (removing old privileges and) adding new ones. If you do that, clearly, the second join table (the self-referencing table) could be updated with new rows, based on what you are passing.
I see that for the self-referencing table, Privilege, you are not setting cascade type. It defaults to no operation, and that sounds like what you want. But my guess is based on what you said "Whenever i set updated list of privileges in a role", and that tells me you are creating new privileges for a role, instead of using existing privileges and associate them with the role.