skip to Main Content

I have two Entities one as

@Entity    
public class Job {  

    @Id
    @GeneratedValue
    private Integer id;

    @Size(min = 5,max = 50, message = "Valid title is 5-50 chars")
    private String title;       

    @ManyToMany(cascade = {CascadeType.MERGE})
    @JoinTable(name = "jobs_categories",
                joinColumns = @JoinColumn(name = "jobs_id"),
                inverseJoinColumns = @JoinColumn(name = "categories_id"))
    private List<Category> categories;
    // omitting setters/getters
}

and other as

@Entity
public class Category {    

    @Id
    @GeneratedValue
    private Integer id;

    @Size(min = 5,max = 15, message = "Valid name is 5-15 chars")
    private String name;

    @ManyToMany(mappedBy = "categories", cascade = {CascadeType.MERGE}) 
    private List<Job> jobs; 
    // omitting setters/getters
    }

I want to implement CRUD operations on those two,
I save one job with some categories as

Job title - Fullstack JavaScript CSS3 / HTML5 / Developer
Categories - AngularJS, Backbone.js, CSS, CSS3, Graphic design, HTML, HTML5,
             JavaScript, MongoDB Node.js, twitter bootstrap, Web design.

then if I want to update the job by adding or removing categories from above saved Job, it is never updating (e.g, adding/removing) categories.

for updating Job I have method in JobService as

@Service
public class JobService{

    @Autowired
    private JobRepository jobRepository;

    @Autowired
    private CategoryRepository categoryRepository;

    @Autowired
    private LocalContainerEntityManagerFactoryBean entityManagerFactory;

    private Session session;

    Logger logger = LoggerFactory.getLogger(JobService.class);      

    public void updateJobCategory(Job job) {                
    EntityManager entityManager = entityManagerFactory.getObject().createEntityManager();
    session= entityManager.unwrap(org.hibernate.Session.class);
    logger.info("got Job ID as  ==> " + String.valueOf(job.getId()));
    Job jobObj = (Job) session.get(Job.class, job.getId());
    jobObj.getCategories().clear();     
    logger.info("got Job ID as  ==> " + String.valueOf(jobObj.getId()));

    if (!job.getCategories().isEmpty()) {
        logger.info("got job.getCategories() as not Null");
        List<Category> categories = job.getCategories();
        for (Category category : categories) {              
            logger.info("Good job.getCategories() to update ==> " + category.getName());
        }
        logger.info("adding job.getCategories() to update ==> ");
        jobObj.setCategories(job.getCategories());      

    }else {
        logger.info("got job.getCategories() as Null");
    }
    session.update(jobObj); 

    }
    public void update(Job job) {
        jobRepository.update(job.getId(), job.getTitle());
        updateJobCategory(job);
    }
}

I get log

logger.info("Got job.getCategories() to update" + category.getName());

I’m getting updated (new added/old removed) list of all categories,

Hibernate: update job set title=? where id=?
INFO : com.rhcloud.jobsnetwork.service.JobService - got Job ID as  ==> 1
Hibernate: select job0_.id as id1_1_0_, job0_.title as title7_1_0_ from job job0_ where job0_.id=?
Hibernate: select categories0_.jobs_id as jobs_id1_1_0_, categories0_.categories_id as categori2_2_0_, category1_.id as id1_0_1_, category1_.name as name2_0_1_ from jobs_categories categories0_ inner join category category1_ on categories0_.categories_id=category1_.id where categories0_.jobs_id=?
INFO : com.rhcloud.jobsnetwork.service.JobService - got Job ID as  ==> 1
INFO : com.rhcloud.jobsnetwork.service.JobService - got job.getCategories() as not Null
INFO : com.rhcloud.jobsnetwork.service.JobService - Good job.getCategories() to update ==> iOS App Dev
INFO : com.rhcloud.jobsnetwork.service.JobService - Good job.getCategories() to update ==> Android App Dev
INFO : com.rhcloud.jobsnetwork.service.JobService - adding job.getCategories() to update

but when I see details of Job I get the categories listed which were added on the creation of job not updated ones, I guess it is not updating categories id values in junction table jobs_categories.
No idea of Updating categories related to job?

UPDATE

When I use

public void persistJob(Job newJob) {
         em.persist(newJob);
       }

       public void saveJob(Job job) {
          em.merge(job);
       }
       public void persistCategory(Category newcat) {
         em.persist(newcat);
       }

without @Transactional at every method I get expception

javax.persistence.TransactionRequiredException: No transactional EntityManager available
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:275)
    at com.sun.proxy.$Proxy167.persist(Unknown Source)
    at com.rhcloud.jobsnetwork.service.JobService.persistJob(JobService.java:29)
    at com.rhcloud.jobsnetwork.service.JobService$$FastClassBySpringCGLIB$$53974ce3.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:649)
    at com.rhcloud.jobsnetwork.service.JobService$$EnhancerBySpringCGLIB$$c08e8d16.persistJob(<generated>)
    at com.rhcloud.jobsnetwork.controllers.JobController.addJobDetail(JobController.java:86)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:776)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:705)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:650)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

After adding @Transactional it works got rid of above exception, I save new job using

@RequestMapping(value = "/add-job", method = RequestMethod.POST)
    public String addJobDetail(@ModelAttribute("job") Job job) {            
        jobService.persistJob(job); 
        return "redirect:/";
    } 

I get all the things working perfect, but when I use saveJob in JobController as

@RequestMapping(value = "/updated", method = RequestMethod.POST)
    public String updateJob(@ModelAttribute("job") Job job) {           
        jobService.saveJob(job);
        return "redirect:/";
    } 

there is no any category(no earlier added no updated ones) saved in updated job, any suggestions in this case.

Update – 2

As per your suggestion I added logger to

 @Transactional
   public void saveJob(Job job) {
       logger.error("got job.getCategories() of " + job.getCategories().size());
      em.merge(job);         
   }

at this point I get NullPointerException at

 logger.error("got job.getCategories() of " + job.getCategories().size() + " size");

log/trace is

java.lang.NullPointerException
    at com.rhcloud.jobsnetwork.service.JobService.saveJob(JobService.java:37)
    at com.rhcloud.jobsnetwork.service.JobService$$FastClassBySpringCGLIB$$53974ce3.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:717)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:653)
    at com.rhcloud.jobsnetwork.service.JobService$$EnhancerBySpringCGLIB$$41dc2ac2.saveJob(<generated>)
    at com.rhcloud.jobsnetwork.controllers.JobController.updateJob(JobController.java:109)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:776)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:705)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:650)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

2

Answers


  1. You have wrong mapping.
    Try this:

    @Entity
    public class Category {    
    
    @Id
    @GeneratedValue
    private Integer id;
    
    @Size(min = 5,max = 15, message = "Valid name is 5-15 chars")
    private String name;
    
    @ManyToMany(cascade = CascadeType.ALL,orphanRemoval = true) 
    @JoinColumn(name = "jobs_id",referencedColumnName = "jobs_id")
    private List<Job> jobs; 
    // omitting setters/getters
    }
    

    and second entity:

    @Entity    
    public class Job {  
    
    @Id
    @GeneratedValue
    private Integer id;
    
    @Size(min = 5,max = 50, message = "Valid title is 5-50 chars")
    private String title;       
    
    @ManyToMany(cascade = CascadeType.ALL,orphanRemoval = true)
    @JoinColumn(name = "categories_id",referencedColumnName = "categories_id")
    private List<Category> categories;
    // omitting setters/getters
    }
    

    P.s. edit column names as in table;

    Login or Signup to reply.
  2. In your case, you have two entities, that can each exist on their own – category is fine without a job, and a job can have no categories assigned. orphanRemoval is used in OnetoMany or OneToOne, if you would like to perform a delete operation on the related entity that is no longer referenced from the parent entity(think composition), which isn’t the case here. I believe you merely want to remove the link between the job and a the particular Category(think association).

    You mapping is correct (bidirectional manytomany with Job as the relationship owner), I would suggest only minor modifications:
    Use java.util.Set(improve performance with delete on join table, prevent ‘can not fetch multiple bags’ exception) for you collections and override equals method(to make collection operations easier).

    Here we go:

    @Entity    
    public class Job { 
    @Id
    @GeneratedValue
    private Integer id;
    
    private String title;
    
    @ManyToMany(fetch=FetchType.EAGER,cascade = { CascadeType.MERGE })
    @JoinTable(name = "jobs_categories", joinColumns = @JoinColumn(name = "jobs_id") , inverseJoinColumns = @JoinColumn(name = "categories_id") )
    private Set<Category> categories = new HashSet<Category>();
    

    And Category

    @Entity
    public class Category {
    
    @Id
    @GeneratedValue
    private Integer id;
    
    private String name;
    
    @ManyToMany(mappedBy = "categories", cascade = { CascadeType.MERGE })
    private List<Job> jobs;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Category other = (Category) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }
    

    One of the issues in your update function is that you override a reference to the collection in the Job entity.

    jobObj.setCategories(job.getCategories());

    You should always modify the original collection, never assign a new collection.

    You might also consider removing the Merge cascade altogether from your ManyToMany as it will make the code workflow easier to understand – categories will be managed separately and you only add already saved categories to the new Job instance(but this is just a suggestion, we will make it work even with the cascade 🙂 ).

    Now for some test code. I am using plain JPA, not Spring repositories, to make it simple to understand.

    JobService(in your case it will be split into multiple classes, repositories..):

    public class JobService {
    
       @PersistenceContext
       EntityManager em;
    
       public void persistJob(Job newJob) {
         em.persist(newJob);
       }
    
       public Job getJob(Integer jobId) {
         return em.find(Job.class, jobId);
       }
    
       public void saveJob(Job job) {
          em.merge(job);
       }
    
       public void persistCategory(Category newcat) {
         em.persist(newcat);
       }
    
       public List<Category> getAllCategories() {
         return em.createQuery("from Category", Category.class).getResultList();
       }  
    }
    

    And lets test it:

    Job job = new Job();
    job.setTitle("Web developer");
    Category catJS = new Category();
    catJS.setName("javascript");
    Category catCSS = new Category();
    catCSS.setName("CSS");
    Category catNG = new Category();
    catNG.setName("AngualrJS");
    Category catHTML = new Category();
    catHTML.setName("HTML5");
    //persist categories separately
    jobservice.persistCategory(catNG);
    jobservice.persistCategory(catCSS);
    jobservice.persistCategory(catHTML);
    jobservice.persistCategory(catJS);
    //at this point, we have 4 categories as managed entities
    //add the saved categories to a new job instance
    job.getCategories().add(catHTML);
    job.getCategories().add(catJS);
    
    //save job in DB
    jobservice.persistJob(job);
    //we do not need to set Job references in Category objects, because Job is the relationship owner - it will correctly update the FKs in join table
    assertNotNull("job should be saved", job.getId());
    Job fromDb = jobservice.getJob(job.getId());//reload from DB
    assertNotNull("job should be stored in DB", fromDb);
    assertTrue("job should have 2 categories", fromDb.getCategories().size() == 2);
    //great si far so good  
    
    //remove one of the assigned categories from Job(this is where you need that **equals** method in Category )
    fromDb.getCategories().remove(catHTML);
    assertTrue("job should not have only one category in Memory", fromDb.getCategories().size() ==1);
    jobservice.saveJob(fromDb);
    //we do not want to delete the *HTML* category from DB, just remove it from the particular job
    Job fromDbAfterUpdate = jobservice.getJob(job.getId());
    assertTrue("job should not have only one category in BD", fromDbAfterUpdate.getCategories().size() ==1);
    List<Category> allCategories = jobservice.getAllCategories();
    assertTrue("there should still be 4 categories in DB", allCategories.size() == 4);
    
    
    //now lets test the Merge cascade
    Category unsavedCategory = new Category();
    unsavedCategory.setName("jquery");
    fromDbAfterUpdate.getCategories().add(unsavedCategory );
    //we have added an unsaved category to an  existing job. As Job has cascade merge on it categories, the *unsavedCategory* will be saved and then linked to job via job_category join table
    jobservice.saveJob(fromDbAfterUpdate);
    fromDbAfterUpdate = jobservice.getJob(fromDbAfterUpdate.getId());
    assertTrue("job should now have 2 categories", fromDbAfterUpdate.getCategories().size() ==2);
    
    
    // one more test, add and remove at the same time
    fromDbAfterUpdate.getCategories().remove(catJS);
    fromDbAfterUpdate.getCategories().add(catHTML);
    fromDbAfterUpdate.getCategories().add(catCSS);
    jobservice.saveJob(fromDbAfterUpdate);
    fromDbAfterUpdate = jobservice.getJob(fromDbAfterUpdate.getId());
    assertTrue("job should now have 3 categories", fromDbAfterUpdate.getCategories().size() ==3);
    allCategories = jobservice.getAllCategories();
    assertTrue("there should be 5 categories in DB", allCategories.size() == 5);
    

    TL;DR

    • Keep your update method simple – you do not need to fetch the object from DB and move things between managed and detached object, let JPA merge do its thing
    • Never change a reference to a collection of managed entities – perform operations directly on the collection
    • Omit any cascades until you really need them

    For more info, please consult JPA Spec

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search