Luke
Luke

Reputation: 768

Getting parent from child using JPA

I have a phone object and a phone history object. I'd like to be able to retrieve a phone history object and then get its parent (phone), but for some reason, when I attempt to do this and then persist the child, I get an exception saying that phone_id isn't populated. Not sure how to fix this, although I suspect it has something to do with my mapping annotations.

Below are my entity classes. I've tried changing the annotations to match the suggestion in the bidirectional relationships section here: http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html/entity.html#entity-mapping-association but this didn't seem to do the trick. I'm new to JPA, so this is probably simple for someone with more experience, but I'm stumped. TIA.

Phone entity:

package com.blah.model;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name = "phones")
public class EPhone implements BaseModel {

    private static final long serialVersionUID = 3497994786207995664L;

    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id",
            nullable = false)
    private EAccount account;

    @Temporal(TemporalType.TIMESTAMP)
    private Calendar created;

    @Column(name = "value",
        nullable = false)
        private Long value;

    @OneToMany(fetch = FetchType.LAZY,
            mappedBy = "phone",
            cascade = CascadeType.ALL)
    private final List<EPhoneHistory> history;

    public EPhone() {
        this(Calendar.getInstance());
    }

    public EPhone(final Calendar createdDate) {
        setCreated(createdDate);
        history = new ArrayList<EPhoneHistory>();
    }

    public void add(EPhoneHistory phoneHistoryItem) {
        history.add(phoneHistoryItem);
    }

    public EAccount getAccount() {
        return account;
    }

    public Calendar getCreated() {
        return created;
    }

    @OneToMany(fetch = FetchType.LAZY,
            mappedBy = "phone",
            cascade = CascadeType.ALL)
    public List<EPhoneHistory> getHistory() {
        return history;
    }

    public Long getId() {
        return id;
    }

    public Long getValue() {
        return value;
    }

    public void setAccount(EAccount account) {
        this.account = account;
    }

    public void setCreated(Calendar created) {
        this.created = created;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setValue(Long value) {
        this.value = value;
    }

}

Phone History entity:

package com.blah.model;

import java.util.Calendar;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name = "phone_history")
public class EPhoneHistory implements BaseModel {

    private static final long serialVersionUID = 4591314258588732076L;

    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long Id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "phone_id",
            nullable = false)
    private EPhone phone;

    @Temporal(TemporalType.TIMESTAMP)
    private Calendar created;

    @Enumerated(EnumType.ORDINAL)
    @Column(name = "type_id")
    private PhoneHistoryType type;

    public Calendar getCreated() {
        return created;
    }

    public Long getId() {
        return Id;
    }

    public EPhone getPhone() {
        return phone;
    }

    public PhoneHistoryType getType() {
        return type;
    }

    public void setCreated(Calendar created) {
        this.created = created;
    }

    public void setId(Long id) {
        Id = id;
    }

    public void setPhone(EPhone phone) {
        this.phone = phone;
    }

    public void setType(PhoneHistoryType type) {
        this.type = type;
    }

}

UPDATE for clarification (this is a test that fails):

package com.blah.model.repository;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import com.blah.AbstractBaseDbTestCase;
import com.blah.model.EPhoneHistory;
import com.blah.model.PhoneHistoryType;

public class PhoneHistoryRepositoryTest extends AbstractBaseDbTestCase {

    @Autowired
    PhoneHistoryRepository phoneHistoryRepo;

    @Test
    @Transactional
    public void testCreateAndSavePhoneHistory() {

        EPhoneHistory existingPhoneHistory = phoneHistoryRepo.find(1L);

        EPhoneHistory newPhoneHistory = new EPhoneHistory();
        newPhoneHistory.setType(PhoneHistoryType.VOICE);

        // I assume this is where I would
        // set the phone, but I don't have it.
        // existingPhoneHistory.setPhone(phone);

        // This is what I'm having trouble with. getPhone() doesn't populate the
        // phone field with the parent phone object.
        existingPhoneHistory.getPhone().add(newPhoneHistory);

        phoneHistoryRepo.persist(existingPhoneHistory);
        phoneHistoryRepo.flush();
    }
}

And the failure stack trace:

org.springframework.dao.DataIntegrityViolationException: NULL not allowed for column "PHONE_ID"; SQL statement:
insert into phone_history (id, created, phone_id, type_id) values (null, ?, ?, ?) [23502-168]; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: NULL not allowed for column "PHONE_ID"; SQL statement:
insert into phone_history (id, created, phone_id, type_id) values (null, ?, ?, ?) [23502-168]
    at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:643)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:104)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:403)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:58)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:163)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:110)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
    at com.blah.model.repository.PhoneHistoryRepository$$EnhancerByCGLIB$$4f90cdde.flush(<generated>)
    at com.blah.model.repository.PhoneHistoryRepositoryTest.testCreateAndSavePhoneHistory(PhoneHistoryRepositoryTest.java:29)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Caused by: org.hibernate.exception.ConstraintViolationException: NULL not allowed for column "PHONE_ID"; SQL statement:
insert into phone_history (id, created, phone_id, type_id) values (null, ?, ?, ?) [23502-168]
    at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:128)
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:49)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:125)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:110)
    at org.hibernate.engine.jdbc.internal.proxy.AbstractStatementProxyHandler.continueInvocation(AbstractStatementProxyHandler.java:129)
    at org.hibernate.engine.jdbc.internal.proxy.AbstractProxyHandler.invoke(AbstractProxyHandler.java:81)
    at $Proxy37.executeUpdate(Unknown Source)
    at org.hibernate.id.IdentityGenerator$GetGeneratedKeysDelegate.executeAndExtract(IdentityGenerator.java:96)
    at org.hibernate.id.insert.AbstractReturningDelegate.performInsert(AbstractReturningDelegate.java:58)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2870)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3381)
    at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:81)
    at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:362)
    at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:203)
    at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:183)
    at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:167)
    at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:320)
    at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:287)
    at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:193)
    at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:126)
    at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:78)
    at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:208)
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:151)
    at org.hibernate.internal.SessionImpl.firePersistOnFlush(SessionImpl.java:870)
    at org.hibernate.internal.SessionImpl.persistOnFlush(SessionImpl.java:863)
    at org.hibernate.engine.spi.CascadingAction$8.cascade(CascadingAction.java:346)
    at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:380)
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:323)
    at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:208)
    at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:409)
    at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:350)
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:326)
    at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:208)
    at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:165)
    at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:160)
    at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:151)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:88)
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1214)
    at org.hibernate.ejb.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:986)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:240)
    at $Proxy32.flush(Unknown Source)
    at com.blah.model.repository.AbstractRepository.flush(AbstractRepository.java:41)
    at com.blah.model.repository.AbstractRepository$$FastClassByCGLIB$$1ba8d45c.invoke(<generated>)
    at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:149)
    at org.springframework.aop.framework.Cglib2AopProxy$CglibMethodInvocation.invokeJoinpoint(Cglib2AopProxy.java:689)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:155)
    ... 36 more
Caused by: org.h2.jdbc.JdbcSQLException: NULL not allowed for column "PHONE_ID"; SQL statement:
insert into phone_history (id, created, phone_id, type_id) values (null, ?, ?, ?) [23502-168]
    at org.h2.message.DbException.getJdbcSQLException(DbException.java:329)
    at org.h2.message.DbException.get(DbException.java:169)
    at org.h2.message.DbException.get(DbException.java:146)
    at org.h2.table.Column.validateConvertUpdateSequence(Column.java:293)
    at org.h2.table.Table.validateConvertUpdateSequence(Table.java:689)
    at org.h2.command.dml.Insert.insertRows(Insert.java:120)
    at org.h2.command.dml.Insert.update(Insert.java:84)
    at org.h2.command.CommandContainer.update(CommandContainer.java:75)
    at org.h2.command.Command.executeUpdate(Command.java:230)
    at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:156)
    at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:142)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.hibernate.engine.jdbc.internal.proxy.AbstractStatementProxyHandler.continueInvocation(AbstractStatementProxyHandler.java:122)
    ... 83 more

Upvotes: 0

Views: 7904

Answers (3)

Glen Best
Glen Best

Reputation: 23115

Should be easy to fix.

  1. LAZY loading not an issue here.
    All your test calls are running inside the same Spring test transaction. So, all your LAZY loaded relationships will be automatically loaded by the JPA provider when you traverse the relationships via .getXXX() calls to obtain the necessary data.
  2. Bug in test code. In child object newPhoneHistory, you haven't set phone object, which provides the mandatory column phoneId. Before or after the call to:

    existingPhoneHistory.getPhone().add(newPhoneHistory);
    

    Simply add:

    newPhoneHistory.setPhone(existingPhoneHistory.getPhone());
    

You need to manually setup relationship data on both sides of bidirectional relationships. From the JPA 2 spec:

Bidirectional relationships between managed entities will be persisted based on references held by the owning side of the relationship. It is the developer’s responsibility to keep the in-memory references held on the owning side and those held on the inverse side consistent with each other when they change. In the case of unidirectional one-to-one and one-to-many relationships, it is the developer’s responsibility to insure (sic) that the semantics of the relationships are adhered to. [29]

It is particularly important to ensure that changes to the inverse side of a relationship result in appropriate updates on the owning side, so as to ensure the changes are not lost when they are synchronized to the database.

:-)

Upvotes: 0

Yogendra Singh
Yogendra Singh

Reputation: 34397

Since you have made the joins lazy, I suspect, you don't have parent object phone populated in your child object phoneHistory. Please verify (debug/print phone object values through phoneHistory). If so, plase set the phone object first in phoneHistory before saving it.

Add a method addPhoneHistory in Phone object as below:

      public void addPhoneHistory(PhoneHistory phoneHistory){
         phoneHistory.setPhone(this); //This is importtant as its sets the parent
         if(this.phoneHistories == null){
            this.phoneHistories = new ArrayList<PhoneHistory>();
         } 
         this.phoneHistories.add(phoneHistory); 
      }

Add line in your code where you are adding PhoneHistory object in Phone object to use above method.

    existingPhoneHistory.getPhone().addPhoneHistory(newPhoneHistory);

Also its good idea to save 'Phonein place ofexistingPhoneHistory` as below:

   Phone phone = existingPhoneHistory.getPhone();
   phone.addPhoneHistory(newPhoneHistory);
   entityManager.save(phone);//use your PhoneRepo, if there to persist

Hope this helps.

Upvotes: 1

nicephotog
nicephotog

Reputation: 21

check in the SQL query(or some other data acquisition system you use) that populates the phone_id variable has been used to acquire the column fields. Second, check those fields were even filled in by whatever system is supposed to create the phone_id data (e.g. other servlets manage and send the info into the sql table).

Upvotes: 0

Related Questions