Hibernate ORM: Saving Parent Entity Saves the Children too? - java

I have the below JSON as input:
{
"type": "Student",
"numOfPeople": "1",
"tenantMembers": [
{
"firstName": "Chris",
"lastName": "C"
}
],
"tenantDetails": {
"firstName": "John",
"lastName": "J",
"email" "xyz#gmail.com"
}
}
I want to use this to do a save:
tenantRepo.save(tenant);
This should save the parent "Tenant" and the children "TenantMembers" and "TenantDetails".
But when I do it does with NULL 'tenant_id's in the children. (If I have foreign keys in the DB gives 'tenant_id' can't be null constraint exception)
My question is: Is this possible in Hibernate?
My models:
Parent class:
#Entity
#Table(name = "tenant")
public class Tenant {
#GeneratedValue
#Id
private Long id;
private String type;
#Column(name = "num_of_people")
private String numOfPeople;
#OneToMany(mappedBy = "tenant", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<TenantMember> tenantMembers;
#OneToOne(mappedBy = "tenant", cascade = CascadeType.ALL)
private TenantDetails tenantDetails;
TenantMember child class:
#Entity
#Table(name = "tenant_member")
public class TenantMember {
#GeneratedValue
#Id
private Long id;
#ManyToOne
#JoinColumn(name = "tenant_id")
private Tenant tenant;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
TenanatDetails child class:
#Entity
#Table(name="tenant_details")
public class TenantDetails {
#GeneratedValue
#Id
private Long id;
#OneToOne
#JoinColumn(name = "tenant_id")
private Tenant tenant;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
private String email;
EDIT:
Following up Dragan Bozanovic's suggestion, tried using #JsonIdentityInfo
for the three tables:
#Entity
#Table(name = "tenant")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Tenant {
#Entity
#Table(name="tenant_details")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class TenantDetails {
#Entity
#Table(name = "tenant_member")
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class TenantMember {
and did the following to save:
#RequestMapping(value = "/set", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public Tenant test(#RequestBody Tenant tenant) {
Tenant t = new Tenant();
t.setType(tenant.getType());
t.setNumOfPeople(tenant.getNumOfPeople());
tenantRepo.save(t);
tenant.setId(t.getId());
tenant.getTenantDetails().setTenant(tenant);
for(TenantMember member: tenant.getTenantMembers()) {
member.setTenant(tenant);
}
return tenantRepo.save(tenant);
}
Would this be the best approach that is possible?

Hibernate does save the children (hence the constraint violation) because of the cascading options you specified, but it does not save the relationship information (join column value) in your case.
TenantMember and TenantDetails are the owners of the association with Tenant (mappedBy attributes in the association annotations in Tenant).
That means that you have to properly update the tenant field in the TenantMember and TenantDetails instances, because Hibernate ignores inverse side of the association when maintaining the relationship.

Related

Spring JPA not returning Foreign keys in response

I have a database with some entities, ( in parent child relationship )I can say and when When I try to make a query to child table to get all the rows, I only get the fields which are not foreign keys. How can I include the foreign key in the response json. Can anyone help me figure this out ?
Parent Class which is working fine when I try to repository.findAll() and it works as expected.
#Entity
#Table(name = "Employees")
#Data
#NoArgsConstructor
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private long id;
private String name;
private String description;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "employee")
private List<Projects> projects;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "employee")
private Address address;
}
Child class:
#Entity
#Table(name = "Address")
#Data
#NoArgsConstructor
public class Address {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String city;
private String state;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "emp_id", nullable = false)
#JsonBackReference
private Employee employee;
}
Here is the repository class for Address Entity
#Repository
public interface AddressRepository extends JpaRepository<Address, Long> {
}
When I try AddressRepository.findAll()
What I get:
[{
"id": 1,
"city": "new york",
"state": "new york"
}]
what I want to get:
"id": 1,
"city": "new york",
"state": "new york",
"emp_id": 1 //which is the foreign key referring to Employee table
}]
What I tried is I updated my Employee column in Address Entity as follow but no luck
#Entity
#Table(name = "Address")
#Data
#NoArgsConstructor
public class Address {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String city;
private String state;
#OneToOne(fetch = FetchType.EAGER)
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "client_id", scope = Client.class)
#JsonIdentityReference(alwaysAsId = true)
#JoinColumn(name = "client_id", nullable = false)
#JsonBackReference
#JsonProperty("clientId")
private Employee employee;
}
You could use a JPA projection:
public class AddressDto {
private long id;
private String city;
private String state;
private long employeeId;
public AddressDto(long id, String city, String state, Employee employee) {
this.id = id;
this.city = city;
this.state = state;
this.employeeId = employee.getId();
}
// getters etc..
}
#Repository
public interface AddressRepository extends JpaRepository<Address, Long> {
List<AddressDto> findAllProjectedBy();
}
Use #JsonProperty on each Foreign Key.
#Entity
#Table(name = "Employees")
#Data
#NoArgsConstructor
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private long id;
private String name;
private String description;
#JsonProperty
#OneToMany(fetch = FetchType.EAGER, mappedBy = "employee")
private List<Projects> projects;
#JsonProperty
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "employee")
private Address address;
}

Thymeleaf to access data of joined tables

I have three tables that have one to one relation the first table is the province entity, the district entity which has a one to relation with the province and the person's entity as each person resides in on district.
I would like to access the district information in thymleaf.
#Entity
#Table (name = "provinces")
public class Province {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int provinceId;
#Column(name="province_name",unique=true,length = 25, nullable = false)
private String provinceName;
//getters and setters
}
#Entity
#Table (name = "districts")
public class District {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(unique=true,length = 25, nullable = false)
private String districtName;
#Column(unique=true, length = 10,nullable = false)
private String abbreviation;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="province")
private Province location;
}
#Entity
public class AppUser implements UserDetails {
#Id
#GeneratedValue (strategy = GenerationType.IDENTITY)
private long id;
private String firstName;
private String lastName;
private String email;
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "location_id")
private District location;
//getters and setters
}
service controller:
#GetMapping("/all")
private String viewDistricts (Model model){
model.addAttribute("listUsers",appUserService.listUser());
return "admin/users";
}
service:
public List<AppUser> listUser(){
return appUserRepository.findAll();
}
thymeleaf:
th:text="${users.location.districtName}"
org.springframework.expression.spel.SpelEvaluationException: EL1007E:
Property or field 'districtName' cannot be found on null
best practice to handle null pointer exception as follows
th:text="${users.location!=null}?${users.location.districtName}:''"
But, the root cause of the problem is that the associated object value is null.

Attribute many ids to single from other tables JPA

I want to create entity USERS_GROUP to link some user_ids to group_id.
I guess I overthink this case and now I can't find a solution.
#Entity
#Table(name = "users")
#Data
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private String userId;
private String name;
#OneToMany(mappedBy = "user")
private List<UserGroups> userGroups;
#Table(name = "groups")
#Data
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "group_id")
private String groupId;
private String category;
private String name;
private String description;
#OneToMany(mappedBy = "group")
private List<UserGroups> userGroups;
#Entity
#Data
#Table(name = "user_groups")
public class UserGroups {
#EmbeddedId
UserGroupsCompositeKey id;
#ManyToOne
#MapsId("userId")
#JoinColumn(name = "user_id")
private Users user;
#ManyToOne
#MapsId("featureId")
#JoinColumn(name = "group_id")
private Group group;
#Embeddable
public class UserGroupsCompositeKey implements Serializable {
#Column(name = "user_id")
String userId;
#Column(name = "group_id")
String groupId;
}
I want to sent POST requests like "/group/{group_id}/users"
to send in request body some lists of user_ids to connect them.
I think I overconfigured this solution with a composite key.
Is there some easier and more readable solution for that case ?
EDIT: This solution is not working as I want it to do.
I create jpaRepository class with CompositeKey (Not sure if it's correct way )
#Repository
public interface ProductFeaturesRepository extends JpaRepository<UserGroups, UserGroupsCompositeKey> {
}
And I want to add some Controller method to group class to add some users to group with request body like:
Endpoint:/group/{group_id}/users
Body:
[
{
"userId": "USER-NR423423534634"
},
{
"userId": "USER-NR2355321"
}
]
It's not working properly nothing is added to database after that request

Null values are inserted in the foreign key fields with Hibernate

I have a Question Entity and Tag entity with getter, setter methods and a OneToMany relationship from question to tag and a OneToOne relationship from question to user
public class Question {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="title")
private String title;
#Column(name="body")
private String body;
#Temporal(TemporalType.DATE)
#Column(name="date_created")
private Date date_created;
#OneToOne(cascade=CascadeType.ALL)
#JoinColumn(name="user_id")
private User user;
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name="tag_id")
private Tag tag;
#Column(name="answer_count")
private int answer_count;
#Column(name="view_count")
private int view_count;
public Question() {
}
Tag entity
public class Tag {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="name")
private String name;
#Column(name="username")
private String username;
#Temporal(TemporalType.DATE)
#Column(name="date_created")
private Date date_created;
public Tag() {
}
When I try to insert a question using Postman with the following details:
{
"title": "stefanyyyxx",
"body": "stefandoyee44",
"date_created": "2019-02-27",
"user_id" : 1,
"tag_id": 1,
"answer_count": 0,
"view_count": 0
}
QuestionRepository.java:
#Override
public void save(Question theQuestion) {
// get the current hibernate session
Session currentSession = entityManager.unwrap(Session.class);
// save employee
currentSession.saveOrUpdate(theQuestion);
}
Null values are being inserted for user_id and tag_id though I used JoinColumn().
MySQL:
As #Karol Dowbecki Suggested,
convert the JSON to DTO object and use that DTO to get the User, Tag Entities from their respective repositories.
Finally create the Question entity object and store it.
Question Entity
#Entity
#Table(name = "question")
public class Question {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "title")
private String title;
#Column(name = "body")
private String body;
#Temporal(TemporalType.DATE)
#Column(name = "date_created")
private Date dateCreated;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "tag_id")
private Set<Tag> tag;
#Column(name = "answer_count")
private int answerCount;
#Column(name = "view_count")
private int viewCount;
}
User Entity
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
Tag Entity
#Entity
#Table(name = "tag")
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "username")
private String username;
#Temporal(TemporalType.DATE)
#Column(name = "date_created")
private Date dateCreated;
}
DTO Class
public class QuestionDTO {
private Long id;
private String title;
private String body;
private Date dateCreated;
private Long user;
private Long tag;
private int answerCount;
private int viewCount;
}
Test Class
#Service
public class TestService {
#Autowired
private QuestionRepository questionRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private TagRepository tagRepository;
public void addQuestion(QuestionDTO dto) {
Tag tag = null;
User user = null;
Question question = null;
Set<Tag> tags = null;
tag = tagRepository.findById(dto.getTag());
tags = new HashSet<>();
tags.add(tag);
user = userRepository.findById(dto.getUser());
question = new Question();
question.setTag(tags);
question.setUser(user);
question.setId(dto.getId());
question.setBody(dto.getBody());
question.setTitle(dto.getTitle());
question.setViewCount(dto.getViewCount());
question.setAnswerCount(dto.getAnswerCount());
question.setDateCreated(dto.getDateCreated());
questionRepository.save(question);
}
}
NOTE :
The relation between Question and Tag are in OneToMany you have to use Collection type.
You have a mismatch between JSON and #Entity structure. JSON contains numeric identifiers while the #Entity contains actual objects representing relationships. You most likely should introduce a separate DTO class to map this JSON while in #Repository you should load User and Tag objects based on their id or create new ones. You already have CascadeType.ALL so Hibernate will cascade the persist operation.
Generally the controller layer should be separate from repository layer unless you are doing something very, very simple. This helps to evolve the service without changing the API contract e.g. adding new columns for auditing changes. By exposing the #Entity as DTO you make your life harder down the road.
You should add referencedColumnName in your Child Entity Foreign Key Column
referencedColumnName="your primaray key column name"
EDIT:
referencedColumnName
The name of the column referenced by this foreign key column.
When used with entity relationship mappings other than the cases
described here, the referenced column is in the table of the target
entity.
When used with a unidirectional OneToMany foreign key mapping, the
referenced column is in the table of the source entity.
When used inside a JoinTable annotation, the referenced key column is
in the entity table of the owning entity, or inverse entity if the
join is part of the inverse join definition.
When used in a CollectionTable mapping, the referenced column is in
the table of the entity containing the collection.
Default (only applies if single join column is being used): The same
name as the primary key column of the referenced table.
Asset is Parent Entity and AssetDetails is Child Entity
Here I have taken OneToOne Relationship
Asset.java
#Entity
#Table(name="asset")
public class Asset {
#Id
#GeneratedValue
#Column(name="assetid")
private BigInteger assetid;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "asset")
#JsonBackReference
private AssetDetails assetDetails;
public AssetDetails getAssetDetails() {
return assetDetails;
}
public void setAssetDetails(AssetDetails assetDetails) {
this.assetDetails = assetDetails;
assetDetails.setAsset(this);
}
public Asset(your fields, AssetDetails assetDetails) {
super();
// your fields
this.assetDetails = assetDetails;
this.assetDetails.setAsset(this);
}
public Asset() {
super();
}
public BigInteger getAssetid() {
return assetid;
}
public void setAssetid(BigInteger assetid) {
this.assetid = assetid;
}
}
AssetDetails.java
#Entity
#Table(name="assetDetails")
public class AssetDetails {
#Id
#GeneratedValue
private BigInteger assetdetailid;
#JoinColumn(name = "assetid",nullable = false, updatable = false,referencedColumnName="assetid")
#OneToOne(cascade=CascadeType.ALL)
#JsonManagedReference
private Asset asset;
public Asset getAsset() {
return asset;
}
public void setAsset(Asset asset) {
this.asset = asset;
}
public AssetDetails(your fields,Asset asset) {
super();
//your fields
this.asset = asset;
}
}

How to load only specified attributes in subgraph in an #NamedEntityGraph

I want to load an UserReference object from the database, but from the verifier attribute I want to load only the id, firstName and lastName, so that the userReference would look something like this:
{
"id": 1,
"company": "company1",
"companyContactName": "some name",
"companyPosition": "programmer",
"referenceDate": "02/04/2005",
"verifier": {
"id":1
"firstName": "Jane",
"lastName": "Smith"
"email":null,
"username":null,
"office:null,
"department":null
}
}
I used an entity graph for the UserReference class, but the one I used loads all the information that the user has, including the email, username, office and department.
Is there any way to specify something like EntityGraphType.FETCH to the subgraph, so that it will load only the id, firstName and lastName for the verifier?
This is my UserReferenceRepository:
public interface UserReferenceRepository extends JpaRepository<UserReference, Long>{
#EntityGraph(value = "userReferenceGraph" , type = EntityGraphType.FETCH )
UserReference findOne(Long id);
}
The UserReference class:
#Getter
#Setter
#EqualsAndHashCode (exclude = {"id", "verifier"})
#ToString(exclude = {"id"})
#Entity
#NamedEntityGraphs({
#NamedEntityGraph(
name = "userReferenceGraph",
attributeNodes = {
#NamedAttributeNode(value = "verifier", subgraph = "verifierGraph")
},
subgraphs = {
#NamedSubgraph(
name = "verifierGraph",
type = User.class,
attributeNodes = {
#NamedAttributeNode(value = "id"),
#NamedAttributeNode(value = "firstName"),
#NamedAttributeNode(value = "lastName")})})
})
public class UserReference {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", referencedColumnName = "user_id", foreignKey = #ForeignKey (name = "FK_UserReference_UserHRDetails_user_id"))
#JsonIgnore
private UserHRDetails hrDetails;
private String company;
private String companyContactName;
private String companyPosition;
private Date referenceDate;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "verifier_id")
private User verifier;
}
and User:
#Getter #Setter
#EqualsAndHashCode(exclude = {"id", "department", "company", "authorities", "hrDetails"})
#ToString(exclude = {"password"})
#Entity
#AllArgsConstructor
#Builder
public class User implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Access(value = AccessType.PROPERTY)
private Long id;
#Size(max = 50)
#Column(name = "first_name", length = 50)
private String firstName;
#Size(max = 50)
#Column(name = "last_name", length = 50)
private String lastName;
#Column(length = 100, unique = true, nullable = false)
private String email;
#Column(length = 50, unique = true, nullable = false)
private String username;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "department_id")
private Department department;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "office_id")
private Office office;
}
I suppose you are using Jackson for generating JSON. In this case this is a battle Jackson vs. Entity Graph and former has no chance to win this battle. Entity Graph is just a hint for building SQL query and you can only tell Hibernate to not load some attributes. Hibernate still does not support Entity Graphs when it loads basic Entity fields, see https://hibernate.atlassian.net/browse/HHH-9270. But main problem is that Jackson will call every getter in your Entity during JSON generation and Hibernate will lazy load them not taking into consideration your Entity Graph. I can propose only #JsonIgnore usage, but that may be not as flexible as you need.
I've met the same problem, and i see 2 ways to solve it:
FAST:
You can make some #PostLoad action in your entity and nullate that field you don't need.
#PostLoad
private void postLoad() {
if (verifier != null) {
verifier.email = null;
verifier.office = null;
verifier.department = null;
}
}
CORRECT:
Another way is to protect your entities with converting them to DTOs. Create separate POJOs and convert your User and UserReference to that DTO POJO classes. There you definitely will have more control over your response.

Categories

Resources