Work on multitenancy

This commit is contained in:
jamesagnew 2020-04-10 11:11:29 -04:00
parent e0fcbe1df2
commit 219332e9e3
13 changed files with 599 additions and 86 deletions

View File

@ -143,3 +143,4 @@ ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService.blacklistedResourceT
ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidTargetTypeForChain=Resource type "{0}" is not a valid target type for reference search parameter: {1}
ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderReference.invalidResourceType=Invalid/unsupported resource type: "{0}"
ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId=Non-unique ID specified, can not process request

View File

@ -14,5 +14,8 @@ None of the limitations listed here are considered permanent. Over time the HAPI
* ValueSet
* CodeSystem
* ConceptMap
* **Search Parameters are not partitioned**: There is only one set of SearchParameter resources for the entire system, and any search parameters will apply to resources in all partitions. All SearchParameter resources must be stored in the default partition.
* **Bulk Operations are not partition aware**: Bulk export operations will export data across all partitions.

View File

@ -11,6 +11,7 @@ import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
import ca.uhn.fhir.jpa.bulk.BulkDataExportSvcImpl;
import ca.uhn.fhir.jpa.bulk.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
@ -33,8 +34,6 @@ import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import org.hibernate.jpa.HibernatePersistenceProvider;
@ -229,8 +228,7 @@ public abstract class BaseConfig {
@Bean
@Lazy
public TerminologyUploaderProvider terminologyUploaderProvider() {
TerminologyUploaderProvider retVal = new TerminologyUploaderProvider();
return retVal;
return new TerminologyUploaderProvider();
}
@Bean

View File

@ -30,6 +30,9 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService;
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
@ -46,8 +49,6 @@ import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils;
import ca.uhn.fhir.jpa.util.xmlpatch.XmlPatchUtils;
@ -1006,7 +1007,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Verify that the resource is for the correct partition
if (partitionId != null) {
if (entity.getPartitionId() != null) {
if (partitionId.getPartitionId() == null) {
if (entity.getPartitionId() != null) {
ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", partitionId, entity.getPartitionId());
entity = null;
}
} else if (entity.getPartitionId() != null) {
if (!entity.getPartitionId().getPartitionId().equals(partitionId.getPartitionId())) {
ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", partitionId, entity.getPartitionId());
entity = null;

View File

@ -39,6 +39,9 @@ public interface IForcedIdDao extends JpaRepository<ForcedId, Long> {
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId IS NULL AND myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByPartitionIdNullAndTypeAndForcedId(@Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);
@Query("SELECT f.myResourcePid FROM ForcedId f WHERE myPartitionId.myPartitionId = :partition_id AND myResourceType = :resource_type AND myForcedId = :forced_id")
Optional<Long> findByPartitionIdAndTypeAndForcedId(@Param("partition_id") Integer thePartitionId, @Param("resource_type") String theResourceType, @Param("forced_id") String theForcedId);

View File

@ -36,6 +36,7 @@ import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.github.benmanes.caffeine.cache.Cache;
@ -49,6 +50,7 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
@ -96,6 +98,8 @@ public class IdHelperService {
private DaoConfig myDaoConfig;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private FhirContext myFhirCtx;
private Cache<String, Long> myPersistentIdCache;
private Cache<String, IResourceLookup> myResourceLookupCache;
@ -141,7 +145,7 @@ public class IdHelperService {
if (myDaoConfig.isDeleteEnabled()) {
retVal = resolveResourceIdentity(thePartitionId, theResourceType, theId);
} else {
String key = thePartitionId + "/" + theResourceType + "/" + theId;
String key = thePartitionId.getPartitionIdStringOrNullString() + "/" + theResourceType + "/" + theId;
retVal = myPersistentIdCache.get(key, t -> resolveResourceIdentity(thePartitionId, theResourceType, theId));
}
@ -254,9 +258,23 @@ public class IdHelperService {
private Long resolveResourceIdentity(@Nullable PartitionId thePartitionId, @Nonnull String theResourceType, @Nonnull String theId) {
Optional<Long> pid;
if (thePartitionId != null) {
pid = myForcedIdDao.findByPartitionIdAndTypeAndForcedId(thePartitionId.getPartitionId(), theResourceType, theId);
if (thePartitionId.getPartitionId() == null) {
pid = myForcedIdDao.findByPartitionIdNullAndTypeAndForcedId(theResourceType, theId);
} else {
pid = myForcedIdDao.findByPartitionIdAndTypeAndForcedId(thePartitionId.getPartitionId(), theResourceType, theId);
}
} else {
pid = myForcedIdDao.findByTypeAndForcedId(theResourceType, theId);
try {
pid = myForcedIdDao.findByTypeAndForcedId(theResourceType, theId);
} catch (IncorrectResultSizeDataAccessException e) {
/*
* This means that:
* 1. There are two resources with the exact same resource type and forced id
* 2. The unique constraint on this column-pair has been dropped
*/
String msg = myFhirCtx.getLocalizer().getMessage(IdHelperService.class, "nonUniqueForcedId");
throw new PreconditionFailedException(msg);
}
}
if (pid.isPresent() == false) {

View File

@ -4,7 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.PartitionId;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;

View File

@ -130,7 +130,11 @@ abstract class BasePredicateBuilder {
void addPredicateParamMissingForNonReference(String theResourceName, String theParamName, boolean theMissing, Join<ResourceTable, ? extends BaseResourceIndexedSearchParam> theJoin, PartitionId thePartitionId) {
if (thePartitionId != null) {
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), thePartitionId.getPartitionId()));
if (thePartitionId.getPartitionId() != null) {
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue"), thePartitionId.getPartitionId()));
} else {
myQueryRoot.addPredicate(myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue")));
}
}
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myResourceType"), theResourceName));
myQueryRoot.addPredicate(myCriteriaBuilder.equal(theJoin.get("myParamName"), theParamName));
@ -209,7 +213,12 @@ abstract class BasePredicateBuilder {
void addPartitionIdPredicate(PartitionId thePartitionId, Join<ResourceTable, ? extends BasePartitionable> theJoin, List<Predicate> theCodePredicates) {
if (thePartitionId != null) {
Integer partitionId = thePartitionId.getPartitionId();
Predicate partitionPredicate = myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue").as(Integer.class), partitionId);
Predicate partitionPredicate;
if (partitionId != null) {
partitionPredicate = myCriteriaBuilder.equal(theJoin.get("myPartitionIdValue").as(Integer.class), partitionId);
} else {
partitionPredicate = myCriteriaBuilder.isNull(theJoin.get("myPartitionIdValue").as(Integer.class));
}
myQueryRoot.addPredicate(partitionPredicate);
}
}

View File

@ -2,11 +2,11 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoSearchParameter;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirResourceDao;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.parser.DataFormatException;

View File

@ -32,6 +32,7 @@ import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
import ca.uhn.fhir.jpa.dao.IResultIterator;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.dao.SearchBuilderFactory;
import ca.uhn.fhir.jpa.dao.partition.RequestPartitionHelperService;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchInclude;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
@ -154,6 +155,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
private boolean myCustomIsolationSupported;
@Autowired
private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory;
@Autowired
private RequestPartitionHelperService myRequestPartitionHelperService;
/**
* Constructor
@ -297,9 +300,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
return new ResourceGoneException(msg);
}
@Autowired
private RequestPartitionHelperService myRequestPartitionHelperService;
@Override
public IBundleProvider registerSearch(final IFhirResourceDao<?> theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequestDetails) {
final String searchUuid = UUID.randomUUID().toString();

View File

@ -3,20 +3,23 @@ package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.*;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -30,6 +33,7 @@ import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.annotation.DirtiesContext;
import javax.servlet.ServletException;
import java.time.LocalDate;
@ -40,6 +44,7 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -48,13 +53,19 @@ import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
/**
* This should be marked as DIRTIES_CONTEXT because it drops an index
*/
@SuppressWarnings("unchecked")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class PartitioningR4Test extends BaseJpaR4SystemTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PartitioningR4Test.class);
private MyInterceptor myPartitionInterceptor;
private LocalDate myPartitionDate;
private int myPartitionId;
private static boolean ourHaveDroppedForcedIdUniqueConstraint;
@After
public void after() {
@ -80,12 +91,79 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
myPartitionInterceptor = new MyInterceptor();
myInterceptorRegistry.registerInterceptor(myPartitionInterceptor);
if (!ourHaveDroppedForcedIdUniqueConstraint) {
runInTransaction(() -> {
myEntityManager.createNativeQuery("alter table " + ForcedId.HFJ_FORCED_ID + " drop constraint " + ForcedId.IDX_FORCEDID_TYPE_FID).executeUpdate();
});
ourHaveDroppedForcedIdUniqueConstraint = true;
}
}
@Test
public void testCreateSearchParameter_DefaultPartition() {
addCreateNoPartition();
SearchParameter sp = new SearchParameter();
sp.addBase("Patient");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.REFERENCE);
sp.setCode("extpatorg");
sp.setName("extpatorg");
sp.setExpression("Patient.extension('http://patext').value.as(Reference)");
Long id = mySearchParameterDao.create(sp).getId().getIdPartAsLong();
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new);
assertNull(resourceTable.getPartitionId());
});
}
@Test
public void testCreateSearchParameter_DefaultPartitionWithDate() {
addCreateNoPartitionId(myPartitionDate);
SearchParameter sp = new SearchParameter();
sp.addBase("Patient");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.REFERENCE);
sp.setCode("extpatorg");
sp.setName("extpatorg");
sp.setExpression("Patient.extension('http://patext').value.as(Reference)");
Long id = mySearchParameterDao.create(sp).getId().getIdPartAsLong();
runInTransaction(() -> {
// HFJ_RESOURCE
ResourceTable resourceTable = myResourceTableDao.findById(id).orElseThrow(IllegalArgumentException::new);
assertEquals(myPartitionId, resourceTable.getPartitionId().getPartitionId().intValue());
assertEquals(myPartitionDate, resourceTable.getPartitionId().getPartitionDate());
});
}
@Test
public void testCreateResourceNoPartition() {
addCreatePartition(null, null);
public void testCreateSearchParameter_NonDefaultPartition() {
addCreatePartition(myPartitionId, myPartitionDate);
SearchParameter sp = new SearchParameter();
sp.addBase("Patient");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.REFERENCE);
sp.setCode("extpatorg");
sp.setName("extpatorg");
sp.setExpression("Patient.extension('http://patext').value.as(Reference)");
try {
mySearchParameterDao.create(sp);
fail();
} catch (PreconditionFailedException e) {
assertEquals("", e.getMessage());
}
}
@Test
public void testCreate_ServerId_NoPartition() {
addCreateNoPartition();
Patient p = new Patient();
p.addIdentifier().setSystem("system").setValue("value");
@ -100,7 +178,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
@Test
public void testCreateResourceWithPartition() {
public void testCreate_ServerId_WithPartition() {
createUniqueCompositeSp();
createRequestId();
@ -185,7 +263,93 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testCreateWithForcedId() {
public void testCreate_ServerId_DefaultPartition() {
createUniqueCompositeSp();
createRequestId();
addCreateNoPartitionId(myPartitionDate);
addCreateNoPartitionId(myPartitionDate);
Organization org = new Organization();
org.setName("org");
IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless();
Patient p = new Patient();
p.getMeta().addTag("http://system", "code", "diisplay");
p.addName().setFamily("FAM");
p.addIdentifier().setSystem("system").setValue("value");
p.setBirthDate(new Date());
p.getManagingOrganization().setReferenceElement(orgId);
Long patientId = myPatientDao.create(p, mySrd).getId().getIdPartAsLong();
runInTransaction(() -> {
// HFJ_RESOURCE
ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new);
assertEquals(null, resourceTable.getPartitionId().getPartitionId());
assertEquals(myPartitionDate, resourceTable.getPartitionId().getPartitionDate());
// HFJ_RES_TAG
List<ResourceTag> tags = myResourceTagDao.findAll();
assertEquals(1, tags.size());
assertEquals(null, tags.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, tags.get(0).getPartitionId().getPartitionDate());
// HFJ_RES_VER
ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, 1L);
assertEquals(null, version.getPartitionId().getPartitionId());
assertEquals(myPartitionDate, version.getPartitionId().getPartitionDate());
// HFJ_HISTORY_TAG
List<ResourceHistoryTag> historyTags = myResourceHistoryTagDao.findAll();
assertEquals(1, historyTags.size());
assertEquals(null, historyTags.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, historyTags.get(0).getPartitionId().getPartitionDate());
// HFJ_RES_VER_PROV
assertNotNull(version.getProvenance());
assertEquals(null, version.getProvenance().getPartitionId().getPartitionId());
assertEquals(myPartitionDate, version.getProvenance().getPartitionId().getPartitionDate());
// HFJ_SPIDX_STRING
List<ResourceIndexedSearchParamString> strings = myResourceIndexedSearchParamStringDao.findAllForResourceId(patientId);
ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * ")));
assertEquals(10, strings.size());
assertEquals(null, strings.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, strings.get(0).getPartitionId().getPartitionDate());
// HFJ_SPIDX_DATE
List<ResourceIndexedSearchParamDate> dates = myResourceIndexedSearchParamDateDao.findAllForResourceId(patientId);
ourLog.info("\n * {}", dates.stream().map(ResourceIndexedSearchParamDate::toString).collect(Collectors.joining("\n * ")));
assertEquals(2, dates.size());
assertEquals(null, dates.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, dates.get(0).getPartitionId().getPartitionDate());
assertEquals(null, dates.get(1).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, dates.get(1).getPartitionId().getPartitionDate());
// HFJ_RES_LINK
List<ResourceLink> resourceLinks = myResourceLinkDao.findAllForResourceId(patientId);
assertEquals(1, resourceLinks.size());
assertEquals(null, resourceLinks.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, resourceLinks.get(0).getPartitionId().getPartitionDate());
// HFJ_RES_PARAM_PRESENT
List<SearchParamPresent> presents = mySearchParamPresentDao.findAllForResource(resourceTable);
assertEquals(3, presents.size());
assertEquals(null, presents.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, presents.get(0).getPartitionId().getPartitionDate());
// HFJ_IDX_CMP_STRING_UNIQ
List<ResourceIndexedCompositeStringUnique> uniques = myResourceIndexedCompositeStringUniqueDao.findAllForResourceId(patientId);
assertEquals(1, uniques.size());
assertEquals(null, uniques.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, uniques.get(0).getPartitionId().getPartitionDate());
});
}
@Test
public void testCreate_ForcedId_WithPartition() {
addCreatePartition(myPartitionId, myPartitionDate);
addCreatePartition(myPartitionId, myPartitionDate);
@ -211,6 +375,59 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testCreate_ForcedId_NoPartition() {
addCreateNoPartition();
addCreateNoPartition();
Organization org = new Organization();
org.setId("org");
org.setName("org");
IIdType orgId = myOrganizationDao.update(org).getId().toUnqualifiedVersionless();
Patient p = new Patient();
p.setId("pat");
p.getManagingOrganization().setReferenceElement(orgId);
myPatientDao.update(p, mySrd);
runInTransaction(() -> {
// HFJ_FORCED_ID
List<ForcedId> forcedIds = myForcedIdDao.findAll();
assertEquals(2, forcedIds.size());
assertEquals(null, forcedIds.get(0).getPartitionId());
assertEquals(null, forcedIds.get(1).getPartitionId());
});
}
@Test
public void testCreate_ForcedId_DefaultPartition() {
addCreateNoPartitionId(myPartitionDate);
addCreateNoPartitionId(myPartitionDate);
Organization org = new Organization();
org.setId("org");
org.setName("org");
IIdType orgId = myOrganizationDao.update(org).getId().toUnqualifiedVersionless();
Patient p = new Patient();
p.setId("pat");
p.getManagingOrganization().setReferenceElement(orgId);
myPatientDao.update(p, mySrd);
runInTransaction(() -> {
// HFJ_FORCED_ID
List<ForcedId> forcedIds = myForcedIdDao.findAll();
assertEquals(2, forcedIds.size());
assertEquals(null, forcedIds.get(0).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, forcedIds.get(0).getPartitionId().getPartitionDate());
assertEquals(null, forcedIds.get(1).getPartitionId().getPartitionId());
assertEquals(myPartitionDate, forcedIds.get(1).getPartitionId().getPartitionDate());
});
}
@Test
public void testUpdateResourceWithPartition() {
createRequestId();
@ -273,51 +490,116 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testReadAcrossPartitions() {
public void testRead_PidId_AllPartitions() {
IIdType patientId1 = createPatient(1, withActiveTrue());
IIdType patientId2 = createPatient(2, withActiveTrue());
addReadPartition(null);
IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
{
addReadPartition(null);
myCaptureQueriesListener.clear();
IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
addReadPartition(null);
IdType gotId2 = myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId2, gotId2);
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
}
{
addReadPartition(null);
IdType gotId2 = myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId2, gotId2);
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
}
}
@Test
public void testReadSpecificPartition_PidId() {
public void testRead_PidId_SpecificPartition() {
IIdType patientIdNull = createPatient(null, withActiveTrue());
IIdType patientId1 = createPatient(1, withActiveTrue());
IIdType patientId2 = createPatient(2, withActiveTrue());
// Read in correct Partition
addReadPartition(1);
IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
{
myCaptureQueriesListener.clear();
addReadPartition(1);
IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
}
// Read in null Partition
addReadPartition(1);
try {
myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
{
addReadPartition(1);
try {
myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
}
}
// Read in wrong Partition
addReadPartition(1);
try {
myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
{
addReadPartition(1);
try {
myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
}
}
}
@Test
public void testReadSpecificPartition_ForcedId() {
public void testRead_PidId_DefaultPartition() {
IIdType patientIdNull = createPatient(null, withActiveTrue());
IIdType patientId1 = createPatient(1, withActiveTrue());
createPatient(2, withActiveTrue());
// Read in correct Partition
{
myCaptureQueriesListener.clear();
addDefaultReadPartition();
IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientIdNull, gotId1);
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
// Only the read columns should be used, no criteria use partition
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID as "));
assertEquals(2, StringUtils.countMatches(searchSql, "PARTITION_ID"));
}
// Read in wrong Partition
{
addDefaultReadPartition();
try {
myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
}
}
}
@Test
public void testRead_ForcedId_SpecificPartition() {
IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(1, withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(2, withActiveTrue(), withId("TWO"));
@ -333,7 +615,7 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
assertThat(e.getMessage(), matchesPattern("Resource Patient/NULL is not known"));
}
// Read in wrong Partition
@ -342,7 +624,79 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/[0-9]+ is not known"));
assertThat(e.getMessage(), matchesPattern("Resource Patient/TWO is not known"));
}
}
@Test
public void testRead_ForcedId_DefaultPartition() {
IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(1, withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(2, withActiveTrue(), withId("TWO"));
// Read in correct Partition
addDefaultReadPartition();
IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientIdNull, gotId1);
// Read in null Partition
addDefaultReadPartition();
try {
myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/ONE is not known"));
}
// Read in wrong Partition
addDefaultReadPartition();
try {
myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
fail();
} catch (ResourceNotFoundException e) {
assertThat(e.getMessage(), matchesPattern("Resource Patient/TWO is not known"));
}
}
@Test
public void testRead_ForcedId_AllPartition() {
IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("NULL"));
IIdType patientId1 = createPatient(1, withActiveTrue(), withId("ONE"));
IIdType patientId2 = createPatient(2, withActiveTrue(), withId("TWO"));
{
addReadPartition(null);
IdType gotId1 = myPatientDao.read(patientIdNull, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientIdNull, gotId1);
}
{
addReadPartition(null);
IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId1, gotId1);
}
{
// Read in wrong Partition
addReadPartition(null);
IdType gotId1 = myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless();
assertEquals(patientId2, gotId1);
}
}
@Test
public void testRead_ForcedId_AllPartition_WithDuplicate() {
IIdType patientIdNull = createPatient(null, withActiveTrue(), withId("FOO"));
IIdType patientId1 = createPatient(1, withActiveTrue(), withId("FOO"));
IIdType patientId2 = createPatient(2, withActiveTrue(), withId("FOO"));
assertEquals(patientIdNull, patientId1);
assertEquals(patientIdNull, patientId2);
{
addReadPartition(null);
try {
myPatientDao.read(patientIdNull, mySrd);
fail();
} catch (PreconditionFailedException e) {
assertEquals("Non-unique ID specified, can not process request", e.getMessage());
}
}
}
@ -387,31 +741,6 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
}
}
@Test
public void testSearch_MissingParamReference_SearchAllPartitions() {
IIdType patientIdNull = createPatient(null, withFamily("FAMILY"));
IIdType patientId1 = createPatient(1, withFamily("FAMILY"));
IIdType patientId2 = createPatient(2, withFamily("FAMILY"));
// :missing=true
{
addReadPartition(null);
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_GENERAL_PRACTITIONER, new StringParam().setMissing(true));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"));
assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE='1919227773735728687'"));
}
}
@Test
public void testSearch_MissingParamString_SearchOnePartition() {
@ -454,6 +783,72 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
}
}
@Test
public void testSearch_MissingParamString_SearchDefaultPartition() {
IIdType patientIdNull = createPatient(null, withFamily("FAMILY"));
createPatient(1, withFamily("FAMILY"));
createPatient(2, withFamily("FAMILY"));
// :missing=true
{
addDefaultReadPartition();
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_ACTIVE, new StringParam().setMissing(true));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, Matchers.contains(patientIdNull));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_MISSING='true'"));
}
// :missing=false
{
addDefaultReadPartition();
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_FAMILY, new StringParam().setMissing(false));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, Matchers.contains(patientIdNull));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_MISSING='false'"));
}
}
@Test
public void testSearch_MissingParamReference_SearchAllPartitions() {
IIdType patientIdNull = createPatient(null, withFamily("FAMILY"));
IIdType patientId1 = createPatient(1, withFamily("FAMILY"));
IIdType patientId2 = createPatient(2, withFamily("FAMILY"));
// :missing=true
{
addReadPartition(null);
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_GENERAL_PRACTITIONER, new StringParam().setMissing(true));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"));
assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE='1919227773735728687'"));
}
}
@Test
public void testSearch_MissingParamReference_SearchOnePartition() {
createPatient(null, withFamily("FAMILY"));
@ -481,6 +876,32 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testSearch_MissingParamReference_SearchDefaultPartition() {
IIdType patientIdDefault = createPatient(null, withFamily("FAMILY"));
createPatient(1, withFamily("FAMILY"));
createPatient(2, withFamily("FAMILY"));
// :missing=true
{
addDefaultReadPartition();
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_GENERAL_PRACTITIONER, new StringParam().setMissing(true));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, Matchers.contains(patientIdDefault));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(1, StringUtils.countMatches(searchSql, "mysearchpa1_.PARTITION_ID is null"));
assertEquals(1, StringUtils.countMatches(searchSql, "HFJ_RES_PARAM_PRESENT"));
assertEquals(1, StringUtils.countMatches(searchSql, "HASH_PRESENCE='1919227773735728687'"));
}
}
@Test
public void testSearch_NoParams_SearchAllPartitions() {
@ -544,6 +965,30 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_NORMALIZED"));
}
@Test
public void testSearch_StringParam_SearchDefaultPartition() {
IIdType patientIdNull = createPatient(null, withFamily("FAMILY"));
createPatient(1, withFamily("FAMILY"));
createPatient(2, withFamily("FAMILY"));
addDefaultReadPartition();
myCaptureQueriesListener.clear();
SearchParameterMap map = new SearchParameterMap();
map.add(Patient.SP_FAMILY, new StringParam("FAMILY"));
map.setLoadSynchronous(true);
IBundleProvider results = myPatientDao.search(map);
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
assertThat(ids, Matchers.contains(patientIdNull));
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
ourLog.info("Search SQL:\n{}", searchSql);
searchSql = searchSql.toUpperCase();
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID IS NULL"));
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_NORMALIZED"));
}
@Test
public void testSearch_StringParam_SearchOnePartition() {
createPatient(null, withFamily("FAMILY"));
@ -747,10 +1192,17 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
private void addCreatePartition(Integer thePartitionId, LocalDate thePartitionDate) {
PartitionId partitionId = null;
if (thePartitionId != null) {
partitionId = new PartitionId(thePartitionId, thePartitionDate);
}
Validate.notNull(thePartitionId);
PartitionId partitionId = new PartitionId(thePartitionId, thePartitionDate);
myPartitionInterceptor.addCreatePartition(partitionId);
}
private void addCreateNoPartition() {
myPartitionInterceptor.addCreatePartition(null);
}
private void addCreateNoPartitionId(LocalDate thePartitionDate) {
PartitionId partitionId = new PartitionId(null, thePartitionDate);
myPartitionInterceptor.addCreatePartition(partitionId);
}
@ -762,15 +1214,29 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
myPartitionInterceptor.addReadPartition(partitionId);
}
private void addDefaultReadPartition() {
PartitionId partitionId = new PartitionId(null, null);
myPartitionInterceptor.addReadPartition(partitionId);
}
public IIdType createPatient(Integer thePartitionId, Consumer<Patient>... theModifiers) {
addCreatePartition(thePartitionId, null);
if (thePartitionId != null) {
addCreatePartition(thePartitionId, null);
} else {
addCreateNoPartition();
}
Patient p = new Patient();
for (Consumer<Patient> next : theModifiers) {
next.accept(p);
}
return myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
if (isNotBlank(p.getId())) {
return myPatientDao.update(p, mySrd).getId().toUnqualifiedVersionless();
} else {
return myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless();
}
}
public void createRequestId() {

View File

@ -25,7 +25,7 @@ import org.hibernate.annotations.ColumnDefault;
import javax.persistence.*;
@Entity()
@Table(name = "HFJ_FORCED_ID", uniqueConstraints = {
@Table(name = ForcedId.HFJ_FORCED_ID, uniqueConstraints = {
@UniqueConstraint(name = "IDX_FORCEDID_RESID", columnNames = {"RESOURCE_PID"}),
@UniqueConstraint(name = ForcedId.IDX_FORCEDID_TYPE_FID, columnNames = {"RESOURCE_TYPE", "FORCED_ID"})
}, indexes = {
@ -40,6 +40,7 @@ public class ForcedId {
public static final int MAX_FORCED_ID_LENGTH = 100;
public static final String IDX_FORCEDID_TYPE_FID = "IDX_FORCEDID_TYPE_FID";
public static final String HFJ_FORCED_ID = "HFJ_FORCED_ID";
@Column(name = "FORCED_ID", nullable = false, length = MAX_FORCED_ID_LENGTH, updatable = false)
private String myForcedId;

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.model.entity;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.time.LocalDate;
@ -27,26 +27,27 @@ public class PartitionId implements Cloneable {
/**
* Constructor
*/
public PartitionId(int thePartitionId, LocalDate thePartitionDate) {
public PartitionId(@Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) {
setPartitionId(thePartitionId);
setPartitionDate(thePartitionDate);
}
@Nonnull
@Nullable
public Integer getPartitionId() {
return myPartitionId;
}
public PartitionId setPartitionId(@Nonnull Integer thePartitionId) {
public PartitionId setPartitionId(@Nullable Integer thePartitionId) {
myPartitionId = thePartitionId;
return this;
}
@Nullable
public LocalDate getPartitionDate() {
return myPartitionDate;
}
public PartitionId setPartitionDate(LocalDate thePartitionDate) {
public PartitionId setPartitionDate(@Nullable LocalDate thePartitionDate) {
myPartitionDate = thePartitionDate;
return this;
}
@ -61,6 +62,13 @@ public class PartitionId implements Cloneable {
@Override
public String toString() {
return getPartitionIdStringOrNullString();
}
/**
* Returns the partition ID (numeric) as a string, or the string "null"
*/
public String getPartitionIdStringOrNullString() {
return defaultIfNull(myPartitionId, "null").toString();
}
}