Is there a way configure model mapper to automatically map parent id to parent ids in nested child object?
Parent entity
#Entity
#Table(name = "parent")
public class ParentEntity {
#Id
#Column(name = "id")
private Long id;
#Basic
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "parent_id", referencedColumnName = "id")
private List<ChildEntity> child;
Child entity
#Entity
#Table(name = "child")
public class ChildEntity {
#Id
#Column(name = "id")
private Long id;
#Basic
#Column(name = "parent_id")
private Long parentId;
#Basic
#Column(name = "name")
private String name;
Parent DTO
public class ParentDto {
private Long id;
private String name;
private List<ChildDto> children;
Child DTO
public class ChildDto {
private Long id;
private Long parentId;
private String name;
Currently Im using model mapper and it conversion works for the most part, and it creates a ParentEntity object with a list of ChildEntity object. Now Im looking for a way to populate the "parentId" field in each ChildEntity with modelmapper. Thanks in advance!
/*
{
id: 1,
name: "dad",
children: [
{
id:10,
name: "child",
},
{
id:20,
name: "child2",
}
]
}
*/
modelMapper.map(parentDto, ParentEntity.class)
The challenge is that when ChildDto is mapped you must have access to the ParentDto that has the List children in which the ChildDto to be mapped is added. Otherwise the mapper does not know about that id.
Luckily you can access ParentDto with a little help from a org.modelmapper.Converter. Implement this interface like:
public Converter<ChildDto, ChildEntity> converter = new Converter<>() {
private final ModelMapper mm = new ModelMapper();
#Override
public ChildEntity convert(MappingContext<ChildDto, ChildEntity> context) {
// map it first normally
ChildEntity ce = mm.map(context.getSource(), ChildEntity.class);
try {
// 1st parent is the List 2nfd is the ParentDto
ParentDto parentDto = (ParentDto)context.getParent().getParent().getSource();
ce.setParentId(parentDto.getId());
} catch (Exception e) {
// well, maybe it was not ParentDto that expected to be the grandparent.
}
return ce;
}
};
Then use it like:
ModelMapper mm = new ModelMapper();
mm.addConverter(converter);
and ChildDto that has no parentId should still be mapped to ChildEntity with parentId.
Related
I have my category entity as shown below. A category can have many children categories or none. I want a solution where I can delete a parent category, and if this parent has children categories, I want to set the FK in these children to NULL.
#ManyToOne(fetch = FetchType.EAGER)
#JsonBackReference
#JoinColumn(name="parent_id", referencedColumnName = "category_id")
private Category parentCategory;
#OneToMany(fetch = FetchType.EAGER, mappedBy="parentCategory", cascade = {CascadeType.MERGE, CascadeType.PERSIST})
#JsonManagedReference
private List<Category> childCategories = new ArrayList<Category>();
public List<Category> getChildCategories() {
return childCategories;
}
public void setParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
I have tried the below solution but it doesn't work:
#PersistenceContext
private EntityManager em;
public void delete(String id) throws Exception{
try {
Category parent = this.findById(id);
for(Category child : parent.getChildCategories()){
child.setParentCategory(null);
this.em.merge(child);
}
this.em.remove(parent);
}catch (TransactionalException ex){
throw new Exception("There is no transaction for this entity manager");
}
}
Create a mapped super class with reusable fields to be used in every entity
#MappedSuperclass
#Audited
public class ReusableFields implements Serializable
{
public static final String SOFT_DELETED_CLAUSE = "is_deleted = 'false'";
#Column(name="is_deleted", columnDefinition="BOOLEAN DEFAULT true")
public boolean isDeleted;
#CreationTimestamp
#Column(name = "created_at")
#JsonProperty("created")
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss ")
private Date created;
#Column(name = "updated_at")
#JsonProperty("updated")
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
#UpdateTimestamp
private Date modified;
//getter setter
In your entities, add a where clause to exclude deleted records
Parent - Category entity
#Entity
#Table(name = "Category")
#Audited
#Where(clause = ReusableFields.SOFT_DELETED_CLAUSE)
public class Category extends ReusableFields
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
Long categoryId;
#NonNull
#JsonDeserialize(using = ToTitleCaseDeserializer.class)
#Column(name="category_name")
String categoryName;
#JsonDeserialize(using = ToSentenceCaseDeserializer.class)
String categoryDescription;
//Gettersetter
}
Child - Product Entity
#Entity
#Table(name = "Product")
#Audited
#Where(clause = ReusableFields.SOFT_DELETED_CLAUSE)
public class Product extends ReusableFields
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
Long productId;
#NonNull
#Column(name="product_name")
String productName;
String productDescription;
String measurementUnit;
Double reorderQuantity;
#ManyToOne(fetch=FetchType.LAZY,cascade = CascadeType.ALL)
#JoinColumn(name="categoryId",nullable=false)
Category category;
Add a softdelete repository and delete method to mark any record as deleted
#NoRepositoryBean
public interface BaseRepository<T, ID> extends JpaRepository<T, ID>
{
default void softDelete(T entity) {
Assert.notNull(entity, "The entity must not be null!");
Assert.isInstanceOf(ReusableFields.class, entity, "The entity must be soft deletable!");
((ReusableFields)entity).setDeleted(true);
save(entity);
}
default void softDeleteById(ID id) {
Assert.notNull(id, "The given id must not be null!");
this.softDelete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
String.format("No %s entity with id %s exists!", "", id), 1)));
}
}
Extend this repository to your actual repository
#Repository
public interface ProductRepo extends BaseRepository<Product, Long>
{
}
Now in your category service class, fetch all products linked to category you want to delete. Then iterate over product list and soft delete them. Then softdelete category.
public void deleteCategory(Long id) throws Exception
{
List<Product> products = pRepo.findByCategoryId(id);
for(Product product: products)
{
productRepo.softDeleteById(product.getProductId());
}
categoryRepo.softDeleteById(id);
}
I have the following "parent class" :
#Entity
#Table(name = "parent_table")
public class Parent {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long parentId;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "Parent", cascade = CascadeType.ALL)
List<Child> children;
}
I also have the following child class :
#Entity
#Table(name = "children")
public class Child {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long childId;
#ManyToOne
#JoinColumn(name="parent_id")
private Parent parent;
private String name;
}
I am sending up the following request in postman :
{
"firstName": "Test",
"lastName": "Parent",
"children":[{
"name":"jack"
},
{
"name":"jill"
}
]
}
When I ask the parent repository to save the parent, it does, but nothing happens for the child... it doesn't save to the database at all...
For reference, this is my line that saves the parent
parentRepository.save(parent)
(the parent in this case, has the two children inside of it - but they don't get saved to the children table).
I ran your example and it seems to be working correctly, only thing i change was mappedBy property of #OneToMany annotation. It must be lower case.
#OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
List<Child> children = new ArrayList<>();
Parent
#Entity
#Table(name = "parent_table")
public class Parent {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "parent_id")
private Long parentId;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
List<Child> children = new ArrayList<>();
}
Child
#Entity
#Table(name = "children")
public class Child {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long childId;
#ManyToOne
#JoinColumn(name="parent_id")
private Parent parent;
private String name;
}
Test
Above code makes next unit case to succeed:
#Test
public void parentRepositoryMustPersistParentAndChildren() {
Parent parent = new Parent("Anakin", "Skywalker");
parent.getChildren().add(new Child("Luke"));
parent.getChildren().add(new Child("Leia"));
Parent saved = parentRepository.save(parent);
Assert.assertNull("Parent does not have and id assigned after persist it", saved.getParentId());
saved.getChildren().forEach((child) ->{
Assert.assertNull("Parent does not have and id assigned after persist it", child.getChildId());
});
}
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;
}
}
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.
I have following JPA entity structure.
#Entity
#Table(name = "PARENT_DETAILS")
class Parent{
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "parent_details_seq")
#SequenceGenerator(name = "parent_details_seq", sequenceName = "PARENT_DETAILS_SEQ", allocationSize = 1)
#Column(name = "PARENT_ID")
private long parentId;
#Column(name = "PARENT_NAME")
private String parentName;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "childPK.parent", cascade = CascadeType.ALL)
private Set<Child> child;
//setters and getters
}
#Entity
#Table(name = "CHILD_DETAILS")
public class Child {
private ChildPK childPK;
public void setProgramProcessesPK(ChildPK childPK) {
this.childPK = childPK;
}
#EmbeddedId
public ChildPK getChildPK() {
return childPK;
}
}
#Embeddable
public class ChildPK implements Serializable {
private static final long serialVersionUID = 1L;
private Parent parent;
private long childId;
#Column(name = "CHILDID")
public long getChildId() {
return childId;
}
public void setChildId(long childId) {
this.childId = childId;
}
#ManyToOne
#JoinColumn(name = "PARENT_ID", referencedColumnName = "PARENT_ID", nullable = false)
public ParentDetails getParent() {
return parent;
}
}
I want to write a JPA query which will return the PARENT_NAME and the count of all children for a given parent_id.
Tthe only solution I can think of is joining and writing a complex criteria query.
I cannot think of a way to get the result using a simple JPA query.
Is there an easier way to do this?
Have you tried SIZE? Something like "Select parent.parentName, Size(parent.child) from Parent parent" might work.
You can use JPA Named Query such as this:
private static class ParentChildsNumber {
public String parentName;
public Integer childsNum;
public ParentChildsNumber(String parentName, Integer childsNum) {
this.parentName = parentName;
this.childsNum = childsNum;
}
}
#NamedQuery(name="getParentChildsNumberQuery", query="SELECT NEW ParentChildsNumber(p.parentName, SIZE(p.child)) FROM Parent p WHERE p.parentId = :parentId GROUP BY p.parentId, p.parentName")
Use it in your code in the following way:
#PersistenceContext(unitName="YourPersistentUnit")
private EntityManager em;
em.createNamedQuery("getParentChildsNumberQuery", ParentChildsNumber.class).setParameter("parentId", parentId).getSingleResult();