Persist parent and child - With data integrity

I have noticed it’s not easy to persist a parent with a child at the same time if you want to maintain data integrity at the DB level. In this case, I would like the child’s parent ID to be not null with a foreign key.

This appears to not be possible because Hibernate persists the parent successfully, then the child without updating the child’s parent ID property. This leaves the insert statement for the child with an invalid value for parent ID. I am confused as to why the parent ID isn’t updated automatically; perhaps there is a config option I haven’t found…

Hibernate’s docs seems to suggest this behaviour is intended/known:
https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#associations-one-to-one-bidirectional

Extract below:

Summary
@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	@OneToOne(
		mappedBy = "phone",
		cascade = CascadeType.ALL,
		orphanRemoval = true,
		fetch = FetchType.LAZY
	)
	private PhoneDetails details;

	//Getters and setters are omitted for brevity

	public void addDetails(PhoneDetails details) {
		details.setPhone(this);
		this.details = details;
	}

	public void removeDetails() {
		if (details != null) {
			details.setPhone(null);
			this.details = null;
		}
	}
}

@Entity(name = "PhoneDetails")
public static class PhoneDetails {

	@Id
	@GeneratedValue
	private Long id;

	private String provider;

	private String technology;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "phone_id")
	private Phone phone;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE PhoneDetails (
    id BIGINT NOT NULL ,
    provider VARCHAR(255) ,
    technology VARCHAR(255) ,
    phone_id BIGINT ,
    PRIMARY KEY ( id )
)

ALTER TABLE PhoneDetails
ADD CONSTRAINT FKeotuev8ja8v0sdh29dynqj05p
FOREIGN KEY (phone_id) REFERENCES Phone

Why does PhoneDetails need its own ID column? This leaves the possibility to end up with a PhoneDetails row without a valid Phone to link to. Why not just have:

phone_id BIGINT NOT NULL

This would link PhoneDetails more tightly to Phone and ensure data is always valid.

Note in my case I am persisting my parent with children in one go, the object comes into my API from JSON so it is impractical to persist the children one by one for every entity/endpoint (and this is surely what Hibernate should be doing for the user!)

Is there some logic to this I’m missing or is it just impossible due to a constraint I’m not aware of? (If so, it would be great to add it to the docs!)

You are mapping two distinct entities, Phone and PhoneDetails, each of which has an @Id private Long id property - that’s why you are seeing distinct id columns. You are then defining a bidirectional @OneToOne association, where PhoneDetails is the owning side and Phone is the indirect side (mappedBy).

If you wish to use the associated Phone’s id as an identifier for PhoneDetails, you should simply drop the Long id column and just annotate the to-one association with @Id. Please also take a look at the @MapsId annotation and at Hibernate’s inheritance capabilities, which might be best suited depending on what you’re trying to achieve.

1 Like

Thank you for the fast reply!

I don’t think inheritance is right for what I’m after. And I think @MapsId requires me to be in the Hibernate context to manage the IDs for me.

I think an example of what I’m trying to do might make more sense. I have the following tables:

DDL
create table companies
(
    id      int auto_increment primary key,
    name    varchar(255)      null
);

create table company_details
(
    company_id       int           not null,
    website          varchar(500)  null,
    constraint company_details_companies_id_fk
        foreign key (company_id) references companies (id)
            on delete cascade
);

Mapped to:

Entities
@Entity
@Table(name = "companies")
public class Company {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id")
	public int id;

	@Column(name = "name")
	public String name;

	@OneToOne(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
	public CompanyDetails details;

}

@Entity
@Table(name = "company_details")
public class CompanyDetails {

	@Id
	@Column(name = "company_id")
	public int companyId;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "company_id")
	private Company company;

	@Column(name = "website", length = 500)
	public String website;

}

I am then:

JSON to Java

Take a new Company from JSON:

{"name":"f", "details": {"website": "test"}}

Run it through Jackson and persist it:
Screenshot from 2024-02-13 15-51-25

And I get the following:

Log
2024-02-13 15:53:13.393 | DEBUG | org.hibernate.SQL                        | dev        : insert into companies (name) values (?)
2024-02-13 15:53:13.398 | TRACE | org.hibernate.orm.jdbc.bind              | dev        : binding parameter (1:VARCHAR) <- [f]
2024-02-13 15:53:14.434 | DEBUG | org.hibernate.SQL                        | dev        : insert into company_details (website,company_id) values (?,?)
2024-02-13 15:53:14.440 | TRACE | org.hibernate.orm.jdbc.bind              | dev        : binding parameter (1:VARCHAR) <- [test]
2024-02-13 15:53:14.442 | TRACE | org.hibernate.orm.jdbc.bind              | dev        : binding parameter (2:INTEGER) <- [0]
2024-02-13 15:53:14.449 |  WARN | o.h.engine.jdbc.spi.SqlExceptionHelper   | dev        : SQL Error: 1452, SQLState: 23000
2024-02-13 15:53:14.450 | ERROR | o.h.engine.jdbc.spi.SqlExceptionHelper   | dev        : Cannot add or update a child row: a foreign key constraint fails (`testDb`.`company_details`, CONSTRAINT `company_details_companies_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE)
2024-02-13 15:53:29.866 | ERROR | testDb.Functions.CompanyFunctions        | dev        : Error while committing the transaction
jakarta.persistence.RollbackException: Error while committing the transaction
	at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:67)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104)
	...
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement [Cannot add or update a child row: a foreign key constraint fails (`testDb`.`company_details`, CONSTRAINT `company_details_companies_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE)] [insert into company_details (website,company_id) values (?,?)]
	at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:62)
	at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:58)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:108)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:107)
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:40)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:52)
	at org.hibernate.persister.entity.mutation.InsertCoordinator.doStaticInserts(InsertCoordinator.java:175)
	at org.hibernate.persister.entity.mutation.InsertCoordinator.coordinateInsert(InsertCoordinator.java:113)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2873)
	at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:104)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:632)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:499)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:363)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:41)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1423)
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:504)
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2339)
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1996)
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
	... 101 common frames omitted
Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (`testDb`.`company_details`, CONSTRAINT `company_details_companies_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1061)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1009)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1320)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:994)
	at org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement.executeUpdate(DelegatingPreparedStatement.java:137)
	at org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement.executeUpdate(DelegatingPreparedStatement.java:137)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:194)
	... 121 common frames omitted

Have I just misinterpreted what I should be doing? I’m aware this might not be the 100% the intended use of Hibernate - most examples show the Java objects being created and manipulated in Java within an entity manager context. But I feel like I’m just missing one tiny annotation I haven’t seen yet!

Thank you :slight_smile:

Thank you for the fast reply. My reply got hidden by the spam filter! Hopefully will be unlocked soon.

I’ve had my reply unhidden now :slight_smile:

This is what’s happening:

  • Company gets persisted, it has a @GeneratedId identifier so when inserting the row in the database a new value is assigned to its id column
  • then, CompanyDetails gets cascade-persisted since it’s mapped as @OneToOne(mappedBy = "company", cascade = CascadeType.ALL, [...]), and it as 0 as companyId
  • when the insert is executed on the datase, the id column of the companies table has another value, so you get a constraint violation error.

If you want the Company foreign key as the identifier for CompanyDetails you should just use @MapsId or remove the companyId property and simply annotate the association with @Id. Otherwise, you will have to manually assign the Company identifier after persisting the entity in the CompanyDetails#companyId property, so you won’t be able to use cascade-persist.

Thank you for the info!

Got it working using @MapsId and the @JsonManagedReference and @JsonBackReference annotations from Jackson.

Cheers.