diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 86ac609723f..afe8b1783f5 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -22,6 +22,12 @@ ${project.version} test + + ca.uhn.hapi.fhir + hapi-fhir-jpa + ${project.version} + test + ca.uhn.hapi.fhir hapi-fhir-caching-testing @@ -49,6 +55,12 @@ jakarta.servlet-api test + + org.hibernate + hibernate-testing + 6.5.2.Final + test + diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/BasicEntityTestFixture.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/BasicEntityTestFixture.java new file mode 100644 index 00000000000..5c9e7c51a84 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/BasicEntityTestFixture.java @@ -0,0 +1,48 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import jakarta.annotation.Nonnull; + +public class BasicEntityTestFixture,J extends IJoinEntity> { + + /** does this scenario support null partition_id? */ + boolean myNullPartitionSupportFlag = true; + + BasicEntityTestFixture(Class theRootType, Class theJoinType) { + myRootType = theRootType; + myJoinType = theJoinType; + } + + public final Class myRootType; + public final Class myJoinType; + + public R buildRootEntity() { + return buildInstance(myRootType); + } + + public J buildJoinEntity() { + return buildInstance(myJoinType); + } + + public boolean isSupportNullPartitionId() { + return myNullPartitionSupportFlag; + } + + public static ,J extends IJoinEntity> BasicEntityTestFixture build(Class theRootType, Class theJoinType) { + return new BasicEntityTestFixture<>(theRootType, theJoinType); + } + + public static ,J extends IJoinEntity> BasicEntityTestFixture buildNoNullPartition(Class theRootType, Class theJoinType) { + BasicEntityTestFixture entityFixture = new BasicEntityTestFixture<>(theRootType, theJoinType); + entityFixture.myNullPartitionSupportFlag = false; + return entityFixture; + } + + + static @Nonnull T buildInstance(Class theClass) { + try { + return theClass.getDeclaredConstructor().newInstance(); + } catch (Exception theE) { + throw new RuntimeException(theE); + } + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/BasicEntityTestTemplate.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/BasicEntityTestTemplate.java new file mode 100644 index 00000000000..41e1d95ab7a --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/BasicEntityTestTemplate.java @@ -0,0 +1,238 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import jakarta.annotation.Nonnull; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +abstract public class BasicEntityTestTemplate,J extends IJoinEntity> { + private static final Logger ourLog = LoggerFactory.getLogger(BasicEntityTestTemplate.class); + + @Autowired + EntityManagerFactory myEntityManagerFactory; + + @Autowired + TransactionTemplate myTransactionTemplate; + + @Autowired + JdbcTemplate myJdbcTemplate; + + @RegisterExtension + SchemaCleanerExtension mySchemaCleanerExtension = new SchemaCleanerExtension(); + + final BasicEntityTestFixture myFixture; + + public BasicEntityTestTemplate(BasicEntityTestFixture theFixture) { + myFixture = theFixture; + } + + static List getPartitions(BasicEntityTestFixture theFixture) { + var result = new ArrayList(); + if (theFixture.isSupportNullPartitionId()) { + result.add(null); + } + result.add(12); + return result; + } + + @Test + void rootEntityBoundToTable() { + // given + myJdbcTemplate.execute("insert into res_root(res_id, partition_id, string_col) values (-1, -1, 'hello!')"); + + doInTx(em->{ + long count = queryCountAll(em, myFixture.myRootType); + + assertEquals(1, count); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery cr = cb.createQuery(myFixture.myRootType); + cr.select(cr.from(myFixture.myRootType)); + R readback = em.createQuery(cr).getSingleResult(); + + assertEquals(-1, readback.getResId()); + assertEquals(-1, readback.getPartitionId()); + assertEquals("hello!", readback.getString()); + }); + } + + @ParameterizedTest + @MethodSource("getPartitions") + void roundTripResourceTable(Integer thePartitionId) { + doInTx(em->{ + R root = myFixture.buildRootEntity(); + root.setPartitionId(thePartitionId); + root.setString("goodbye!"); + em.persist(root); + + em.flush(); + em.clear(); + + Object id = myEntityManagerFactory.getPersistenceUnitUtil().getIdentifier(root); + ourLog.info("flushed root entity. Id is {}", id); + R readback = em.find(myFixture.myRootType, id); + assertNotNull(readback); + assertEquals(root.getResId(), readback.getResId()); + assertNotNull(readback.getResId()); + assertEquals(thePartitionId, readback.getPartitionId()); + assertEquals("goodbye!", readback.getString()); + }); + } + + + @ParameterizedTest + @MethodSource("getPartitions") + void updateResourceTable(Integer thePartitionId) { + doInTx(em->{ + R root = myFixture.buildRootEntity(); + root.setPartitionId(thePartitionId); + root.setString("hello!"); + em.persist(root); + + em.flush(); + em.clear(); + + Object id = myEntityManagerFactory.getPersistenceUnitUtil().getIdentifier(root); + ourLog.info("flushed root entity. Id is {}", id); + R readback = em.find(myFixture.myRootType, id); + + readback.setString("goodbye!"); + em.flush(); + em.clear(); + + readback = em.find(myFixture.myRootType, id); + assertNotNull(readback); + assertEquals("goodbye!", readback.getString()); + }); + } + + @ParameterizedTest + @MethodSource("getPartitions") + void roundTripJoin(Integer thePartitionId) { + doInTx(em->{ + var root = myFixture.buildRootEntity(); + root.setPartitionId(thePartitionId); + root.setString("parent"); + + var join = myFixture.buildJoinEntity(); + join.setParent(root); + join.setString("child"); + join.setPartitionId(thePartitionId); + em.persist(root); + em.persist(join); + + em.flush(); + em.clear(); + + Object id = myEntityManagerFactory.getPersistenceUnitUtil().getIdentifier(root); + ourLog.info("flushed root entity. Id is {}", id); + R readback = em.find(myFixture.myRootType, id); + + assertNotNull(readback); + assertEquals(root.getResId(), readback.getResId()); + assertNotNull(readback.getResId()); + assertEquals(thePartitionId, readback.getPartitionId()); + assertEquals("parent", readback.getString()); + + Collection joins = readback.getJoins(); + assertNotNull(joins); + assertEquals(1, joins.size()); + J joinReadback = joins.iterator().next(); + assertNotNull(joinReadback); + assertNotNull(joinReadback.getResId()); + assertEquals(root.getResId(), joinReadback.getResId()); + assertEquals(thePartitionId, joinReadback.getPartitionId()); + assertEquals("child", joinReadback.getString()); + }); + } + + @ParameterizedTest + @MethodSource("getPartitions") + void fetchJoinQuery(Integer thePartitionId) { + doInTx(em -> { + var root0 = myFixture.buildRootEntity(); + root0.setPartitionId(thePartitionId); + root0.setString("parent"); + + var join0 = myFixture.buildJoinEntity(); + join0.setParent(root0); + join0.setString("child"); + join0.setPartitionId(thePartitionId); + + var join1 = myFixture.buildJoinEntity(); + join1.setParent(root0); + join1.setString("child1"); + join1.setPartitionId(thePartitionId); + + em.persist(root0); + em.persist(join0); + em.persist(join1); + + em.flush(); + em.clear(); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(myFixture.myRootType); + Root from = cr.from(myFixture.myRootType); + from.fetch("myJoinEntities"); + cr.select(from); + + List resultList = em.createQuery(cr).getResultList(); + assertEquals(1,resultList.size()); + + resultList.forEach(e-> { + ourLog.info("root: {}", e); + assertNotNull(e); + assertNotNull(e.getJoins()); + assertEquals(2, e.getJoins().size()); + assertNotNull(e.getJoins().iterator().next()); + e.getJoins().forEach(j-> ourLog.info("join: {}", j)); + }); + + }); + } + + + private void doInTx(Consumer theCallback) { + myTransactionTemplate.execute(status-> { + theCallback.accept(getEntityManagerOrThrow()); + return null; + }); + } + + private long queryCountAll(EntityManager em, Class rootType) { + CriteriaBuilder qb = em.getCriteriaBuilder(); + CriteriaQuery cq = qb.createQuery(Long.class); + CriteriaQuery select = cq.select(qb.count(cq.from(rootType))); + long count = em.createQuery(select).getSingleResult(); + return count; + } + + @Nonnull + EntityManager getEntityManagerOrThrow() { + return Objects.requireNonNull(EntityManagerFactoryUtils.getTransactionalEntityManager(myEntityManagerFactory)); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/IJoinEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/IJoinEntity.java new file mode 100644 index 00000000000..4e0c3f45e83 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/IJoinEntity.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +public interface IJoinEntity

{ + Long getPid(); + + void setString(String theString); + + void setParent(P theRoot); + + String getString(); + + void setPartitionId(Integer thePartitionId); + + Integer getPartitionId(); + + Long getResId(); +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/IRootEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/IRootEntity.java new file mode 100644 index 00000000000..4acf4fabb18 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/IRootEntity.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import java.util.Collection; + +public interface IRootEntity { + Long getResId(); + + void setPartitionId(Integer thePartitionId); + + Integer getPartitionId(); + + String getString(); + + void setString(String theString); + + Collection getJoins(); +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/PKSpikeDefaultJPAConfig.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/PKSpikeDefaultJPAConfig.java new file mode 100644 index 00000000000..a5d1ff202de --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/PKSpikeDefaultJPAConfig.java @@ -0,0 +1,109 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect; +import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; +import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManagerFactory; +import org.apache.commons.dbcp2.BasicDataSource; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.time.Duration; +import java.util.Properties; +import java.util.function.Consumer; + +@Configuration +public class PKSpikeDefaultJPAConfig { + + @Inject + FhirContext myFhirContext; + + @Bean + DataSource datasource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriver(new org.h2.Driver()); + dataSource.setUrl("jdbc:h2:mem:testdb_r4" + System.currentTimeMillis()); + dataSource.setMaxWait(Duration.ofMillis(30000)); + dataSource.setUsername(""); + dataSource.setPassword(""); + dataSource.setMaxTotal(10); + + SchemaInit.initSchema(dataSource); + + return dataSource; + } + + + @Bean + public HapiFhirLocalContainerEntityManagerFactoryBean entityManagerFactory( +// ModuleMigrationMetadata theModuleMigrationMetadata, + ConfigurableListableBeanFactory theConfigurableListableBeanFactory, + DataSource theDataSource, + PersistenceManagedTypes theManagedTypes, + @Autowired(required = false) @Nullable Consumer theEntityManagerFactoryCustomizer) { + + HapiFhirLocalContainerEntityManagerFactoryBean entityManagerFactoryBean = + new HapiFhirLocalContainerEntityManagerFactoryBean(theConfigurableListableBeanFactory); + + entityManagerFactoryBean.setJpaDialect(new HapiFhirHibernateJpaDialect(myFhirContext.getLocalizer())); + HibernatePersistenceProvider persistenceProvider = new HibernatePersistenceProvider(); + entityManagerFactoryBean.setPersistenceProvider(persistenceProvider); + Properties jpaProperties = new Properties(); + jpaProperties.put("hibernate.search.enabled", "false"); + jpaProperties.put("hibernate.format_sql", "false"); + jpaProperties.put("hibernate.show_sql", "false"); + jpaProperties.put("hibernate.integration.envers.enabled=false", "false"); + jpaProperties.put("hibernate.hbm2ddl.auto", "none"); + jpaProperties.put("hibernate.dialect", HapiFhirH2Dialect.class.getName()); + entityManagerFactoryBean.setJpaProperties(jpaProperties); + + entityManagerFactoryBean.setPersistenceUnitName("HapiPU"); + entityManagerFactoryBean.setDataSource(theDataSource); + entityManagerFactoryBean.setManagedTypes(theManagedTypes); + + if (theEntityManagerFactoryCustomizer != null) { + theEntityManagerFactoryCustomizer.accept(entityManagerFactoryBean); + } + + return entityManagerFactoryBean; + } + + @Bean + public PlatformTransactionManager transactionManager(EntityManagerFactory theEntityManagerFactory) { + JpaTransactionManager retVal = new JpaTransactionManager(); + retVal.setEntityManagerFactory(theEntityManagerFactory); + return retVal; + } + + + @Bean + public TransactionTemplate transactionTemplate(PlatformTransactionManager theTransactionManager) { + return new TransactionTemplate(theTransactionManager); + } + + @Bean + public JdbcTemplate jdbcTemplate(DataSource theDataSource) { + return new JdbcTemplate(theDataSource); + } + + @Bean + @Nonnull + JpaStorageSettings storageSettings() { + JpaStorageSettings jpaStorageSettings = new JpaStorageSettings(); + return jpaStorageSettings; + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/SchemaCleanerExtension.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/SchemaCleanerExtension.java new file mode 100644 index 00000000000..69005b5ee2f --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/SchemaCleanerExtension.java @@ -0,0 +1,22 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import javax.sql.DataSource; + +public class SchemaCleanerExtension implements AfterEachCallback { + + @Override + public void afterEach(ExtensionContext theExtensionContext) throws Exception { + ApplicationContext springContext = SpringExtension.getApplicationContext(theExtensionContext); + var dataSource = springContext.getBean(DataSource.class); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("delete from res_join"); + jdbcTemplate.execute("delete from res_root"); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/SchemaInit.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/SchemaInit.java new file mode 100644 index 00000000000..feceaf51e99 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/SchemaInit.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.springframework.jdbc.core.JdbcTemplate; + +class SchemaInit { + public static void initSchema(BasicDataSource theDataSource) { + var t = new JdbcTemplate(theDataSource); + + t.execute("CREATE SEQUENCE RES_ROOT_SEQ increment by 50"); + t.execute("CREATE SEQUENCE RES_JOIN_SEQ increment by 50"); + t.execute("create table res_root (RES_ID IDENTITY PRIMARY KEY, PARTITION_ID numeric, STRING_COL varchar)"); + t.execute("create table res_join (PID IDENTITY PRIMARY KEY, RES_ID numeric, PARTITION_ID numeric, STRING_COL varchar)"); + + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/ValueTypeBasedParameterResolver.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/ValueTypeBasedParameterResolver.java new file mode 100644 index 00000000000..9ab2bada6c0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/ValueTypeBasedParameterResolver.java @@ -0,0 +1,33 @@ +package ca.uhn.fhir.jpa.model.pkspike; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +public class ValueTypeBasedParameterResolver implements ParameterResolver { + + private final T myValue; + + public static ValueTypeBasedParameterResolver build(T theValue) { + return new ValueTypeBasedParameterResolver<>(theValue); + } + + ValueTypeBasedParameterResolver(T theValue) { + myValue = theValue; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.getParameter().getType().isAssignableFrom(myValue.getClass()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return myValue; + } + + public T get() { + return myValue; + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/EmbeddedIdPkJpaBindingTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/EmbeddedIdPkJpaBindingTest.java new file mode 100644 index 00000000000..408fe64b947 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/EmbeddedIdPkJpaBindingTest.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.jpa.model.pkspike.embeddedid; + +import ca.uhn.fhir.jpa.config.r4.FhirContextR4Config; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestTemplate; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestFixture; +import ca.uhn.fhir.jpa.model.pkspike.PKSpikeDefaultJPAConfig; +import ca.uhn.fhir.jpa.model.pkspike.ValueTypeBasedParameterResolver; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Spike to assess variable binding against a db. + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + EmbeddedIdTypesConfig.class, PKSpikeDefaultJPAConfig.class, FhirContextR4Config.class +}) +public class EmbeddedIdPkJpaBindingTest { + + static final BasicEntityTestFixture ourFixture = BasicEntityTestFixture.buildNoNullPartition(ResRootEmbeddedIdEntity.class, ResJoinEmbeddedIdEntity.class); + + @RegisterExtension + static final ParameterResolver ourResolver = ValueTypeBasedParameterResolver.build(ourFixture); + + + @Nested + class Common extends BasicEntityTestTemplate { + Common() { + super(ourFixture); + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/EmbeddedIdTypesConfig.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/EmbeddedIdTypesConfig.java new file mode 100644 index 00000000000..7a290ca3107 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/EmbeddedIdTypesConfig.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.model.pkspike.embeddedid; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; + +@Configuration +public class EmbeddedIdTypesConfig { + @Bean + PersistenceManagedTypes getManagedTypes() { + return PersistenceManagedTypes.of( + ResRootEmbeddedIdEntity.class.getName(), + ResJoinEmbeddedIdEntity.class.getName() + ); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/ResJoinEmbeddedIdEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/ResJoinEmbeddedIdEntity.java new file mode 100644 index 00000000000..b31af0d033b --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/ResJoinEmbeddedIdEntity.java @@ -0,0 +1,82 @@ +package ca.uhn.fhir.jpa.model.pkspike.embeddedid; + +import ca.uhn.fhir.jpa.model.pkspike.IJoinEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; + +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table( + name = "RES_JOIN" +) +public class ResJoinEmbeddedIdEntity implements IJoinEntity { + @Id +// @GenericGenerator(name = "SEQ_RESOURCE_ID", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(name = "PID") + Long myId; + @Column(name = "PARTITION_ID", nullable = true, insertable = false, updatable = false) + Integer myPartitionId; + @Column(name = "RES_ID", nullable = false, updatable = false, insertable = false) + Long myResourceId; + @Column(name = "STRING_COL") + String myString; + + // fixme mb which side controls vs reads? + @ManyToOne( + optional = false) + @JoinColumns({ + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID"), + @JoinColumn(name = "PARTITION_ID", referencedColumnName = "PARTITION_ID") + }) + ResRootEmbeddedIdEntity myResource; + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + @Override + public Long getPid() { + return myId; + } + + @Override + public void setString(String theString) { + myString = theString; + } + + @Override + public void setParent(ResRootEmbeddedIdEntity theRoot) { + myResource = theRoot; + } + + @Override + public String getString() { + return myString; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + @Override + public Long getResId() { + // fixme keep copy + return myResource==null?null:myResource.getResId(); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/ResRootEmbeddedIdEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/ResRootEmbeddedIdEntity.java new file mode 100644 index 00000000000..093e6e22383 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/embeddedid/ResRootEmbeddedIdEntity.java @@ -0,0 +1,116 @@ +package ca.uhn.fhir.jpa.model.pkspike.embeddedid; + +import ca.uhn.fhir.jpa.model.pkspike.IRootEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * fixme MB IdClass vs embeddable? + * + */ +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table(name = "RES_ROOT") +public class ResRootEmbeddedIdEntity implements IRootEntity { + private static final Logger ourLog = LoggerFactory.getLogger(ResRootEmbeddedIdEntity.class); + + @EmbeddedId + ResRootPK myId = new ResRootPK(); + + @Column(name = "PARTITION_ID", nullable = true, insertable = false, updatable = false) + Integer myPartitionId; + + @Column(name = "STRING_COL") + String myString; + + @OneToMany(mappedBy = "myResource") + Collection myJoinEntities = new ArrayList<>(); + + public ResRootEmbeddedIdEntity() { + ourLog.info("new ResRootCompositeEntity()"); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + @Override + public Long getResId() { + return myId==null?null:myId.myId; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + myId.myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + @Override + public String getString() { + return myString; + } + + @Override + public void setString(String theString) { + myString = theString; + } + + @Override + public Collection getJoins() { + return myJoinEntities; + } + + @Embeddable + static class ResRootPK { + @GeneratedValue() + @Column(name = "RES_ID") + public Long myId; + + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + public Integer myPartitionId; + + /** For Hibernate */ + protected ResRootPK() {} + + public ResRootPK(Long theId, Integer thePartitionId) { + myId = theId; + myPartitionId = thePartitionId; + } + + @Override + public boolean equals(Object theO) { + return EqualsBuilder.reflectionEquals(this,theO); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassKeyTypesConfig.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassKeyTypesConfig.java new file mode 100644 index 00000000000..2ea0f03e60e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassKeyTypesConfig.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.model.pkspike.idclass; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; + +@Configuration +public class IdClassKeyTypesConfig { + @Bean + PersistenceManagedTypes getManagedTypes() { + return PersistenceManagedTypes.of( + ResRootIdClassEntity.class.getName(), + ResJoinIdClassEntity.class.getName() + ); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassPkCustomXmlJpaBindingTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassPkCustomXmlJpaBindingTest.java new file mode 100644 index 00000000000..597a289fe68 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassPkCustomXmlJpaBindingTest.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.jpa.model.pkspike.idclass; + +import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; +import ca.uhn.fhir.jpa.config.r4.FhirContextR4Config; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestTemplate; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestFixture; +import ca.uhn.fhir.jpa.model.pkspike.PKSpikeDefaultJPAConfig; +import ca.uhn.fhir.jpa.model.pkspike.ValueTypeBasedParameterResolver; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.function.Consumer; + +/** + * Override the JPA annotations with an orm.xml file to add PARTITION_ID to the root PK, and the join expressions. + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + IdClassPkCustomXmlJpaBindingTest.Config.class, IdClassKeyTypesConfig.class, PKSpikeDefaultJPAConfig.class, FhirContextR4Config.class +}) +public class IdClassPkCustomXmlJpaBindingTest { + private static final Logger ourLog = LoggerFactory.getLogger(IdClassPkCustomXmlJpaBindingTest.class); + + @Configuration + static class Config { + @Bean + Consumer entityManagerFactoryCustomizer() { + return em->{ + ourLog.info("Injecting custom persistence.xml"); + em.setMappingResources("/ca/uhn/fhir/jpa/model/pkspike/idclass/ormComposite.xml"); + }; + } + } + + static final BasicEntityTestFixture ourFixture = BasicEntityTestFixture.buildNoNullPartition(ResRootIdClassEntity.class, ResJoinIdClassEntity.class); + @RegisterExtension + static final ParameterResolver ourFixtureResolver = ValueTypeBasedParameterResolver.build(ourFixture); + + @Nested + class Common extends BasicEntityTestTemplate { + Common() { + super(ourFixture); + } + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassPkJpaBindingTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassPkJpaBindingTest.java new file mode 100644 index 00000000000..41786f1b91a --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/IdClassPkJpaBindingTest.java @@ -0,0 +1,54 @@ +package ca.uhn.fhir.jpa.model.pkspike.idclass; + +import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean; +import ca.uhn.fhir.jpa.config.r4.FhirContextR4Config; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestTemplate; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestFixture; +import ca.uhn.fhir.jpa.model.pkspike.PKSpikeDefaultJPAConfig; +import ca.uhn.fhir.jpa.model.pkspike.ValueTypeBasedParameterResolver; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.function.Consumer; + +/** + * Use an IdClass even though the PK is only a single column. + * This allows us to extend the PK next door in the {@link IdClassPkCustomXmlJpaBindingTest}. + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + IdClassPkJpaBindingTest.Config.class, IdClassKeyTypesConfig.class, PKSpikeDefaultJPAConfig.class, FhirContextR4Config.class +}) +public class IdClassPkJpaBindingTest { + private static final Logger ourLog = LoggerFactory.getLogger(IdClassPkJpaBindingTest.class); + + @Configuration + static class Config { + @Bean + Consumer entityManagerFactoryCustomizer() { + return em->{ + ourLog.info("Injecting custom persistence.xml"); + em.setMappingResources("/ca/uhn/fhir/jpa/model/pkspike/idclass/ormLong.xml"); + }; + } + } + + public static final BasicEntityTestFixture ourFixture = BasicEntityTestFixture.build(ResRootIdClassEntity.class, ResJoinIdClassEntity.class); + @RegisterExtension + static final ParameterResolver ourFixtureResolver = ValueTypeBasedParameterResolver.build(ourFixture); + + @Nested + class Common extends BasicEntityTestTemplate { + Common() { + super(ourFixture); + } + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/ResJoinIdClassEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/ResJoinIdClassEntity.java new file mode 100644 index 00000000000..8d32df41d0e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/ResJoinIdClassEntity.java @@ -0,0 +1,137 @@ +package ca.uhn.fhir.jpa.model.pkspike.idclass; + +import ca.uhn.fhir.jpa.model.pkspike.IJoinEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.hibernate.annotations.PartitionKey; + +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table( + name = "RES_JOIN" +) +@IdClass(ResJoinIdClassEntity.ResJoinPK.class) +public class ResJoinIdClassEntity implements IJoinEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(name = "PID") + Long myId; + + @PartitionKey + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + Integer myPartitionId; + + @Column(name = "STRING_COL") + String myString; + + // fixme mb which side controls vs reads? + @ManyToOne( + optional = false) + @JoinColumns({ + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID"), +// @JoinColumn(name = "PARTITION_ID", referencedColumnName = "PARTITION_ID") + }) + ResRootIdClassEntity myResource; + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + @Override + public Long getPid() { + return myId; + } + + @Override + public void setString(String theString) { + myString = theString; + } + + @Override + public void setParent(ResRootIdClassEntity theRoot) { + myResource = theRoot; + } + + @Override + public String getString() { + return myString; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + @Override + public Long getResId() { + // fixme keep copy + return myResource == null? null: myResource.myId; + } + + + static class ResJoinPK { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(name = "PID") + Long myId; + + /** for Hibernate */ + public ResJoinPK() {} + + + public ResJoinPK(Long theId) { + myId = theId; + } + + @Override + public boolean equals(Object theO) { + return EqualsBuilder.reflectionEquals(this,theO); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + } + + static class ResJoinCompositePK extends ResJoinPK { + @Id + @Column(name = "PARTITION_ID", nullable = false, insertable = false, updatable = false) + Integer myPartitionId; + + /** for Hibernate */ + public ResJoinCompositePK() {} + + public ResJoinCompositePK(Long theId, Integer thePartitionId) { + super(theId); + myPartitionId = thePartitionId; + } + + + + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/ResRootIdClassEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/ResRootIdClassEntity.java new file mode 100644 index 00000000000..23d624cad83 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/idclass/ResRootIdClassEntity.java @@ -0,0 +1,141 @@ +package ca.uhn.fhir.jpa.model.pkspike.idclass; + +import ca.uhn.fhir.jpa.model.pkspike.IRootEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.hibernate.annotations.PartitionKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * fixme MB IdClass vs embeddable? + * + */ +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table(name = "RES_ROOT") +@IdClass(ResRootIdClassEntity.ResRootPK.class) +public class ResRootIdClassEntity implements IRootEntity { + private static final Logger ourLog = LoggerFactory.getLogger(ResRootIdClassEntity.class); + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "RES_ID") + Long myId; + +// @Id + //@PartitionKey + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + Integer myPartitionId; + +// ResRootPK getPK() { +// return new ResRootPK(myId, myPartitionId); +// } + + @Column(name = "STRING_COL") + String myString; + + @OneToMany(mappedBy = "myResource") + Collection myJoinEntities = new ArrayList<>(); + + public ResRootIdClassEntity() { + ourLog.info("new ResRootCompositeEntity()"); + } + + public Long getId() { + return myId; + } + + @Override + public Long getResId() { + return myId; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + public String getString() { + return myString; + } + + public void setString(String theString) { + myString = theString; + } + + @Override + public Collection getJoins() { + return myJoinEntities; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + static class ResRootPK { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "RES_ID") + Long myId; + + /** for Hibernate */ + public ResRootPK() {} + + + public ResRootPK(Long theId) { + myId = theId; + } + + @Override + public boolean equals(Object theO) { + return EqualsBuilder.reflectionEquals(this,theO); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + } + + static class ResRootCompositePK extends ResRootPK { + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + Integer myPartitionId; + + /** for Hibernate */ + public ResRootCompositePK() {} + + public ResRootCompositePK(Long theId, Integer thePartitionId) { + super(theId); + myPartitionId = thePartitionId; + } + + + + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/package-info.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/package-info.java new file mode 100644 index 00000000000..7a8cd08e981 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/package-info.java @@ -0,0 +1,29 @@ +/** + * Various tests of a parent-child JPA relationship exercising configurable pk definition and joins. + *

+ * We have a test template that does some basic queries ( {@link ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestTemplate}). + *

    + *
  • See {@link ca.uhn.fhir.jpa.model.pkspike.primitive.SimplePkJpaBindingTest} for the normal case. Supports null partition_id. + *
  • See {@link ca.uhn.fhir.jpa.model.pkspike.embeddedid.EmbeddedIdPkJpaBindingTest} for an embedded Id class path. Does not support null partition_id. + *
  • See {@link ca.uhn.fhir.jpa.model.pkspike.partitionkey.PartitionJpaBindingTest} for the new Hibernate 6 @{@link org.hibernate.annotations.PartitionKey} annotation. Does support null partition_id. + *
  • See {@link ca.uhn.fhir.jpa.model.pkspike.idclass.IdClassPkJpaBindingTest}. Supports null partition_id. + *
  • See {@link ca.uhn.fhir.jpa.model.pkspike.idclass.IdClassPkCustomXmlJpaBindingTest} which adds PARTITION_ID to the pk and join to the mappings defined in IdClassPkJpaBindingTest by adding an orm.xml file. Does not support null partition_id. + *
+ *

+ * Things we learned: + *

    + *
  • Hibernate can not fetch any entity with a composite key if any component is null. + *
  • It's a real pain to merge orm.xml with annotations when we have duplicate columns. E.g. PARTITION_ID used as a key and join fight over who should be "insert=true" vs false. + *

    + * Further hacking Ideas: + *

      + *
    • Try to wrap the partition_id column with a user type (e.g. Optional?) so we can pretend it isn't null. + *

      + *

      + * Add this to logback to explore the sql. + *

      + *   
      + *  	
      + *  
      + */ +package ca.uhn.fhir.jpa.model.pkspike; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/PartitionJpaBindingTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/PartitionJpaBindingTest.java new file mode 100644 index 00000000000..c44595ce4b0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/PartitionJpaBindingTest.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.jpa.model.pkspike.partitionkey; + +import ca.uhn.fhir.jpa.config.r4.FhirContextR4Config; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestTemplate; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestFixture; +import ca.uhn.fhir.jpa.model.pkspike.PKSpikeDefaultJPAConfig; +import ca.uhn.fhir.jpa.model.pkspike.ValueTypeBasedParameterResolver; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Try out the new @{@link org.hibernate.annotations.PartitionKey} annotation. + * This annotation annotates columns to include in entity update/delete statements, so they can be efficient in a partitioned table. + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + PartitionTypesConfig.class, PKSpikeDefaultJPAConfig.class, FhirContextR4Config.class +}) +class PartitionJpaBindingTest { + + static final BasicEntityTestFixture ourConfig = BasicEntityTestFixture.buildNoNullPartition(ResRootPartitionEntity.class, ResJoinPartitionEntity.class); + + @RegisterExtension + static final ParameterResolver ourResolver = ValueTypeBasedParameterResolver.build(ourConfig); + + @Nested + class Common extends BasicEntityTestTemplate { + Common() { + super(ourConfig); + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/PartitionTypesConfig.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/PartitionTypesConfig.java new file mode 100644 index 00000000000..2d64ee5c8a2 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/PartitionTypesConfig.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.model.pkspike.partitionkey; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; + +@Configuration +public class PartitionTypesConfig { + @Bean + PersistenceManagedTypes getManagedTypes() { + return PersistenceManagedTypes.of( + ResRootPartitionEntity.class.getName(), + ResJoinPartitionEntity.class.getName() + ); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/ResJoinPartitionEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/ResJoinPartitionEntity.java new file mode 100644 index 00000000000..01a09dd4e3e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/ResJoinPartitionEntity.java @@ -0,0 +1,86 @@ +package ca.uhn.fhir.jpa.model.pkspike.partitionkey; + +import ca.uhn.fhir.jpa.model.pkspike.IJoinEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.hibernate.annotations.PartitionKey; + +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table( + name = "RES_JOIN" +) +public class ResJoinPartitionEntity implements IJoinEntity { + @Id +// @GenericGenerator(name = "SEQ_RESOURCE_ID", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(name = "PID") + Long myId; + + @PartitionKey + @Column(name = "PARTITION_ID", nullable = true, insertable = false, updatable = false) + Integer myPartitionId; + + @Column(name = "STRING_COL") + String myString; + + @Column(name = "RES_ID", nullable = false, insertable = false, updatable = false) + Long myResId; + + @ManyToOne( + optional = false) + @JoinColumns({ + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", nullable = false, insertable = true, updatable = false), + @JoinColumn(name = "PARTITION_ID", referencedColumnName = "PARTITION_ID", nullable = false, insertable = true, updatable = false) + }) + ResRootPartitionEntity myResource; + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + @Override + public Long getPid() { + return myId; + } + + @Override + public void setString(String theString) { + myString = theString; + } + + @Override + public void setParent(ResRootPartitionEntity theRoot) { + myResource = theRoot; + } + + @Override + public String getString() { + return myString; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + @Override + public Long getResId() { + return myResId; + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/ResRootPartitionEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/ResRootPartitionEntity.java new file mode 100644 index 00000000000..aed3ac1d675 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/partitionkey/ResRootPartitionEntity.java @@ -0,0 +1,74 @@ +package ca.uhn.fhir.jpa.model.pkspike.partitionkey; + +import ca.uhn.fhir.jpa.model.pkspike.IRootEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.hibernate.annotations.PartitionKey; + +import java.util.ArrayList; +import java.util.Collection; + +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table(name = "RES_ROOT") +public class ResRootPartitionEntity implements IRootEntity { + @Id + @GeneratedValue() + @Column(name = "RES_ID") + Long myId; + + @PartitionKey + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + Integer myPartitionId; + + @Column(name = "STRING_COL") + String myString; + + @OneToMany(mappedBy = "myResource", fetch = FetchType.EAGER) + Collection myJoinEntities = new ArrayList<>(); + + public Long getId() { + return myId; + } + + public String getString() { + return myString; + } + + public void setString(String theString) { + myString = theString; + } + + @Override + public Collection getJoins() { + return myJoinEntities; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + @Override + public Long getResId() { + return myId; + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/ResJoinEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/ResJoinEntity.java new file mode 100644 index 00000000000..1b023cc7316 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/ResJoinEntity.java @@ -0,0 +1,83 @@ +package ca.uhn.fhir.jpa.model.pkspike.primitive; + +import ca.uhn.fhir.jpa.model.pkspike.IJoinEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table( + name = "RES_JOIN" +) +public class ResJoinEntity implements IJoinEntity { + @Id +// @GenericGenerator(name = "SEQ_RESOURCE_ID", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(name = "PID") + Long myId; + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + Integer myPartitionId; + + @Column(name = "STRING_COL") + String myString; + + @Column(name = "RES_ID", nullable = false, insertable = false, updatable = false) + Long myResId; + + @ManyToOne( + optional = false) + @JoinColumn( + name = "RES_ID", + referencedColumnName = "RES_ID", + nullable = false, + updatable = false) + ResRootEntity myResource; + + @Override + public Long getResId() { + return myResId; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + @Override + public Long getPid() { + return myId; + } + + @Override + public void setString(String theString) { + myString = theString; + } + + @Override + public void setParent(ResRootEntity theRoot) { + myResource = theRoot; + } + + @Override + public String getString() { + return myString; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/ResRootEntity.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/ResRootEntity.java new file mode 100644 index 00000000000..5883f43f64c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/ResRootEntity.java @@ -0,0 +1,71 @@ +package ca.uhn.fhir.jpa.model.pkspike.primitive; + +import ca.uhn.fhir.jpa.model.pkspike.IRootEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.ArrayList; +import java.util.Collection; + +@SuppressWarnings("JpaDataSourceORMInspection") +@Entity +@Table(name = "RES_ROOT") +public class ResRootEntity implements IRootEntity { + @Id + @GeneratedValue() + @Column(name = "RES_ID") + Long myId; + + @Column(name = "PARTITION_ID", nullable = true, insertable = true, updatable = false) + Integer myPartitionId; + + @Column(name = "STRING_COL") + String myString; + + @OneToMany(mappedBy = "myResource", fetch = FetchType.EAGER) + Collection myJoinEntities = new ArrayList<>(); + + public Long getId() { + return myId; + } + + @Override + public Long getResId() { + return myId; + } + + @Override + public void setPartitionId(Integer thePartitionId) { + myPartitionId = thePartitionId; + } + + @Override + public Integer getPartitionId() { + return myPartitionId; + } + + public String getString() { + return myString; + } + + public void setString(String theString) { + myString = theString; + } + + @Override + public Collection getJoins() { + return myJoinEntities; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/SimplePkJpaBindingTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/SimplePkJpaBindingTest.java new file mode 100644 index 00000000000..3eb47bf7628 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/SimplePkJpaBindingTest.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.jpa.model.pkspike.primitive; + +import ca.uhn.fhir.jpa.config.r4.FhirContextR4Config; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestTemplate; +import ca.uhn.fhir.jpa.model.pkspike.BasicEntityTestFixture; +import ca.uhn.fhir.jpa.model.pkspike.PKSpikeDefaultJPAConfig; +import ca.uhn.fhir.jpa.model.pkspike.ValueTypeBasedParameterResolver; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * The simple scenario - single column keys and joins. + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = { + SimpleTypesConfig.class, PKSpikeDefaultJPAConfig.class, FhirContextR4Config.class +}) +public class SimplePkJpaBindingTest extends BasicEntityTestTemplate { + + public static final BasicEntityTestFixture ourFixture = BasicEntityTestFixture.build(ResRootEntity.class, ResJoinEntity.class); + @RegisterExtension + static final ParameterResolver ourFixtureResolver = ValueTypeBasedParameterResolver.build(ourFixture); + + public SimplePkJpaBindingTest() { + super(ourFixture); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/SimpleTypesConfig.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/SimpleTypesConfig.java new file mode 100644 index 00000000000..3fc13c708b2 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/model/pkspike/primitive/SimpleTypesConfig.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.jpa.model.pkspike.primitive; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; + +@Configuration +public class SimpleTypesConfig { + @Bean + PersistenceManagedTypes getManagedTypes() { + return PersistenceManagedTypes.of( + ResRootEntity.class.getName(), + ResJoinEntity.class.getName() + ); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/ormComposite.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/ormComposite.xml new file mode 100644 index 00000000000..9e34c54f5e0 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/ormComposite.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/ormLong.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/ormLong.xml new file mode 100644 index 00000000000..cb11bbb269e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/ormLong.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/persistence.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/persistence.xml new file mode 100644 index 00000000000..b3b19041c32 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/ca/uhn/fhir/jpa/model/pkspike/idclass/persistence.xml @@ -0,0 +1,24 @@ + + + ./overrideMappings.xml + + + + + + diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/LoggingExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/LoggingExtension.java index de0630eeb66..7667b0c9808 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/LoggingExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/LoggingExtension.java @@ -35,14 +35,15 @@ package ca.uhn.fhir.test.utilities; +import jakarta.annotation.Nonnull; import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.MessageFormat; - /** * This JUnit rule generates log messages to delineate the start and finish of a JUnit test case and also to note any exceptions * that are thrown. @@ -50,17 +51,34 @@ import java.text.MessageFormat; * @author Brian Matthews * @version 1.0.0 */ -public class LoggingExtension implements BeforeEachCallback, AfterEachCallback { - - @Override - public void afterEach(ExtensionContext context) { - final Logger logger = LoggerFactory.getLogger(context.getTestClass().get()); - logger.info(MessageFormat.format("Finished test case [{0}]", context.getTestMethod().get().getName())); - } +public class LoggingExtension implements BeforeEachCallback, BeforeTestExecutionCallback, AfterEachCallback, AfterTestExecutionCallback { @Override public void beforeEach(ExtensionContext context) { - final Logger logger = LoggerFactory.getLogger(context.getTestClass().get()); - logger.info(MessageFormat.format("Starting test case [{0}]", context.getTestMethod().get().getName())); + getLoggerForTestClass(context).info("Starting setup for test case [{}]", getMethodName(context)); } + + @Override + public void beforeTestExecution(ExtensionContext context) { + getLoggerForTestClass(context).info("Starting test case [{}]", getMethodName(context)); + + } + @Override + public void afterTestExecution(ExtensionContext context) { + getLoggerForTestClass(context).info("Finished test case [{}]", getMethodName(context)); + } + + @Override + public void afterEach(ExtensionContext context) { + getLoggerForTestClass(context).info("Finished teardown for test case [{}]", getMethodName(context)); + } + + private static Logger getLoggerForTestClass(ExtensionContext context) { + return LoggerFactory.getLogger(context.getTestClass().orElseThrow()); + } + + private static @Nonnull String getMethodName(ExtensionContext context) { + return context.getTestMethod().orElseThrow().getName(); + } + }