Spring Boot 2, Spring 5 JPA: Dealing with multiple OneToMany JoinTables - spring

Not sure the best approach to implementing the CrudRepository for an Entity that has multiple
#OneToMany associations with a #JoinTable
#Entity
#Table(name = "contact", uniqueConstraints = {#UniqueConstraint(columnNames ={"first_name","last_name"})})
#SuppressWarnings("PersistenceUnitPresent")
public class Contact extends Auditable implements Serializable {
#Id
#Column(name = "contact_id")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "contact_generator")
#SequenceGenerator(name = "contact_generator", sequenceName = "contact_seq", allocationSize = 50)
private Long contactId;
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "last_name", nullable = false)
private String lastName;
#Column(name = "middle_name", nullable = true)
private String middleName;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinTable(
name = "contact_phone"
)
private List<Phone> phoneNumbers = new ArrayList<>();
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinTable(name = "contact_email")
private List<EmailAddress> emailAddresses = new ArrayList<>();
public interface ContactRepo extends CrudRepository<Contact, Long> {
List<Contact> findByLastNameContainingIgnoreCase(String lastName);
}
I have the FetchType.LAZY so I don't get the MultipleBagFetchException from 2 cartesian products.
So I know I need to split the 2 joins up which is where I am stuck as to the best solution.
Put in a custom repo and customImpl class that has can access the EntityManager and code out the 2 joins?
I am not crazy and letting Java take care of the cartesian via a Set, nor the one having FetchType.EAGER and dealing with the other with another query??
Generates:
create table contact (
contact_id bigint not null,
create_tm timestamp not null,
created_by varchar(255) not null,
updated_tm timestamp not null,
updated_by varchar(255) not null,
first_name varchar(255) not null,
last_name varchar(255) not null,
middle_name varchar(255),
primary key (contact_id)
)
create table email_address (
email_id bigint not null,
email_addr varchar(255) not null,
email_type varchar(255),
primary_addr boolean default false,
primary key (email_id)
)
create table contact_email (
Contact_contact_id bigint not null,
emailAddresses_email_id bigint not null
)
create table phone (
phone_id bigint not null,
phone_nbr varchar(255) not null,
phone_type varchar(255),
primary_ph boolean default false,
primary key (phone_id)
)
create table contact_phone (
Contact_contact_id bigint not null,
phoneNumbers_phone_id bigint not null
)
The strange think is my JpaDataTests worked find. The find all and findByLastNameContainingIgnoreCase return the phone numbers and email addresses.
However, The Service does not.
#Autowired
private ContactRepo contactRepo;
#Override
public List<Contact> findAllContacts() throws GcaServiceException {
try {
Iterable<Contact> iter = contactRepo.findAll();
return IteratorUtils.toList(iter.iterator());
} catch(DataAccessException e) {
throw new GcaServiceException(e.getMessage());
}
}
#Override
public List<Contact> findByLastName(String lastName) throws GcaServiceException {
try {
return contactRepo.findByLastNameContainingIgnoreCase(lastName);
} catch (DataAccessException e) {
throw new GcaServiceException(e.getMessage());
}
}
[
{
"createTm": "2021-01-11T16:27:19.995011",
"createdBy": "UncleMoose",
"updatedBy": "UncleMoose",
"updateTm": "2021-01-11T16:27:19.995011",
"contactId": 1,
"firstName": "Bruce",
"lastName": "Randall",
"middleName": null,
"phoneNumbers": [],
"emailAddresses": []
},
{
"createTm": "2021-01-11T16:27:19.996009",
"createdBy": "UncleMoose",
"updatedBy": "UncleMoose",
"updateTm": "2021-01-11T16:27:19.996009",
"contactId": 51,
"firstName": "Boss",
"lastName": "Randall",
"middleName": null,
"phoneNumbers": [],
"emailAddresses": []
}
]

Part of the mystery of DataJpaTest vs manual integration testing differences was I decided to look at a map and make sure I wasn't hiking down the wrong Google trail. I turned the H2 console on and found the Join Tables empty even though the insert occurred? However, I notice I was getting different Join Table column names between live and automated testing.
Solution was to explicitly name the Join Table columns. It appears Spring has handled the MultipleBagFetchException issues with multiple OneToMany JoinTable attributes in an Entity.
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinTable(
name = "contact_phone"
,joinColumns = #JoinColumn(name = "contact_id")
,inverseJoinColumns = #JoinColumn(name = "phone_id")
)
private List<Phone> phoneNumbers = new ArrayList<>();
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinTable(
name = "contact_email"
,joinColumns = #JoinColumn(name = "contact_id")
,inverseJoinColumns = #JoinColumn(name = "email_id")
)
private List<EmailAddress> emailAddresses = new ArrayList<>();

Related

One to Many relationship throwing (null value in column "permission_group_id" violates not-null constraint)

Parent.java:
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pgroup_generator")
#SequenceGenerator(name="pgroup_generator", sequenceName = "pg_seq", allocationSize=50)
#Column(
name = "group_id",
unique = true,
updatable = false,
nullable = false
)
private Long id;
#Column(name="group_name",unique = true)
private String groupName;
#OneToMany(targetEntity=PermissionsEntity.class, mappedBy = "permissionGroup", cascade=CascadeType.ALL, fetch = FetchType.LAZY)
private List<PermissionsEntity> permissions= new ArrayList<>();
public void setPermissions(List<PermissionsEntity> permissions) {
this.permissions = permissions;
for(PermissionsEntity p:permissions) {
p.setPermissionGroup(this);
}
}
child.java:
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name="group_id", referencedColumnName = "group_id", insertable = false, updatable = false)
private PermissionGroupEntity permissionGroup;
Here is the error log:
org.postgresql.util.PSQLException: ERROR: null value in column "permission_group_id" violates not-null constraint
Detail: Failing row contains (11, null, 2020-11-02 10:52:34.849, null, 2020-11-02 10:52:34.849, Allow the user to create findings, create audit or workpaper findings, null, null, null, null, null, null).
Because you have insertable = false on that permissionGroup, it's not being inserted, leaving it null. Remove that setting to leave it the default true.

Hibernate: Find entity from one to many table

I have two tables
CREATE TABLE `heroic_quality`
(
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(515) NOT NULL UNIQUE,
PRIMARY KEY (`id`)
);
CREATE TABLE `hero`
(
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(515) NOT NULL UNIQUE,
`quality_id` INT DEFAULT NULL,
FOREIGN KEY (`quality_id`) REFERENCES heroic_quality (id),
PRIMARY KEY (`id`)
);
And the objects in hibernate are
#Table(name = "heroic_quality")
#Entity(name = "heroic_quality")
public class HeroicQuality
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
protected long id;
#Column(name = "name", nullable = false, unique = true)
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "id")
#Fetch(FetchMode.SELECT)
private List<Hero> heroes;
//ommited getters and setters for shortness
}
#Table(name = "hero")
#Entity(name = "hero")
public class Hero
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
protected long id;
#Column(name = "name", nullable = false, unique = true)
private String name;
//ommited getters and setters for shortness
}
As you see my Hero class doesn't have reference to heroic quality, and I would like to keep it that way.
Also I have a repository
#Repository
public interface HeroicQualityDAO
extends PagingAndSortingRepository<HeroicQuality, Long>
{
Optional<HeroicQuality> findByName(String name);
List<HeroicQuality> findByOrderByIdDesc();
}
What I would like to do is have a method such as
Optional<HeroicQuality> findByHeroName(String heroName)
Such that if given a name of hero from Hero table I will be able to get heroic quality object.
How can I make such a method?
Is there any way I can get heroic quality object without having a reference to it in the hero object?
How can I go about doing that?
Add the following method to HeroicQualityDAO.
Optional<HeroicQuality> findByHeroesName(String heroName);
If you are not happy with the method name, you can do
#Query("Select h from HeroicQuality hq join hq.heros h where h.name = :name")
Optional<HeroicQuality> findByHeroName(String name);

Handling a oneToMany relationship in Spring boot JPA

In my database I have a user who can have multiple email addresses. An email address can have only one user. I have following two tables in my database to handle this.
CREATE TABLE IF NOT EXISTS w4a_user (
id INTEGER NOT NULL AUTO_INCREMENT,
login_id VARCHAR(100) NOT NULL UNIQUE,
first_name VARCHAR(100),
last_name VARCHAR(100),
division INTEGER NOT NULL,
created_date TIMESTAMP NOT NULL,
last_active DATE,
PRIMARY KEY (id),
FOREIGN KEY (login_id) REFERENCES w4a_authentication_data (login_id) ON DELETE RESTRICT,
FOREIGN KEY (division) REFERENCES w4a_division (id) ON DELETE RESTRICT
);
CREATE TABLE IF NOT EXISTS w4a_email_address(
email_address VARCHAR(100) NOT NULL,
user_id INTEGER NOT NULL,
is_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (email_address),
FOREIGN KEY (user_id) REFERENCES w4a_user (id) ON DELETE CASCADE
);
In my Spring boot application, I have following entity classes to handle this.
User.java
#Entity
#Table(name = "w4a_user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private int id;
#Column(name = "first_name")
#Size(max = 100, message = GlobalConstants.ErrorMessageConstants.ERROR_FIRST_NAME_LENGTH_EXCEEDED)
private String firstName;
#Column(name = "last_name")
#Size(max = 100, message = GlobalConstants.ErrorMessageConstants.ERROR_LAST_NAME_LENGTH_EXCEEDED)
private String lastName;
#Column(name = "created_date")
private Date createdDate;
#Column(name = "last_active")
private Date lastActive;
#ManyToOne
#JoinColumn(name = "division", referencedColumnName = "id")
private Division division;
#OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true)
#Size(min = 1)
private List<ContactNumber> contactNumberList;
#OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, orphanRemoval = true)
#Size(min = 1)
private List<EmailAddress> emailAddresses;
.
.
}
EmailAddress.java
#Entity
#Table(name = "w4a_email_address")
public class EmailAddress {
#Id
#Column(name = "email_address")
#Email(message = GlobalConstants.ErrorMessageConstants.ERROR_EMAIL_INCORRECT_FORMAT,
regexp = GlobalConstants.RegexList.EMAIL_REGEX)
#Size(max = 100, message = GlobalConstants.ErrorMessageConstants.ERROR_EMAIL_LENGTH_EXCEEDED)
private String emailAddress;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private User userId;
#Column(name = "is_confirmed")
private Boolean isConfirmed;
.
.
}
I use following method to persist entitites to my database.
#PersistenceContext
private EntityManager em;
#Override
public T createEntity(T entity) {
this.em.unwrap(Session.class).save(entity);
return entity;
}
I set email address list in the user entity and perform above method to create a new user.
The issue I have is when adding a user with an email address already used by an existing user. In this case, the database entry for the email address gets updated with the id of the new user. Instead I want to give an error saying the email address is already in use. What is the best way of handling this?

How #RequestBody works

How to get more details:
I am doing simple rest post request from Postman chrome extension.
My controller is :
#Controller
#RequestMapping("/theme")
public class ThemeController {
#RequestMapping(value = "/create", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody
Status addTheme(#RequestBody Theme theme) {
try {
themeServices.addEntity(theme);
return new Status(1, "Theme added Successfully !");
} catch (Exception e) {
// e.printStackTrace();
return new Status(0, e.toString());
}
}
In Theme.java:
#Entity
#Table(name = "theme", uniqueConstraints = { #UniqueConstraint(columnNames = { "theme_id" }) })
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
#NamedQuery(name = "Theme.findAll", query = "SELECT t FROM Theme t")
public class Theme implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "theme_id")
private long themeId;
private String description;
private String name;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "category_id", nullable=true)
private ThemeCategory themeCategory;
In ThemeCategory.java:
#Entity
#Table(name = "theme_category", uniqueConstraints = { #UniqueConstraint(columnNames = { "category_id" }) })
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
#NamedQuery(name = "ThemeCategory.findAll", query = "SELECT t FROM ThemeCategory t")
public class ThemeCategory implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "category_id")
private long categoryId;
private String description;
private String name;
// bi-directional many-to-one association to Theme
// #OneToMany(mappedBy="themeCategory")
#OneToMany(mappedBy = "themeCategory", fetch = FetchType.EAGER)
#Column(nullable = true)
#JsonManagedReference
private Set<Theme> themes;
// bi-directional many-to-one association to ThemeCategory
#ManyToOne
#JoinColumn(name = "parent_category_id", nullable=true)
#JsonBackReference
private ThemeCategory parentThemeCategory;
// bi-directional many-to-one association to ThemeCategory
// #OneToMany(mappedBy="themeCategory")
#OneToMany(mappedBy = "parentThemeCategory", fetch = FetchType.EAGER)
#Column(nullable = true)
#JsonManagedReference
private Set<ThemeCategory> themeCategories;
Theme Category Table:
CREATE TABLE `theme_category` (
`category_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`parent_category_id` smallint(5) unsigned DEFAULT NULL,
`name` varchar(45) NOT NULL,
`description` varchar(1000) DEFAULT NULL ,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`category_id`),
KEY `idx_parent_category_id` (`parent_category_id`),
CONSTRAINT `fk_parent_category_id` FOREIGN KEY (`parent_category_id`) REFERENCES `theme_category` (`category_id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=301 DEFAULT CHARSET=utf8;
Theme Table:
CREATE TABLE `theme` (
`theme_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`category_id` smallint(5) unsigned NOT NULL,
`file_path` varchar(200) DEFAULT NULL,
`description` varchar(1000) DEFAULT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`theme_id`),
KEY `idx_category_id` (`category_id`),
CONSTRAINT `fk_category_id` FOREIGN KEY (`category_id`) REFERENCES `theme_category` (`category_id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=401 DEFAULT CHARSET=utf8;
I am using Postman extension to do a rest post call:
http://localhost:8080/CustomerRegistration/theme/create
Header params:
Content-Type: application/json
Json Body:
{"description":"theme8","name":"theme8","themeCategory":{"categoryId":302, "themes":[],"parentThemeCategory":{}, "themeCategories":[]}}
And tried around 2 hours with multiple ways of body. But it consistently saying:
The server refused this request because the request entity is in a format not supported
by the requested resource for the requested method.
To analyse, I am not getting any thing else. In Eclipse console also not showing anything regarding the this issue.
What is wrong? Is there any tools to create valid requests.

null id generated for composite PK

I have the following tables and the following relationship table too: , which has a composite PK as follow:
UserRole.java
#RooJavaBean
#RooJpaEntity(identifierType = UserRolePK.class, versionField = "", table = "UserRole", schema = "dbo")
#RooDbManaged(automaticallyDelete = true)
#RooToString(excludeFields = { "idApplication", "idRole", "idUserName" })
public class UserRole {
}
UserRole_Roo_DbManaged.aj
#ManyToOne
#JoinColumn(name = "IdApplication", referencedColumnName = "IdApplication", nullable = false, insertable = false, updatable = false)
private Application UserRole.idApplication;
#ManyToOne
#JoinColumn(name = "IdRole", referencedColumnName = "IdRole", nullable = false, insertable = false, updatable = false)
private Role UserRole.idRole;
#ManyToOne
#JoinColumn(name = "IdUserName", referencedColumnName = "IdUserName", nullable = false, insertable = false, updatable = false)
private Users UserRole.idUserName;
But also exist a PK table:
#RooIdentifier(dbManaged = true)
public final class UserRolePK {}
And its identifier class (UserRolePK_Roo_Identifier.aj)
privileged aspect UserRolePK_Roo_Identifier {
declare #type: UserRolePK: #Embeddable;
#Column(name = "IdRole", nullable = false)
private Long UserRolePK.idRole;
#Column(name = "IdUserName", nullable = false, length = 16)
private String UserRolePK.idUserName;
#Column(name = "IdApplication", nullable = false)
private Long UserRolePK.idApplication;
The way how I'm setting the service objec to save is:
UserRole userRole= new UserRole();
userRole.setIdApplication(app);
userRole.setIdRole(invited);
userRole.setIdUserName(user);
appService.saveURole(userRole);
app has been set and saved before (same transaction), as well as invited and user objects.
Since user (from Users table with composite PK: IdUserName which is a String ), is defined as follow, otherwise doesnt work.
#RooJavaBean
#RooJpaEntity(versionField = "", table = "Users", schema = "dbo")
#RooDbManaged(automaticallyDelete = true)
#RooToString(excludeFields = { "quotations", "taxes", "userRoles", "idCompany", "idPreferredLanguage" })
public class Users {
#Id
//#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "IdUserName", length = 16, insertable = true, updatable = true)
private String idUserName;
}
So, the error that I'm getting is:
org.springframework.orm.jpa.JpaSystemException: org.hibernate.id.IdentifierGenerationException: null id generated for:class com.domain.UserRole; nested exception is javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: null id generated for:class com.domain.UserRole
Try this:
public class UserRole {
#PrePersist
private void prePersiste() {
if (getId() == null) {
UserRolePK pk = new UserRolePK();
pk.setIdApplication(getIdApplication());
pk.setIdRole(getIdRole);
pk.setIdUserName(getIdUserName());
setId(pk);
}
}
}
Roo is generating the fields on UserRole entity and its id embedded class, but is not the same thing (UserRole.idRole is not the same than UserRole.id.idRole). In your example, you fill the UserRole fields, but not the id fields. This code makes it for you before entity is persisted.
Good luck!
In my case if the follow example tries to be persisted in DB, then similar Exception mentioned above is thrown:
EntityExample e = new EntityExample();
repositoryExample.save(e);
//throw ex
This is caused due to missing id field values which needs to be set something like that:
EntityExample e = new EntityExample();
e.setId(new EmbeddedIdExample(1, 2, 3));
repositoryExample.save(e);

Resources