Am relatively new to springboot and I have a scenario.I have users and I have jobs. A user can apply to many jobs and a job can be applied by multiple user.I have job and user entities with bidirectional relationship as follows
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "job_applications")
#JsonBackReference
private Collection<JobApplicant> applicants= new ArrayList<>();
#ManyToMany(mappedBy = "applicants",fetch = FetchType.LAZY)
#JsonManagedReference
private Collection<Job> jobs= new ArrayList();
The challenge I want to associate a user with a job programatically and this is what I was trying in my service class
public void addJobToUserJobList(int useId,int jobId) {
Job job=jobRepository.getOne(jobId);
System.out.println("Job found: "+job.getName());
JobApplicant applicant= applicantRepo.getOne(useId);
Set<Job>jobslist= new HashSet();
jobslist.add(job);
applicant.setJobs(jobslist);
applicantRepo.save(applicant);
jobRepository.save(job);
}
When I hit applicants/1/25 end-point this code in the controller the join table is not updated.
#RequestMapping(value = "/applicants/{userid}/{jobId}", method = { RequestMethod.PUT,
RequestMethod.GET })
public void applyToJob(#PathVariable int userid,#PathVariable int jobId) {
jobService.addJobToUserJobList(userid, jobId);
}
Is there a better approach to doing this ?
Turns out the issue was the owning side of my relatioship. I was saving updating my jobApplicant entity which was not the owning side hence it could not update the join table. I made the entity the owning side and it worked as below
#ManyToMany(mappedBy = "jobs",cascade = CascadeType.ALL)
#JoinTable(name = "job_applications")
#JsonBackReference
private Collection<JobApplicant> applicants= new ArrayList<>();
#ManyToMany(fetch = FetchType.LAZY)
#JsonManagedReference
private Collection<Job> jobs= new ArrayList();
Related
I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading
I am using Java+JPA+Hibernate+Playframework1 in a web application and have models Classroom, Student and Group (see below). Each model has exactly one instance in database, and those instances are related to each other. Ideally, deleting a classroom should also delete it's groups, and dissociate it's students, deleting a student should dissociate it's classrooms and groups, and deleting a group should dissociate it's students, but it is not working.
|Classroom|[Many]-------[Many]|Student|
[One] [Many]
| /
| /
[Many] /
|Group|[Many]--------------ยด
When I try to delete a classroom, a student or a group they are simply not deleted. No runtime exception, no SQL error, no nothing. It just fails silently. I even activated sql debug and didn't see any delete, just selects.
Update: Somehow, courses with groups not related to students are deleted correctly.
Here are mappings:
#Entity(name = "r_classroom")
public class Classroom extends Model {
#OneToMany(mappedBy = "classroom", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Group> groups = new ArrayList<>();
#ManyToMany(mappedBy = "classrooms", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Student> students = new ArrayList<>();
public String name;
}
#Entity(name = "r_student")
public class Student extends Model {
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Classroom> classrooms = new ArrayList<>();
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST})
public List<Group> groups = new ArrayList<>();
public String name;
}
#Entity(name = "r_group")
public class Group extends Model {
#ManyToOne
public Classroom classroom;
#ManyToMany(mappedBy = "groups", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Student> students = new ArrayList<>();
public String name;
}
The Model class is part of Play Framework. I am using Play 1.4.4.
I believe something is wrong with my mappings, but what?
I posted the source code at github.
I'm working on a java spring mvc application with hibernate. I have two Entities Acl and AclGroupand These two entities have Many to Many relationship with a join table. But, when I save an AclGroup object, hibernate doesn't insert any record in join table and just inserts into AclGroup table. Here is structure of my classes:
Acl.java:
public class Acl implements Serializable{
...
#JoinTable(name = "acl_group_acl", joinColumns = {
#JoinColumn(name = "acl_id", referencedColumnName = "id")}, inverseJoinColumns = {
#JoinColumn(name = "acl_group_id", referencedColumnName = "id")})
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Collection<AclGroup> aclGroupCollection;
public Collection<AclGroup> getAclGroupCollection() {
return aclGroupCollection;
}
public void setAclGroupCollection(Collection<AclGroup> aclGroupCollection) {
this.aclGroupCollection = aclGroupCollection;
}
...
}
AclGroup.java:
public class AclGroup implements Serializable{
...
#ManyToMany(mappedBy = "aclGroupCollection",fetch = FetchType.LAZY)
private Collection<Acl> aclCollection;
public Collection<Acl> getAclCollection() {
return aclCollection;
}
public void setAclCollection(Collection<Acl> aclCollection) {
this.aclCollection = aclCollection;
}
}
Here is how I save my object:
AclGroup aclGroup = new AclGroup();
List<Acl> acls = new ArrayList<>();
/*
add some elements to acls
*/
aclGroup.setAclCollection(acls);
/*
hibernate config and opening a session
*/
session.save(aclGroup); //session.persist also did not work
Could anyone help me to solve this problem? Thank you for your attention.
The owner side of the association is Acl. AclGroup is the inverse side (since it has the mappedBy attribute). Hibernate only cares about the owner side.
So make sure to add the group to the acl when you add the acl to the group: that will work whatever the owner side is, and will make your graph coherent. Or, if you absolutely don't want to do that, put the mapping annotations on AclGroup, and make Acl the inverse side.
Assume a product can have a list of accounts, the relationship between product and account is #OneToMany. In product class, I have created a Set interface to store accounts.
Next, I have made a method using Struts 2 to add accounts to a product. Each time I run this method, the HashSet is reset and objects are removed from it. Below, you can find the DefaultProduct class.
#Entity
public class DefaultProduct {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID", nullable = false)
Long id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "product", targetEntity = DefaultAccount.class)
private final Set<Account> accounts = new HashSet<Account>();
public DefaultProduct(Long id, Set<Account> accounts) {
Assert.notEmpty(accounts);
this.accounts.addAll(accounts);
}
// Getters
}
The Struts 2 action method calls editProduct() method from the service class, which is Dependency Injected using Spring framework. The service class does not have any issues as this has been accurately tested. Below you can find methods from service class and Struts 2.
Service:
#Transactional
public void editProduct(Product product) {
entityManager.merge(product);
}
Struts 2:
public String addAccount(){
Set<Account> accounts = new HashSet<Account>();
accounts.add(coreService.findAccountById(accountId));
coreService.editProduct(new DefaultProduct(id, accounts));
System.out.println(coreService.findProductById(id).getAccounts().size()); //Size-check
return SUCCESS;
}
Note: Product is an interface implemented by DefaultProduct class.
Many thanks
One thing you have to add to one-to-many association is cascade type.
#OneToMany(fetch = FetchType.LAZY, mappedBy = "product", targetEntity = DefaultAccount.class, cascade = {CascadeType.MERGE})
private final Set<Account> accounts = new HashSet<Account>();
I have the following entities:
#Entity
#Table(name = "place_revision")
public class PoiRevision {
#OneToMany(mappedBy = "pk.revision", cascade = {CascadeType.ALL})
private Collection<PoiRevisionCategory> categoryMapping;
// ...
}
#Entity
#Table(name = "place_revision__category")
#AssociationOverrides({
#AssociationOverride(name = "pk.revision",
joinColumns = #JoinColumn(name = "place_revision_id")),
#AssociationOverride(name = "pk.category",
joinColumns = #JoinColumn(name = "category_id"))
})
public class PoiRevisionCategory {
#EmbeddedId
private PoiRevisionCategoryId pk = new PoiRevisionCategoryId();
// ...
}
#Embeddable
public class PoiRevisionCategoryId implements Serializable {
#ManyToOne
private PoiRevision revision;
#ManyToOne
private Category category;
// ...
}
#Entity
#Table(name = "category")
public class Category {
#ManyToMany(targetEntity = Section.class, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
#JoinTable(
name = "category__section",
joinColumns = #JoinColumn(name = "category_id"),
inverseJoinColumns = #JoinColumn(name = "section_id")
)
private Collection<Section> sections;
// ...
}
And want to select PoiRevisions that have Categories that have some Sections.
I'm using Spring-data Specification to query the database for these entities.
My intent is to write something like:
Specification<PoiRevision> spec = new Specification<PoiRevision>() {
#Override
public Predicate toPredicate(Root<PoiRevision> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> conditions = new ArrayList<>(CONDITION_COUNT);
CollectionJoin<PoiRevision, PoiRevisionCategory> mapping = root.join(PoiRevision_.categoryMapping);
// BROKEN here as we cannot use nested path for joins
Join<PoiRevisionCategory, Category> categories = mapping.join("pk.category");
conditions.add(categories.get("sections").in(sections));
// ...
return cb.and(conditions.toArray(new Predicate[] {}));
}
};
But we cannot use nested path for such joins as JPA provider (Hibernate, in my case) looks only for direct properties of PoiRevisionCategory class. And we cannot "join" embedded Id to our result set because it's not a manageable entity.
I'm really stuck with this issue which seems to be far from complicated when translated into SQL yet it has some complexity on the ORM-side.
Any idea is much appreciated.
After switching completely to metamodel API it became clearer and I was actually able to join embedded entity just like I tried and failed with string api.
So the correct way is just to join like one would normally do
Join<PoiRevisionCategory, PoiRevisionCategoryId> pk = mapping.join(PoiRevisionCategory_.pk);
Join<PoiRevisionCategoryId, Category> cats = pk.join(PoiRevisionCategoryId_.category);
CollectionJoin<Category, Section> sec = cats.join(Category_.sections);
conditions.add(sec.get(Section_.id).in(sections));
And it does the thing just fine!
What a relief.