From b2d2346228a8d1fcaa17afbf8e2932da661b4815 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 7 Feb 2020 15:18:06 -0500 Subject: [PATCH 01/63] Work on multitenancy --- .../java/ca/uhn/fhir/util/VersionEnum.java | 3 +- .../hapi/fhir/changelog/4_3_0/changes.yaml | 13 ++ hapi-fhir-jpaserver-base/pom.xml | 53 ++----- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 34 ++++- .../fhir/jpa/dao/r4/MultitenantR4Test.java | 130 ++++++++++++++++++ .../tasks/HapiFhirJpaMigrationTasks.java | 8 +- .../jpa/model/entity/BaseHasResource.java | 12 +- .../BaseResourceIndexedSearchParam.java | 29 +++- .../fhir/jpa/model/entity/ResourceTable.java | 33 ++--- .../fhir/jpa/model/entity/ResourceTag.java | 36 +++-- .../uhn/fhir/jpa/model/entity/TenantId.java | 39 ++++++ .../uhn/fhir/jpa/model/util/JpaConstants.java | 55 ++------ pom.xml | 7 +- 13 files changed, 317 insertions(+), 135 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index acc92dcb2a6..99d8ccd7939 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -58,7 +58,8 @@ public enum VersionEnum { V4_0_0, V4_0_3, V4_1_0, - V4_2_0; + V4_2_0, + V4_3_0; public static VersionEnum latestVersion() { VersionEnum[] values = VersionEnum.values(); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml new file mode 100644 index 00000000000..fd6d640f6b3 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml @@ -0,0 +1,13 @@ +--- +- item: + type: "add" + title: "The version of a few dependencies have been bumped to the latest versions + (dependent HAPI modules listed in brackets): + " +- item: + type: change + title: "The JPA server now shared a single set of tags for all versions of a resource, bringing it in line with the + functional description in the FHIR specification. This means that it is no longer possible to modify the tags for + a specific version of a resource, and also causes significant performance improvements in some cases." diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 73c106c90e2..3e94f6f8eda 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -236,18 +236,6 @@ javax.annotation javax.annotation-api - - - - - @@ -255,11 +243,6 @@ derby test - org.apache.derby derbytools @@ -319,15 +302,7 @@ - - - org.springframework spring-core @@ -370,12 +345,6 @@ org.springframework spring-messaging - org.springframework spring-tx @@ -422,6 +391,16 @@ + + org.hibernate + hibernate-java8 + + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + + + org.hibernate hibernate-ehcache @@ -451,10 +430,6 @@ javax.activation 1.2.0 - - - - javax.mail javax.mail-api @@ -471,14 +446,6 @@ - - - - - javax.el javax.el-api diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index fa4b228bb78..81940910cba 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1,10 +1,21 @@ package ca.uhn.fhir.jpa.dao; -import ca.uhn.fhir.context.*; +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeChildResourceDefinition; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; 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.data.*; +import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; +import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; +import ca.uhn.fhir.jpa.dao.data.IResourceProvenanceDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; +import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; import ca.uhn.fhir.jpa.dao.expunge.ExpungeService; import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; import ca.uhn.fhir.jpa.dao.index.IdHelperService; @@ -75,7 +86,11 @@ import org.springframework.transaction.support.TransactionSynchronizationAdapter import org.springframework.transaction.support.TransactionSynchronizationManager; import javax.annotation.PostConstruct; -import javax.persistence.*; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceContextType; +import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; @@ -84,7 +99,12 @@ import javax.xml.stream.events.XMLEvent; import java.util.*; import java.util.Map.Entry; -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.left; +import static org.apache.commons.lang3.StringUtils.trim; /* * #%L @@ -149,6 +169,8 @@ public abstract class BaseHapiFhirDao extends BaseStora @Autowired protected IInterceptorBroadcaster myInterceptorBroadcaster; @Autowired + protected DaoRegistry myDaoRegistry; + @Autowired ExpungeService myExpungeService; @Autowired private DaoConfig myConfig; @@ -159,8 +181,6 @@ public abstract class BaseHapiFhirDao extends BaseStora @Autowired private ISearchParamPresenceSvc mySearchParamPresenceSvc; @Autowired - protected DaoRegistry myDaoRegistry; - @Autowired private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor; @Autowired private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer; @@ -620,6 +640,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } } + return retVal; } @@ -681,6 +702,7 @@ public abstract class BaseHapiFhirDao extends BaseStora } } } + return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java new file mode 100644 index 00000000000..bc68bfaef01 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java @@ -0,0 +1,130 @@ +package ca.uhn.fhir.jpa.dao.r4; + +import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; +import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.ResourceTag; +import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.TestUtil; +import org.apache.commons.io.IOUtils; +import org.hamcrest.Matchers; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent; +import org.hl7.fhir.r4.model.Bundle.BundleType; +import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.Month; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.matchesPattern; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class MultitenantR4Test extends BaseJpaR4SystemTest { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MultitenantR4Test.class); + + @After + public void after() { + } + + @Before + public void beforeDisableResultReuse() { + } + + + @Test + public void testCreateResourceNoTenant() { + Patient p = new Patient(); + p.addIdentifier().setSystem("system").setValue("value"); + p.setBirthDate(new Date()); + Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); + + runInTransaction(()->{ + ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(() -> new IllegalArgumentException()); + assertNull(resourceTable.getTenantId()); + }); + } + + @Test + public void testCreateResourceWithTenant() { + Patient p = new Patient(); + p.setUserData(JpaConstants.USERDATA_TENANT_ID, 3); + p.setUserData(JpaConstants.USERDATA_TENANT_DATE, LocalDate.of(2020, Month.JANUARY, 14)); + p.addIdentifier().setSystem("system").setValue("value"); + p.setBirthDate(new Date()); + Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); + + runInTransaction(()->{ + ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(() -> new IllegalArgumentException()); + assertNull(resourceTable.getTenantId()); + }); + + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 90e920e1234..41a430a2bc4 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -56,7 +56,13 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { init360(); // 20180918 - 20181112 init400(); // 20190401 - 20190814 init410(); // 20190815 - 20191014 - init420(); // 20191015 - present + init420(); // 20191015 - 20200213 + init430(); // 20200213 - present + } + + protected void init430() { + Builder version = forVersion(VersionEnum.V4_3_0); + } protected void init420() { // 20191015 - present diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java index 6970b64dc68..71ad0e10e80 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseHasResource.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.jpa.model.entity; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; -import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.api.Constants; @@ -64,6 +63,9 @@ public abstract class BaseHasResource implements IBaseResourceEntity, IBasePersi @OptimisticLock(excluded = true) private Date myUpdated; + @Embedded + private TenantId myTenantId; + /** * This is stored as an optimization to avoid neeind to query for this * after an update @@ -71,6 +73,14 @@ public abstract class BaseHasResource implements IBaseResourceEntity, IBasePersi @Transient private transient String myTransientForcedId; + public TenantId getTenantId() { + return myTenantId; + } + + public void setTenantId(TenantId theTenantId) { + myTenantId = theTenantId; + } + public String getTransientForcedId() { return myTransientForcedId; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java index b891a3889b2..b88d63e6cb1 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java @@ -31,7 +31,14 @@ import com.google.common.hash.Hashing; import org.hibernate.search.annotations.ContainedIn; import org.hibernate.search.annotations.Field; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; import java.util.Date; @MappedSuperclass @@ -47,10 +54,9 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { private static final byte[] DELIMITER_BYTES = "|".getBytes(Charsets.UTF_8); private static final long serialVersionUID = 1L; - // TODO: make this nullable=false and a primitive (written may 2017) @Field() - @Column(name = "SP_MISSING", nullable = true) - private Boolean myMissing = Boolean.FALSE; + @Column(name = "SP_MISSING", nullable = false) + private boolean myMissing = false; @Field @Column(name = "SP_NAME", length = MAX_SP_NAME, nullable = false) @@ -64,14 +70,27 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { @Column(name = "RES_ID", insertable = false, updatable = false, nullable = false) private Long myResourcePid; + // FIXME: replace with join @Field() @Column(name = "RES_TYPE", nullable = false, length = Constants.MAX_RESOURCE_NAME_LENGTH) private String myResourceType; + @Field() @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3 @Temporal(TemporalType.TIMESTAMP) private Date myUpdated; + @Embedded + private TenantId myTenantId; + + public TenantId getTenantId() { + return myTenantId; + } + + public void setTenantId(TenantId theTenantId) { + myTenantId = theTenantId; + } + /** * Subclasses may override */ @@ -123,7 +142,7 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { } public boolean isMissing() { - return Boolean.TRUE.equals(myMissing); + return myMissing; } public BaseResourceIndexedSearchParam setMissing(boolean theMissing) { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index b46c9d9cd6c..cf6511f28ab 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -27,12 +27,20 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hibernate.annotations.OptimisticLock; -import org.hibernate.search.annotations.*; +import org.hibernate.search.annotations.Analyze; +import org.hibernate.search.annotations.Analyzer; +import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.Fields; +import org.hibernate.search.annotations.Indexed; +import org.hibernate.search.annotations.Store; -import javax.persistence.Index; import javax.persistence.*; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.defaultString; @@ -52,12 +60,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas private static final int MAX_PROFILE_LENGTH = 200; private static final long serialVersionUID = 1L; -// @Transient -// private transient byte[] myResource; -// -// @Transient -// private transient ResourceEncodingEnum myEncoding; - /** * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB */ @@ -186,9 +188,9 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas * {@link #myHasLinks} is true, meaning that there are actually resource links present * right now. This avoids Hibernate Search triggering a select on the resource link * table. - * + *

* This field is used by FulltextSearchSvcImpl - * + *

* You can test that any changes don't cause extra queries by running * FhirResourceDaoR4QueryCountTest */ @@ -421,7 +423,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas return this; } - @Override public Collection getTags() { if (myTags == null) { myTags = new HashSet<>(); @@ -590,16 +591,16 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas * This is a convenience to avoid loading the version a second time within a single transaction. It is * not persisted. */ - public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) { - myCurrentVersionEntity = theCurrentVersionEntity; + public ResourceHistoryTable getCurrentVersionEntity() { + return myCurrentVersionEntity; } /** * This is a convenience to avoid loading the version a second time within a single transaction. It is * not persisted. */ - public ResourceHistoryTable getCurrentVersionEntity() { - return myCurrentVersionEntity; + public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) { + myCurrentVersionEntity = theCurrentVersionEntity; } @Override diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java index 9fcb5200ce4..7d06439837c 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java @@ -28,8 +28,8 @@ import org.apache.commons.lang3.builder.ToStringStyle; import javax.persistence.*; @Entity -@Table(name = "HFJ_RES_TAG", uniqueConstraints= { - @UniqueConstraint(name="IDX_RESTAG_TAGID", columnNames= {"RES_ID","TAG_ID"}) +@Table(name = "HFJ_RES_TAG", uniqueConstraints = { + @UniqueConstraint(name = "IDX_RESTAG_TAGID", columnNames = {"RES_ID", "TAG_ID"}) }) public class ResourceTag extends BaseTag { @@ -42,7 +42,7 @@ public class ResourceTag extends BaseTag { private Long myId; @ManyToOne(cascade = {}) - @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", foreignKey=@ForeignKey(name="FK_RESTAG_RESOURCE")) + @JoinColumn(name = "RES_ID", referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_RESTAG_RESOURCE")) private ResourceTable myResource; @Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false) @@ -50,14 +50,8 @@ public class ResourceTag extends BaseTag { @Column(name = "RES_ID", insertable = false, updatable = false) private Long myResourceId; - - public Long getResourceId() { - return myResourceId; - } - - public void setResourceId(Long theResourceId) { - myResourceId = theResourceId; - } + @Embedded + private TenantId myTenantId; public ResourceTag() { } @@ -69,18 +63,30 @@ public class ResourceTag extends BaseTag { setResourceType(theResourceTable.getResourceType()); } + public void setTenantId(TenantId theTenantId) { + myTenantId = theTenantId; + } + + public Long getResourceId() { + return myResourceId; + } + + public void setResourceId(Long theResourceId) { + myResourceId = theResourceId; + } + public ResourceTable getResource() { return myResource; } - public String getResourceType() { - return myResourceType; - } - public void setResource(ResourceTable theResource) { myResource = theResource; } + public String getResourceType() { + return myResourceType; + } + public void setResourceType(String theResourceType) { myResourceType = theResourceType; } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java new file mode 100644 index 00000000000..3143e8559cf --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.jpa.model.entity; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.time.LocalDate; + +@Embeddable +public class TenantId implements Cloneable { + + @Column(name = "TENANT_ID", nullable = true) + private Integer myTenantId; + @Column(name = "TENANT_DATE", nullable = true) + private LocalDate myTenantDate; + + public Integer getTenantId() { + return myTenantId; + } + + public TenantId setTenantId(Integer theTenantId) { + myTenantId = theTenantId; + return this; + } + + public LocalDate getTenantDate() { + return myTenantDate; + } + + public TenantId setTenantDate(LocalDate theTenantDate) { + myTenantDate = theTenantDate; + return this; + } + + @Override + protected TenantId clone() { + return new TenantId() + .setTenantId(getTenantId()) + .setTenantDate(getTenantDate()); + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index 66f2c5b17ed..d5a9a547828 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -24,39 +24,30 @@ import ca.uhn.fhir.rest.api.Constants; public class JpaConstants { - /** - * Non-instantiable - */ - private JpaConstants() { - // nothing - } + public static final String USERDATA_TENANT_ID = JpaConstants.class.getName() + "_USERDATA_TENANT_ID"; + public static final String USERDATA_TENANT_DATE = JpaConstants.class.getName() + "_USERDATA_TENANT_DATE"; /** * Operation name for the $apply-codesystem-delta-add operation */ public static final String OPERATION_APPLY_CODESYSTEM_DELTA_ADD = "$apply-codesystem-delta-add"; - /** * Operation name for the $apply-codesystem-delta-remove operation */ public static final String OPERATION_APPLY_CODESYSTEM_DELTA_REMOVE = "$apply-codesystem-delta-remove"; - /** * Operation name for the $expunge operation */ public static final String OPERATION_EXPUNGE = "$expunge"; - /** * Operation name for the $match operation */ public static final String OPERATION_MATCH = "$match"; - /** * @deprecated Replace with {@link #OPERATION_EXPUNGE} */ @Deprecated public static final String OPERATION_NAME_EXPUNGE = OPERATION_EXPUNGE; - /** * Parameter name for the $expunge operation */ @@ -84,113 +75,91 @@ public class JpaConstants { * be removed if they are nt explicitly included in updates */ public static final String HEADER_META_SNAPSHOT_MODE = "X-Meta-Snapshot-Mode"; - /** * Operation name for the $lookup operation */ public static final String OPERATION_LOOKUP = "$lookup"; - /** * Operation name for the $expand operation */ public static final String OPERATION_EXPAND = "$expand"; - /** * Operation name for the $validate-code operation */ public static final String OPERATION_VALIDATE_CODE = "$validate-code"; - /** * Operation name for the $get-resource-counts operation */ public static final String OPERATION_GET_RESOURCE_COUNTS = "$get-resource-counts"; - /** * Operation name for the $meta operation */ public static final String OPERATION_META = "$meta"; - /** * Operation name for the $validate operation */ // NB don't delete this, it's used in Smile as well, even though hapi-fhir-server uses the version from Constants.java public static final String OPERATION_VALIDATE = Constants.EXTOP_VALIDATE; - /** * Operation name for the $suggest-keywords operation */ public static final String OPERATION_SUGGEST_KEYWORDS = "$suggest-keywords"; - /** * Operation name for the $everything operation */ public static final String OPERATION_EVERYTHING = "$everything"; - /** * Operation name for the $process-message operation */ public static final String OPERATION_PROCESS_MESSAGE = "$process-message"; - /** * Operation name for the $meta-delete operation */ public static final String OPERATION_META_DELETE = "$meta-delete"; - /** * Operation name for the $meta-add operation */ public static final String OPERATION_META_ADD = "$meta-add"; - /** * Operation name for the $translate operation */ public static final String OPERATION_TRANSLATE = "$translate"; - /** * Operation name for the $document operation */ public static final String OPERATION_DOCUMENT = "$document"; - /** * Trigger a subscription manually for a given resource */ public static final String OPERATION_TRIGGER_SUBSCRIPTION = "$trigger-subscription"; - /** * Operation name for the "$subsumes" operation */ public static final String OPERATION_SUBSUMES = "$subsumes"; - /** * Operation name for the "$snapshot" operation */ public static final String OPERATION_SNAPSHOT = "$snapshot"; - /** * Operation name for the "$binary-access" operation */ public static final String OPERATION_BINARY_ACCESS_READ = "$binary-access-read"; - /** * Operation name for the "$binary-access" operation */ public static final String OPERATION_BINARY_ACCESS_WRITE = "$binary-access-write"; - /** * Operation name for the "$upload-external-code-system" operation */ public static final String OPERATION_UPLOAD_EXTERNAL_CODE_SYSTEM = "$upload-external-code-system"; - /** * Operation name for the "$export" operation */ public static final String OPERATION_EXPORT = "$export"; - /** * Operation name for the "$export-poll-status" operation */ public static final String OPERATION_EXPORT_POLL_STATUS = "$export-poll-status"; - /** *

* This extension should be of type string and should be @@ -198,7 +167,6 @@ public class JpaConstants { *

*/ public static final String EXT_SUBSCRIPTION_SUBJECT_TEMPLATE = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-subject-template"; - /** * This extension URL indicates whether a REST HOOK delivery should * include the version ID when delivering. @@ -208,7 +176,6 @@ public class JpaConstants { *

*/ public static final String EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids"; - /** * This extension URL indicates whether a REST HOOK delivery should * reload the resource and deliver the latest version always. This @@ -226,12 +193,10 @@ public class JpaConstants { *

*/ public static final String EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION = "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version"; - /** * Indicate which strategy will be used to match this subscription */ public static final String EXT_SUBSCRIPTION_MATCHING_STRATEGY = "http://hapifhir.io/fhir/StructureDefinition/subscription-matching-strategy"; - /** *

* This extension should be of type string and should be @@ -239,45 +204,43 @@ public class JpaConstants { *

*/ public static final String EXT_SUBSCRIPTION_EMAIL_FROM = "http://hapifhir.io/fhir/StructureDefinition/subscription-email-from"; - /** * Extension ID for external binary references */ public static final String EXT_EXTERNALIZED_BINARY_ID = "http://hapifhir.io/fhir/StructureDefinition/externalized-binary-id"; - /** * Placed in system-generated extensions */ public static final String EXTENSION_EXT_SYSTEMDEFINED = JpaConstants.class.getName() + "_EXTENSION_EXT_SYSTEMDEFINED"; - /** * Message added to expansion valueset */ public static final String EXT_VALUESET_EXPANSION_MESSAGE = "http://hapifhir.io/fhir/StructureDefinition/valueset-expansion-message"; - - /** * Parameter for the $export operation */ public static final String PARAM_EXPORT_POLL_STATUS_JOB_ID = "_jobId"; - /** * Parameter for the $export operation */ public static final String PARAM_EXPORT_OUTPUT_FORMAT = "_outputFormat"; - /** * Parameter for the $export operation */ public static final String PARAM_EXPORT_TYPE = "_type"; - /** * Parameter for the $export operation */ public static final String PARAM_EXPORT_SINCE = "_since"; - /** * Parameter for the $export operation */ public static final String PARAM_EXPORT_TYPE_FILTER = "_typeFilter"; + + /** + * Non-instantiable + */ + private JpaConstants() { + // nothing + } } diff --git a/pom.xml b/pom.xml index 1371e1b3c8a..2ef99fd2479 100644 --- a/pom.xml +++ b/pom.xml @@ -645,7 +645,7 @@ 3.0.2 6.1.0 - 5.4.6.Final + 5.4.10.Final 5.11.3.Final 5.5.5 @@ -1278,6 +1278,11 @@ hibernate-core ${hibernate_version}
+ + org.hibernate + hibernate-java8 + ${hibernate_version} + org.hibernate hibernate-ehcache From f7ec41ffc5e03aa1abc127950ccd830bd8a8ac3d Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sat, 8 Feb 2020 19:59:37 -0500 Subject: [PATCH 02/63] Work on multitenancy --- .../interceptor/api/IInterceptorService.java | 5 + .../ca/uhn/fhir/interceptor/api/Pointcut.java | 37 ++++++ .../executor/InterceptorService.java | 17 +++ .../hapi/fhir/changelog/4_3_0/changes.yaml | 2 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 15 ++- .../ca/uhn/fhir/jpa/dao/BaseStorageDao.java | 5 +- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 25 +++- .../dao/index/DaoSearchParamSynchronizer.java | 1 + .../fhir/jpa/dao/r4/MultitenantR4Test.java | 123 ++++++++---------- .../jpa/model/entity/BaseResourceIndex.java | 14 ++ .../BaseResourceIndexedSearchParam.java | 11 -- .../fhir/jpa/model/entity/ResourceTable.java | 9 ++ .../uhn/fhir/jpa/model/entity/TenantId.java | 28 +++- .../uhn/fhir/jpa/model/util/JpaConstants.java | 3 - .../uhn/fhirtest/config/TestDstu2Config.java | 2 + .../uhn/fhirtest/config/TestDstu3Config.java | 2 + .../ca/uhn/fhirtest/config/TestR4Config.java | 2 + .../ca/uhn/fhirtest/config/TestR5Config.java | 2 + pom.xml | 4 +- 19 files changed, 216 insertions(+), 91 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java index 04a6d513bbc..c4655ce8acb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.interceptor.api; import javax.annotation.Nullable; import java.util.Collection; import java.util.List; +import java.util.function.Function; public interface IInterceptorService extends IInterceptorBroadcaster { @@ -90,4 +91,8 @@ public interface IInterceptorService extends IInterceptorBroadcaster { void registerInterceptors(@Nullable Collection theInterceptors); + /** + * Unregisters all interceptors that are indicated by the given callback function returning true + */ + void unregisterInterceptorsIf(Function theShouldUnregisterFunction); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index 634ccade76e..e569f7ca872 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -1321,6 +1321,43 @@ public enum Pointcut { "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" ), + /** + * Storage Hook: + * Invoked before an $expunge operation on all data (expungeEverything) is called. + *

+ * Hooks will be passed a reference to a counter containing the current number of records that have been deleted. + * If the hook deletes any records, the hook is expected to increment this counter by the number of records deleted. + *

+ * Hooks may accept the following parameters: + *
    + * org.hl7.fhir.instance.model.api.IBaseResource - The resource that will be created and needs a tenant ID assigned. + *
  • + * ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. Note that the bean + * properties are not all guaranteed to be populated, depending on how early during processing the + * exception occurred. + *
  • + *
  • + * ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will + * only be populated when operating in a RestfulServer implementation. It is provided as a convenience. + *
  • + *
+ *

+ * Hooks should return an instance of ca.uhn.fhir.jpa.model.entity.TenantId or null. + *

+ */ + STORAGE_TENANT_IDENTIFY_CREATE ( + // Return type + "ca.uhn.fhir.jpa.model.entity.TenantId", + // Params + "org.hl7.fhir.instance.model.api.IBaseResource", + "ca.uhn.fhir.rest.api.server.RequestDetails", + "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" + ), + /** * Performance Tracing Hook: * This hook is invoked when any informational messages generated by the diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java index 0c01735b266..25ff3fed920 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java @@ -41,6 +41,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Collectors; public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster { @@ -145,6 +146,22 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa } } + @Override + public void unregisterInterceptorsIf(Function theShouldUnregisterFunction) { + unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers); + unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers); + } + + private void unregisterInterceptorsIf(Function theShouldUnregisterFunction, ListMultimap theGlobalInvokers) { + for (Iterator> iter = theGlobalInvokers.entries().iterator(); iter.hasNext(); ) { + Map.Entry next = iter.next(); + Object nextInterceptor = next.getValue().getInterceptor(); + if (theShouldUnregisterFunction.apply(nextInterceptor)) { + iter.remove(); + } + } + } + @Override public boolean registerThreadLocalInterceptor(Object theInterceptor) { if (!myThreadlocalInvokersEnabled) { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml index fd6d640f6b3..a763784c8b7 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml @@ -4,7 +4,7 @@ title: "The version of a few dependencies have been bumped to the latest versions (dependent HAPI modules listed in brackets):
    -
  • Hibernate ORM (JPA): 5.4.6 -> 5.4.10
  • +
  • Hibernate ORM (JPA): 5.4.6.Final -> 5.4.11.Final
" - item: type: change diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 8a5bfe6f555..8b2a70ea717 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -390,6 +390,19 @@ public abstract class BaseHapiFhirResourceDao extends B ResourceTable entity = new ResourceTable(); entity.setResourceType(toResourceName(theResource)); + if (myDaoConfig.isMultiTenancyEnabled()) { + // Interceptor call: STORAGE_TENANT_IDENTIFY_CREATE + HookParams params = new HookParams() + .add(IBaseResource.class, theResource) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + TenantId tenantId = (TenantId) doCallHooksAndReturnObject(theRequest, Pointcut.STORAGE_TENANT_IDENTIFY_CREATE, params); + if (tenantId != null) { + ourLog.debug("Resource has been assigned tenant ID: {}", tenantId); + entity.setTenantId(tenantId); + } + } + if (isNotBlank(theIfNoneExist)) { Set match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theRequest); if (match.size() > 1) { @@ -432,7 +445,7 @@ public abstract class BaseHapiFhirResourceDao extends B notifyInterceptors(RestOperationTypeEnum.CREATE, requestDetails); } - // Notify JPA interceptors + // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED HookParams hookParams = new HookParams() .add(IBaseResource.class, theResource) .add(RequestDetails.class, theRequest) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java index b845ea07598..f996a9936cc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseStorageDao.java @@ -54,7 +54,6 @@ import java.util.Set; import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.OO_SEVERITY_ERROR; import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.OO_SEVERITY_INFO; import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseStorageDao { @@ -161,6 +160,10 @@ public abstract class BaseStorageDao { JpaInterceptorBroadcaster.doCallHooks(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams); } + protected Object doCallHooksAndReturnObject(RequestDetails theRequestDetails, Pointcut thePointcut, HookParams theParams) { + return JpaInterceptorBroadcaster.doCallHooksAndReturnObject(getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams); + } + protected abstract IInterceptorBroadcaster getInterceptorBroadcaster(); public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 6168d8fe5e4..2f5936b3c0e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -183,6 +183,11 @@ public class DaoConfig { */ private boolean myPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets; + /** + * @since 4.3.0 + */ + private boolean myMultiTenancyEnabled; + /** * Constructor */ @@ -1907,7 +1912,25 @@ public class DaoConfig { setPreExpandValueSetsDefaultCount(Math.min(getPreExpandValueSetsDefaultCount(), getPreExpandValueSetsMaxCount())); } - public enum StoreMetaSourceInformationEnum { + /** + * If enabled (default is false) the JPA server will support multitenant queries + * + * @since 4.3.0 + */ + public void setMultiTenancyEnabled(boolean theMultiTenancyEnabled) { + myMultiTenancyEnabled = theMultiTenancyEnabled; + } + + /** + * If enabled (default is false) the JPA server will support multitenant queries + * + * @since 4.3.0 + */ + public boolean isMultiTenancyEnabled() { + return myMultiTenancyEnabled; + } + + public enum StoreMetaSourceInformationEnum { NONE(false, false), SOURCE_URI(true, false), REQUEST_ID(false, true), diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java index a5b96de34b1..54a5e373e84 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/DaoSearchParamSynchronizer.java @@ -70,6 +70,7 @@ public class DaoSearchParamSynchronizer { theEntity.getParamsQuantity().remove(next); } for (T next : quantitiesToAdd) { + next.setTenantId(theEntity.getTenantId()); myEntityManager.merge(next); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java index bc68bfaef01..23597b1e688 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java @@ -1,84 +1,31 @@ package ca.uhn.fhir.jpa.dao.r4; -import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; +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.model.entity.ResourceEncodingEnum; -import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceTable; -import ca.uhn.fhir.jpa.model.entity.ResourceTag; -import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; +import ca.uhn.fhir.jpa.model.entity.TenantId; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.ReferenceParam; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; -import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; -import org.apache.commons.io.IOUtils; -import org.hamcrest.Matchers; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent; -import org.hl7.fhir.r4.model.Bundle.BundleEntryResponseComponent; -import org.hl7.fhir.r4.model.Bundle.BundleType; -import org.hl7.fhir.r4.model.Bundle.HTTPVerb; -import org.hl7.fhir.r4.model.Observation.ObservationStatus; -import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.r4.model.Patient; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionTemplate; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; +import javax.servlet.ServletException; import java.time.LocalDate; import java.time.Month; +import java.util.Collections; import java.util.Date; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.matchesPattern; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; public class MultitenantR4Test extends BaseJpaR4SystemTest { @@ -86,10 +33,17 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { @After public void after() { + myDaoConfig.setMultiTenancyEnabled(new DaoConfig().isMultiTenancyEnabled()); + + myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof MyInterceptor); } + @Override @Before - public void beforeDisableResultReuse() { + public void before() throws ServletException { + super.before(); + + myDaoConfig.setMultiTenancyEnabled(true); } @@ -100,28 +54,57 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { p.setBirthDate(new Date()); Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); - runInTransaction(()->{ - ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(() -> new IllegalArgumentException()); + runInTransaction(() -> { + ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); assertNull(resourceTable.getTenantId()); }); } @Test public void testCreateResourceWithTenant() { + int expectId = 3; + LocalDate expectDate = LocalDate.of(2020, Month.JANUARY, 14); + myInterceptorRegistry.registerInterceptor(new MyInterceptor(new TenantId(expectId, expectDate))); + Patient p = new Patient(); - p.setUserData(JpaConstants.USERDATA_TENANT_ID, 3); - p.setUserData(JpaConstants.USERDATA_TENANT_DATE, LocalDate.of(2020, Month.JANUARY, 14)); + p.addName().setFamily("FAM"); p.addIdentifier().setSystem("system").setValue("value"); p.setBirthDate(new Date()); Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); - runInTransaction(()->{ - ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(() -> new IllegalArgumentException()); - assertNull(resourceTable.getTenantId()); + runInTransaction(() -> { + ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); + assertEquals(expectId, resourceTable.getTenantId().getTenantId().intValue()); + assertEquals(expectDate, resourceTable.getTenantId().getTenantDate()); + + List strings = myResourceIndexedSearchParamStringDao.findAll(); + ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * "))); + assertEquals(10, strings.size()); + assertEquals(expectId, strings.get(0).getTenantId().getTenantId().intValue()); + assertEquals(expectDate, strings.get(0).getTenantId().getTenantDate()); }); } + @Interceptor + public static class MyInterceptor { + + private final List myTenantIds; + + public MyInterceptor(TenantId theTenantId) { + Validate.notNull(theTenantId); + myTenantIds = Collections.singletonList(theTenantId); + } + + @Hook(Pointcut.STORAGE_TENANT_IDENTIFY_CREATE) + public TenantId tenantIdentifyCreate() { + TenantId retVal = myTenantIds.get(0); + ourLog.info("Returning tenant ID: {}", retVal); + return retVal; + } + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java index df24e65b0b4..2f2d9b7024d 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndex.java @@ -20,10 +20,24 @@ package ca.uhn.fhir.jpa.model.entity; * #L% */ +import javax.persistence.Embedded; +import javax.persistence.MappedSuperclass; import java.io.Serializable; +@MappedSuperclass public abstract class BaseResourceIndex implements Serializable { + @Embedded + private TenantId myTenantId; + + public TenantId getTenantId() { + return myTenantId; + } + + public void setTenantId(TenantId theTenantId) { + myTenantId = theTenantId; + } + public abstract Long getId(); public abstract void setId(Long theId); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java index b88d63e6cb1..032db5adac8 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BaseResourceIndexedSearchParam.java @@ -80,17 +80,6 @@ public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex { @Temporal(TemporalType.TIMESTAMP) private Date myUpdated; - @Embedded - private TenantId myTenantId; - - public TenantId getTenantId() { - return myTenantId; - } - - public void setTenantId(TenantId theTenantId) { - myTenantId = theTenantId; - } - /** * Subclasses may override */ diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index cf6511f28ab..1fea6e588af 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -221,6 +221,13 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas @Transient private transient ResourceHistoryTable myCurrentVersionEntity; + /** + * Constructor + */ + public ResourceTable() { + super(); + } + @Override public ResourceTag addTag(TagDefinition theTag) { for (ResourceTag next : getTags()) { @@ -423,6 +430,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas return this; } + @Override public Collection getTags() { if (myTags == null) { myTags = new HashSet<>(); @@ -551,6 +559,7 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas retVal.setFhirVersion(getFhirVersion()); retVal.setDeleted(getDeleted()); retVal.setForcedId(getForcedId()); + retVal.setTenantId(getTenantId()); retVal.getTags().clear(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java index 3143e8559cf..555f98cd016 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java @@ -1,5 +1,8 @@ package ca.uhn.fhir.jpa.model.entity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + import javax.persistence.Column; import javax.persistence.Embeddable; import java.time.LocalDate; @@ -12,7 +15,22 @@ public class TenantId implements Cloneable { @Column(name = "TENANT_DATE", nullable = true) private LocalDate myTenantDate; - public Integer getTenantId() { + /** + * Constructor + */ + public TenantId() { + super(); + } + + /** + * Constructor + */ + public TenantId(int theTenantId, LocalDate theTenantDate) { + setTenantId(theTenantId); + setTenantDate(theTenantDate); + } + + public Integer getTenantId() { return myTenantId; } @@ -36,4 +54,12 @@ public class TenantId implements Cloneable { .setTenantId(getTenantId()) .setTenantDate(getTenantDate()); } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("id", myTenantId) + .append("date", myTenantDate) + .toString(); + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index d5a9a547828..98265649049 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -24,9 +24,6 @@ import ca.uhn.fhir.rest.api.Constants; public class JpaConstants { - public static final String USERDATA_TENANT_ID = JpaConstants.class.getName() + "_USERDATA_TENANT_ID"; - public static final String USERDATA_TENANT_DATE = JpaConstants.class.getName() + "_USERDATA_TENANT_DATE"; - /** * Operation name for the $apply-codesystem-delta-add operation */ diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index fdb1dccca7d..e66b4eb4e26 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor; import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.lang3.time.DateUtils; import org.hibernate.dialect.PostgreSQL94Dialect; import org.hl7.fhir.dstu2.model.Subscription; import org.springframework.beans.factory.annotation.Value; @@ -94,6 +95,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); + retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index eed61ff64af..e174228ffe1 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor; import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.lang3.time.DateUtils; import org.hibernate.dialect.PostgreSQL94Dialect; import org.hl7.fhir.dstu2.model.Subscription; import org.springframework.beans.factory.annotation.Autowire; @@ -101,6 +102,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); + retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index 6b836aab1b4..d290cf3c385 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor; import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.lang3.time.DateUtils; import org.hibernate.dialect.PostgreSQL94Dialect; import org.hl7.fhir.dstu2.model.Subscription; import org.springframework.beans.factory.annotation.Autowire; @@ -86,6 +87,7 @@ public class TestR4Config extends BaseJavaConfigR4 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); + retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); return retVal; } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java index c71bfd1259c..922e2087dad 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java @@ -10,6 +10,7 @@ import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhirtest.interceptor.PublicSecurityInterceptor; import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.lang3.time.DateUtils; import org.hibernate.dialect.PostgreSQL94Dialect; import org.hl7.fhir.dstu2.model.Subscription; import org.springframework.beans.factory.annotation.Autowire; @@ -86,6 +87,7 @@ public class TestR5Config extends BaseJavaConfigR5 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); + retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); return retVal; } diff --git a/pom.xml b/pom.xml index 2ef99fd2479..719bb34d765 100644 --- a/pom.xml +++ b/pom.xml @@ -645,7 +645,7 @@ 3.0.2 6.1.0 - 5.4.10.Final + 5.4.11.Final 5.11.3.Final 5.5.5 @@ -1326,7 +1326,7 @@ org.postgresql postgresql - 42.2.9 + 42.2.10 org.quartz-scheduler From 1d1aadb81300c2b84296bab1ca410185d9673c1f Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 9 Feb 2020 20:57:50 -0500 Subject: [PATCH 03/63] Work on multitenancy --- ...sourceIndexedCompositeStringUniqueDao.java | 8 +-- .../IResourceIndexedSearchParamStringDao.java | 10 +-- .../fhir/jpa/dao/data/IResourceLinkDao.java | 14 ++-- .../jpa/dao/data/ISearchParamPresentDao.java | 20 +++--- .../jpa/sp/SearchParamPresenceSvcImpl.java | 1 + .../fhir/jpa/dao/r4/MultitenantR4Test.java | 72 ++++++++++++++++++- .../ResourceIndexedCompositeStringUnique.java | 11 +++ .../jpa/model/entity/SearchParamPresent.java | 13 +++- .../uhn/fhir/jpa/model/entity/TenantId.java | 16 ++--- 9 files changed, 127 insertions(+), 38 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java index c1c2bf8d069..29ea1e54730 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedCompositeStringUniqueDao.java @@ -25,15 +25,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.Collection; -import java.util.Optional; +import java.util.List; public interface IResourceIndexedCompositeStringUniqueDao extends JpaRepository { @Query("SELECT r FROM ResourceIndexedCompositeStringUnique r WHERE r.myIndexString = :str") ResourceIndexedCompositeStringUnique findByQueryString(@Param("str") String theQueryString); - @Query("SELECT r.myResourceId FROM ResourceIndexedCompositeStringUnique r WHERE r.myIndexString IN :str") - Collection findResourcePidsByQueryStrings(@Param("str") Collection theQueryString); - + @Query("SELECT r FROM ResourceIndexedCompositeStringUnique r WHERE r.myResourceId = :resId") + List findAllForResourceId(@Param("resId") Long theResourceId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java index 32b1b10deca..00910347186 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamStringDao.java @@ -20,9 +20,8 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import org.springframework.data.jpa.repository.JpaRepository; - import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -32,6 +31,9 @@ import java.util.List; public interface IResourceIndexedSearchParamStringDao extends JpaRepository { @Modifying - @Query("delete from ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resid") - void deleteByResourceId(@Param("resid") Long theResourcePid); + @Query("DELETE FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resId") + void deleteByResourceId(@Param("resId") Long theResourcePid); + + @Query("SELECT t FROM ResourceIndexedSearchParamString t WHERE t.myResourcePid = :resId") + List findAllForResourceId(@Param("resId") Long thePatientId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index 51b9059a34d..d9cf4628516 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -20,16 +20,20 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import org.springframework.data.jpa.repository.JpaRepository; - import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface IResourceLinkDao extends JpaRepository { +import java.util.List; + +public interface IResourceLinkDao extends JpaRepository { @Modifying - @Query("delete from ResourceLink t WHERE t.mySourceResourcePid = :resid") - void deleteByResourceId(@Param("resid") Long theResourcePid); + @Query("DELETE FROM ResourceLink t WHERE t.mySourceResourcePid = :resId") + void deleteByResourceId(@Param("resId") Long theResourcePid); + + @Query("SELECT t FROM ResourceLink t WHERE t.mySourceResourcePid = :resId") + List findAllForResourceId(@Param("resId") Long thePatientId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java index d965b5e2f88..30b9ec80fb3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/ISearchParamPresentDao.java @@ -1,7 +1,13 @@ package ca.uhn.fhir.jpa.dao.data; -import java.util.Collection; -import java.util.Date; +import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; /* * #%L @@ -23,18 +29,10 @@ import java.util.Date; * #L% */ -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import ca.uhn.fhir.jpa.model.entity.ResourceTable; -import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; - public interface ISearchParamPresentDao extends JpaRepository { @Query("SELECT s FROM SearchParamPresent s WHERE s.myResource = :res") - Collection findAllForResource(@Param("res") ResourceTable theResource); + List findAllForResource(@Param("res") ResourceTable theResource); @Modifying @Query("delete from SearchParamPresent t WHERE t.myResourcePid = :resid") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java index 925808c3b34..d37f27819fb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java @@ -66,6 +66,7 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc { present.setResource(theResource); present.setParamName(paramName); present.setPresent(next.getValue()); + present.setTenantId(theResource.getTenantId()); present.calculateHashes(); newHashToPresence.put(present.getHashPresence(), present); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java index 23597b1e688..33b975a8947 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java @@ -4,13 +4,22 @@ 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.model.entity.ResourceHistoryTable; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; +import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceTable; +import ca.uhn.fhir.jpa.model.entity.SearchParamPresent; import ca.uhn.fhir.jpa.model.entity.TenantId; -import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.searchparam.SearchParamConstants; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.SearchParameter; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -19,6 +28,7 @@ import org.junit.Test; import javax.servlet.ServletException; import java.time.LocalDate; import java.time.Month; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; @@ -62,30 +72,88 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { @Test public void testCreateResourceWithTenant() { + createUniqueCompositeSp(); + int expectId = 3; LocalDate expectDate = LocalDate.of(2020, Month.JANUARY, 14); myInterceptorRegistry.registerInterceptor(new MyInterceptor(new TenantId(expectId, expectDate))); + Organization org = new Organization(); + org.setName("org"); + IIdType orgId = myOrganizationDao.create(org).getId().toUnqualifiedVersionless(); + Patient p = new Patient(); p.addName().setFamily("FAM"); p.addIdentifier().setSystem("system").setValue("value"); p.setBirthDate(new Date()); + p.getManagingOrganization().setReferenceElement(orgId); Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); runInTransaction(() -> { + // HFJ_RESOURCE ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); assertEquals(expectId, resourceTable.getTenantId().getTenantId().intValue()); assertEquals(expectDate, resourceTable.getTenantId().getTenantDate()); - List strings = myResourceIndexedSearchParamStringDao.findAll(); + // HFJ_RES_VER + ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, 1L); + assertEquals(expectId, version.getTenantId().getTenantId().intValue()); + assertEquals(expectDate, version.getTenantId().getTenantDate()); + + // HFJ_SPIDX_STRING + List strings = myResourceIndexedSearchParamStringDao.findAllForResourceId(patientId); ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * "))); assertEquals(10, strings.size()); assertEquals(expectId, strings.get(0).getTenantId().getTenantId().intValue()); assertEquals(expectDate, strings.get(0).getTenantId().getTenantDate()); + + // HFJ_RES_LINK + List resourceLinks = myResourceLinkDao.findAllForResourceId(patientId); + assertEquals(1, resourceLinks.size()); + assertEquals(expectId, resourceLinks.get(0).getTenantId().getTenantId().intValue()); + assertEquals(expectDate, resourceLinks.get(0).getTenantId().getTenantDate()); + + // HFJ_RES_PARAM_PRESENT + List presents = mySearchParamPresentDao.findAllForResource(resourceTable); + assertEquals(3, presents.size()); + assertEquals(expectId, presents.get(0).getTenantId().getTenantId().intValue()); + assertEquals(expectDate, presents.get(0).getTenantId().getTenantDate()); + + // HFJ_IDX_CMP_STRING_UNIQ + List uniques = myResourceIndexedCompositeStringUniqueDao.findAllForResourceId(patientId); + assertEquals(3, uniques.size()); + assertEquals(expectId, uniques.get(0).getTenantId().getTenantId().intValue()); + assertEquals(expectDate, uniques.get(0).getTenantId().getTenantDate()); }); } + private void createUniqueCompositeSp() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-birthdate"); + sp.setType(Enumerations.SearchParamType.DATE); + sp.setCode("birthdate"); + sp.setExpression("Patient.birthDate"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.addBase("Patient"); + mySearchParameterDao.update(sp); + + sp = new SearchParameter(); + sp.setId("SearchParameter/patient-birthdate"); + sp.setType(Enumerations.SearchParamType.COMPOSITE); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.addBase("Patient"); + sp.addComponent() + .setExpression("Patient") + .setDefinition("SearchParameter/patient-birthdate"); + sp.addExtension() + .setUrl(SearchParamConstants.EXT_SP_UNIQUE) + .setValue(new BooleanType(true)); + mySearchParameterDao.update(sp); + + mySearchParamRegistry.forceRefresh(); + } + @Interceptor public static class MyInterceptor { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java index 2c6305ebdd8..27870d6e608 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java @@ -48,6 +48,8 @@ public class ResourceIndexedCompositeStringUnique implements Comparable Date: Wed, 4 Mar 2020 13:49:36 -0500 Subject: [PATCH 04/63] Add columns --- .../fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java index 15dde30bba0..200585874e6 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java @@ -47,6 +47,9 @@ public class ResourceHistoryProvenanceEntity { private String mySourceUri; @Column(name = "REQUEST_ID", length = Constants.REQUEST_ID_LENGTH, nullable = true) private String myRequestId; + // FIXME: make sure this gets populated + @Embedded + private TenantId myTenantId; public ResourceTable getResourceTable() { return myResourceTable; From 9df4c5812279ad58502eb3f4b5d695f171cab6d0 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Fri, 27 Mar 2020 09:22:00 -0400 Subject: [PATCH 05/63] Work on multitenancy --- .../hapi/fhir/changelog/4_3_0/changes.yaml | 5 -- .../fhir/jpa/dao/r4/MultitenantR4Test.java | 80 ++++++++++++++----- .../tasks/HapiFhirJpaMigrationTasks.java | 7 +- .../fhir/jpa/model/entity/ResourceTable.java | 16 ---- .../fhir/jpa/model/entity/ResourceTag.java | 1 + 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml index d1ed402450b..08691d1826a 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/changes.yaml @@ -25,8 +25,3 @@ has been replaced with an equivalent `FhirContext.newFhirPath()`. The FhirPath expression language was initially called FluentPath before being renamed, so this change brings HAPI FHIR inline with the correct naming. " -- item: - type: change - title: "The JPA server now shared a single set of tags for all versions of a resource, bringing it in line with the - functional description in the FHIR specification. This means that it is no longer possible to modify the tags for - a specific version of a resource, and also causes significant performance improvements in some cases." diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java index 33b975a8947..f62bc29557e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java @@ -28,6 +28,7 @@ import org.junit.Test; import javax.servlet.ServletException; import java.time.LocalDate; import java.time.Month; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -36,16 +37,19 @@ import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; public class MultitenantR4Test extends BaseJpaR4SystemTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MultitenantR4Test.class); + private MyInterceptor myTenantInterceptor; @After public void after() { myDaoConfig.setMultiTenancyEnabled(new DaoConfig().isMultiTenancyEnabled()); myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof MyInterceptor); + myInterceptor = null; } @Override @@ -74,56 +78,84 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { public void testCreateResourceWithTenant() { createUniqueCompositeSp(); - int expectId = 3; - LocalDate expectDate = LocalDate.of(2020, Month.JANUARY, 14); - myInterceptorRegistry.registerInterceptor(new MyInterceptor(new TenantId(expectId, expectDate))); + addTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); + addTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); 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).getId().getIdPartAsLong(); + when(mySrd.getRequestId()).thenReturn("REQUEST_ID"); + Long patientId = myPatientDao.create(p, mySrd).getId().getIdPartAsLong(); runInTransaction(() -> { // HFJ_RESOURCE ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); - assertEquals(expectId, resourceTable.getTenantId().getTenantId().intValue()); - assertEquals(expectDate, resourceTable.getTenantId().getTenantDate()); + assertEquals(3, resourceTable.getTenantId().getTenantId().intValue()); + assertEquals(LocalDate.of(2020, Month.JANUARY, 14), resourceTable.getTenantId().getTenantDate()); + + resourceTable.getProfile() // HFJ_RES_VER ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, 1L); - assertEquals(expectId, version.getTenantId().getTenantId().intValue()); - assertEquals(expectDate, version.getTenantId().getTenantDate()); + assertEquals(3, version.getTenantId().getTenantId().intValue()); + assertEquals(LocalDate.of(2020, Month.JANUARY, 14), version.getTenantId().getTenantDate()); // HFJ_SPIDX_STRING List strings = myResourceIndexedSearchParamStringDao.findAllForResourceId(patientId); ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * "))); assertEquals(10, strings.size()); - assertEquals(expectId, strings.get(0).getTenantId().getTenantId().intValue()); - assertEquals(expectDate, strings.get(0).getTenantId().getTenantDate()); + assertEquals(3, strings.get(0).getTenantId().getTenantId().intValue()); + assertEquals(LocalDate.of(2020, Month.JANUARY, 14), strings.get(0).getTenantId().getTenantDate()); // HFJ_RES_LINK List resourceLinks = myResourceLinkDao.findAllForResourceId(patientId); assertEquals(1, resourceLinks.size()); - assertEquals(expectId, resourceLinks.get(0).getTenantId().getTenantId().intValue()); - assertEquals(expectDate, resourceLinks.get(0).getTenantId().getTenantDate()); + assertEquals(3, resourceLinks.get(0).getTenantId().getTenantId().intValue()); + assertEquals(LocalDate.of(2020, Month.JANUARY, 14), resourceLinks.get(0).getTenantId().getTenantDate()); // HFJ_RES_PARAM_PRESENT List presents = mySearchParamPresentDao.findAllForResource(resourceTable); assertEquals(3, presents.size()); - assertEquals(expectId, presents.get(0).getTenantId().getTenantId().intValue()); - assertEquals(expectDate, presents.get(0).getTenantId().getTenantDate()); + assertEquals(3, presents.get(0).getTenantId().getTenantId().intValue()); + assertEquals(LocalDate.of(2020, Month.JANUARY, 14), presents.get(0).getTenantId().getTenantDate()); // HFJ_IDX_CMP_STRING_UNIQ List uniques = myResourceIndexedCompositeStringUniqueDao.findAllForResourceId(patientId); assertEquals(3, uniques.size()); - assertEquals(expectId, uniques.get(0).getTenantId().getTenantId().intValue()); - assertEquals(expectDate, uniques.get(0).getTenantId().getTenantDate()); + assertEquals(3, uniques.get(0).getTenantId().getTenantId().intValue()); + assertEquals(LocalDate.of(2020, Month.JANUARY, 14), uniques.get(0).getTenantId().getTenantDate()); + }); + + } + + @Test + public void testUpdateResourceWithTenant() { + createUniqueCompositeSp(); + + addTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); + + Patient p = new Patient(); + p.setActive(true); + Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); + + p = new Patient(); + p.setId("Patient/" + patientId); + p.setActive(false); + myPatientDao.update(p); + + runInTransaction(() -> { + // HFJ_RES_VER + ResourceHistoryTable resVer = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, 2); + assertEquals(tenantId, resVer.getTenantId().getTenantId().intValue()); + assertEquals(tenantDate, resVer.getTenantId().getTenantDate()); + }); } @@ -154,19 +186,27 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { mySearchParamRegistry.forceRefresh(); } + public void addTenant(int theTenantId, LocalDate theTenantDate) { + if (myTenantInterceptor == null) { + myTenantInterceptor = new MyInterceptor(); + myInterceptorRegistry.registerInterceptor(myInterceptor); + } + myTenantInterceptor.addTenant(new TenantId(theTenantId, theTenantDate)); + } + @Interceptor public static class MyInterceptor { - private final List myTenantIds; + private final List myTenantIds = new ArrayList<>(); - public MyInterceptor(TenantId theTenantId) { + public void addTenant(TenantId theTenantId) { Validate.notNull(theTenantId); - myTenantIds = Collections.singletonList(theTenantId); + myTenantIds.add(theTenantId); } @Hook(Pointcut.STORAGE_TENANT_IDENTIFY_CREATE) public TenantId tenantIdentifyCreate() { - TenantId retVal = myTenantIds.get(0); + TenantId retVal = myTenantIds.remove(0); ourLog.info("Returning tenant ID: {}", retVal); return retVal; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 31a71c7b1f1..057c28aaafd 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -57,10 +57,10 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { init400(); // 20190401 - 20190814 init410(); // 20190815 - 20191014 init420(); // 20191015 - 20200217 - init430(); // 20200218 - present + init500(); // 20200218 - present } - protected void init430() { // 20200218 - present + protected void init500() { // 20200218 - present Builder version = forVersion(VersionEnum.V4_3_0); // Eliminate circular dependency. @@ -68,6 +68,9 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { version.onTable("HFJ_RES_VER").dropColumn("20200218.2", "FORCED_ID_PID"); version.onTable("HFJ_RES_VER").addForeignKey("20200218.3", "FK_RESOURCE_HISTORY_RESOURCE").toColumn("RES_ID").references("HFJ_RESOURCE", "RES_ID"); version.onTable("HFJ_RES_VER").modifyColumn("20200220.1", "RES_ID").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); + + // Add mlutiitenancy + version.onTable("HFJ_RESOURCE").dropColumn("20200327.1", "RES_PROFILE"); } protected void init420() { // 20191015 - 20200217 diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java index 731227e8af4..0f1ffb8f769 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTable.java @@ -60,7 +60,6 @@ import static org.apache.commons.lang3.StringUtils.defaultString; public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource, IResourceLookup { public static final int RESTYPE_LEN = 40; private static final int MAX_LANGUAGE_LENGTH = 20; - private static final int MAX_PROFILE_LENGTH = 200; private static final long serialVersionUID = 1L; /** @@ -167,10 +166,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas @OptimisticLock(excluded = true) private boolean myParamsUriPopulated; - @Column(name = "RES_PROFILE", length = MAX_PROFILE_LENGTH, nullable = true) - @OptimisticLock(excluded = true) - private String myProfile; - // Added in 3.0.0 - Should make this a primitive Boolean at some point @OptimisticLock(excluded = true) @Column(name = "SP_CMPSTR_UNIQ_PRESENT") @@ -402,17 +397,6 @@ public class ResourceTable extends BaseHasResource implements Serializable, IBas getParamsUri().addAll(theParamsUri); } - public String getProfile() { - return myProfile; - } - - public void setProfile(String theProfile) { - if (defaultString(theProfile).length() > MAX_PROFILE_LENGTH) { - throw new UnprocessableEntityException("Profile name exceeds maximum length of " + MAX_PROFILE_LENGTH + " chars: " + theProfile); - } - myProfile = theProfile; - } - @Override public Long getResourceId() { return getId(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java index 7d06439837c..a7e2cd1c950 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceTag.java @@ -50,6 +50,7 @@ public class ResourceTag extends BaseTag { @Column(name = "RES_ID", insertable = false, updatable = false) private Long myResourceId; + @Embedded private TenantId myTenantId; From 691f2c4e9a681780cf8c07af3d7fe003f3d2e8cf Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 29 Mar 2020 13:35:20 -0400 Subject: [PATCH 06/63] Work on muiltitenancy --- hapi-fhir-converter/pom.xml | 6 + ...83-fail-if-overriding-sps-is-disabled.yaml | 7 + .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 2 + .../FhirResourceDaoSearchParameterDstu2.java | 7 +- .../IResourceIndexedSearchParamDateDao.java | 8 +- .../FhirResourceDaoSearchParameterDstu3.java | 3 +- .../r4/FhirResourceDaoSearchParameterR4.java | 21 +- .../r5/FhirResourceDaoSearchParameterR5.java | 3 +- .../dao/r4/FhirResourceDaoR4CreateTest.java | 45 ++++- ...hirResourceDaoR4UniqueSearchParamTest.java | 21 +- .../fhir/jpa/dao/r4/MultitenantR4Test.java | 185 ++++++++++++++---- .../tasks/HapiFhirJpaMigrationTasks.java | 3 +- .../uhn/fhir/jpa/model/entity/ForcedId.java | 10 + .../ResourceHistoryProvenanceEntity.java | 19 +- .../ResourceIndexedCompositeStringUnique.java | 1 + .../ResourceIndexedSearchParamDate.java | 3 +- .../fhir/jpa/model/entity/ResourceTable.java | 1 - 17 files changed, 275 insertions(+), 70 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1783-fail-if-overriding-sps-is-disabled.yaml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 09e0e7df7c6..63dd461e26e 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -67,6 +67,12 @@ ${project.version} true + + ca.uhn.hapi.fhir + hapi-fhir-structures-r5 + ${project.version} + true + ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1783-fail-if-overriding-sps-is-disabled.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1783-fail-if-overriding-sps-is-disabled.yaml new file mode 100644 index 00000000000..3295f8fa849 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/4_3_0/1783-fail-if-overriding-sps-is-disabled.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 1783 +title: "In the JPA sevrer, if overriding built-in search parameters is not enabled, the server + will now return an error if a client tries to create a SearchParameter that is + trying to override one. Previously, the SearchParameter would be stored but silently ignored, + which was confusing." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 3a3ca82a8da..20d30f7b211 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -219,6 +219,7 @@ public abstract class BaseHapiFhirDao extends BaseStora retVal.setResourceType(theEntity.getResourceType()); retVal.setForcedId(theId.getIdPart()); retVal.setResource(theEntity); + retVal.setTenantId(theEntity.getTenantId()); theEntity.setForcedId(retVal); } } @@ -1094,6 +1095,7 @@ public abstract class BaseHapiFhirDao extends BaseStora ResourceHistoryProvenanceEntity provenance = new ResourceHistoryProvenanceEntity(); provenance.setResourceHistoryTable(historyEntry); provenance.setResourceTable(entity); + provenance.setTenantId(entity.getTenantId()); if (haveRequestId) { provenance.setRequestId(left(requestId, Constants.REQUEST_ID_LENGTH)); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java index da4cacaddd1..ed542a9d3a3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoSearchParameterDstu2.java @@ -24,8 +24,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoSearchParameterR4; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; -import ca.uhn.fhir.model.dstu2.composite.MetaDt; -import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.SearchParameter; import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; import ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum; @@ -38,8 +36,6 @@ import java.util.List; public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao implements IFhirResourceDaoSearchParameter { - @Autowired - private IFhirSystemDao mySystemDao; @Autowired private ISearchParamExtractor mySearchParamExtractor; @@ -79,8 +75,9 @@ public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao String expression = theResource.getXpath(); FhirContext context = getContext(); SearchParamTypeEnum type = theResource.getTypeElement().getValueAsEnum(); + String code = theResource.getCode(); - FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamExtractor, type, status, base, expression, context, getConfig()); + FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamRegistry, mySearchParamExtractor, code, type, status, base, expression, context, getConfig()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java index c37f62e6df7..457cd6515fb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceIndexedSearchParamDateDao.java @@ -20,15 +20,19 @@ package ca.uhn.fhir.jpa.dao.data; * #L% */ -import org.springframework.data.jpa.repository.JpaRepository; - import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; + public interface IResourceIndexedSearchParamDateDao extends JpaRepository { @Modifying @Query("delete from ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :resid") void deleteByResourceId(@Param("resid") Long theResourcePid); + + @Query("SELECT t FROM ResourceIndexedSearchParamDate t WHERE t.myResourcePid = :resId") + List findAllForResourceId(@Param("resId") Long thePatientId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java index 3041c843d8f..da2d908c356 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoSearchParameterDstu3.java @@ -70,8 +70,9 @@ public class FhirResourceDaoSearchParameterDstu3 extends BaseHapiFhirResourceDao String expression = theResource.getExpression(); FhirContext context = getContext(); Enumerations.SearchParamType type = theResource.getType(); + String code = theResource.getCode(); - FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamExtractor, type, status, base, expression, context, getConfig()); + FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamRegistry, mySearchParamExtractor, code, type, status, base, expression, context, getConfig()); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java index 8dab747cd37..71ac871635c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoSearchParameterR4.java @@ -2,11 +2,14 @@ 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.dao.BaseHapiFhirResourceDao; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.IFhirResourceDaoSearchParameter; 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; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.ElementUtil; @@ -78,14 +81,15 @@ public class FhirResourceDaoSearchParameterR4 extends BaseHapiFhirResourceDao status = theResource.getStatus(); List base = theResource.getBase(); + String code = theResource.getCode(); String expression = theResource.getExpression(); FhirContext context = getContext(); Enum type = theResource.getType(); - FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamExtractor, type, status, base, expression, context, getConfig()); + FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamRegistry, mySearchParamExtractor, code, type, status, base, expression, context, getConfig()); } - public static void validateSearchParam(ISearchParamExtractor theSearchParamExtractor, Enum theType, Enum theStatus, List theBase, String theExpression, FhirContext theContext, DaoConfig theDaoConfig) { + public static void validateSearchParam(ISearchParamRegistry theSearchParamRegistry, ISearchParamExtractor theSearchParamExtractor, String theCode, Enum theType, Enum theStatus, List theBase, String theExpression, FhirContext theContext, DaoConfig theDaoConfig) { if (theStatus == null) { throw new UnprocessableEntityException("SearchParameter.status is missing or invalid"); } @@ -146,6 +150,19 @@ public class FhirResourceDaoSearchParameterR4 extends BaseHapiFhirResourceDao nextBaseType : theBase) { + String nextBase = nextBaseType.getValueAsString(); + RuntimeSearchParam existingSearchParam = theSearchParamRegistry.getActiveSearchParam(nextBase, theCode); + if (existingSearchParam.getId() == null) { + throw new UnprocessableEntityException("Can not override built-in search parameter " + nextBase + ":" + theCode + " because overriding is disabled on this server"); + } + } + } + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoSearchParameterR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoSearchParameterR5.java index 2ff6366c092..cc6f8d02aa1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoSearchParameterR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoSearchParameterR5.java @@ -68,11 +68,12 @@ public class FhirResourceDaoSearchParameterR5 extends BaseHapiFhirResourceDao status = theResource.getStatus(); List base = theResource.getBase(); + String code = theResource.getCode(); String expression = theResource.getExpression(); FhirContext context = getContext(); Enum type = theResource.getType(); - FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamExtractor, type, status, base, expression, context, getConfig()); + FhirResourceDaoSearchParameterR4.validateSearchParam(mySearchParamRegistry, mySearchParamExtractor, code, type, status, base, expression, context, getConfig()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java index 56bec7d4391..b33feba256c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4CreateTest.java @@ -2,27 +2,39 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.param.QuantityParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.lang3.time.DateUtils; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.SampledData; +import org.hl7.fhir.r4.model.SearchParameter; import org.junit.After; import org.junit.AfterClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.PageRequest; import java.io.IOException; import java.util.Date; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { private static final Logger ourLog = LoggerFactory.getLogger(FhirResourceDaoR4CreateTest.class); @@ -31,6 +43,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { public void afterResetDao() { myDaoConfig.setResourceServerIdStrategy(new DaoConfig().getResourceServerIdStrategy()); myDaoConfig.setResourceClientIdStrategy(new DaoConfig().getResourceClientIdStrategy()); + myDaoConfig.setDefaultSearchParamsCanBeOverridden(new DaoConfig().isDefaultSearchParamsCanBeOverridden()); } @Test @@ -149,7 +162,7 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { p = new Patient(); p.setActive(false); try { - myPatientDao.create(p).getId(); + myPatientDao.create(p); fail(); } catch (ResourceVersionConflictException e) { // good @@ -280,6 +293,26 @@ public class FhirResourceDaoR4CreateTest extends BaseJpaR4Test { } + @Test + public void testOverrideBuiltInSearchParamFailsIfDisabled() { + myModelConfig.setDefaultSearchParamsCanBeOverridden(false); + + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/patient-birthdate"); + sp.setType(Enumerations.SearchParamType.DATE); + sp.setCode("birthdate"); + sp.setExpression("Patient.birthDate"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.addBase("Patient"); + try { + mySearchParameterDao.update(sp); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("Can not override built-in search parameter Patient:birthdate because overriding is disabled on this server", e.getMessage()); + } + + } + @AfterClass public static void afterClassClearContext() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java index 34f8f6c6d4b..d1aa5668a33 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -22,7 +22,6 @@ import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; -import ca.uhn.fhir.test.utilities.UnregisterScheduledProcessor; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.*; @@ -32,10 +31,7 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatchers; -import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.util.ProxyUtils; -import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; @@ -48,8 +44,17 @@ import java.util.UUID; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.dao.BaseHapiFhirDao.INDEX_STATUS_INDEXED; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.either; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -695,7 +700,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { assertEquals(1, myResourceReindexingSvc.forceReindexingPass()); assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); - runInTransaction(()->{ + runInTransaction(() -> { List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(uniques.toString(), 1, uniques.size()); assertThat(uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue(), either(equalTo("Observation/" + id2.getIdPart())).or(equalTo("Observation/" + id3.getIdPart()))); @@ -712,7 +717,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { myResourceReindexingSvc.forceReindexingPass(); assertEquals(0, myResourceReindexingSvc.forceReindexingPass()); - runInTransaction(()->{ + runInTransaction(() -> { List uniques = myResourceIndexedCompositeStringUniqueDao.findAll(); assertEquals(uniques.toString(), 1, uniques.size()); assertThat(uniques.get(0).getResource().getIdDt().toUnqualifiedVersionless().getValue(), either(equalTo("Observation/" + id2.getIdPart())).or(equalTo("Observation/" + id3.getIdPart()))); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java index f62bc29557e..d77a5e755d1 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java @@ -4,8 +4,10 @@ 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.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedCompositeStringUnique; +import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.jpa.model.entity.ResourceTable; @@ -17,6 +19,7 @@ import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.SearchParameter; @@ -29,13 +32,13 @@ import javax.servlet.ServletException; import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.function.Consumer; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.when; @@ -43,6 +46,8 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MultitenantR4Test.class); private MyInterceptor myTenantInterceptor; + private LocalDate myTenantDate; + private int myTenantId; @After public void after() { @@ -58,6 +63,11 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { super.before(); myDaoConfig.setMultiTenancyEnabled(true); + myDaoConfig.setUniqueIndexesEnabled(true); + myModelConfig.setDefaultSearchParamsCanBeOverridden(true); + + myTenantDate = LocalDate.of(2020, Month.JANUARY, 14); + myTenantId = 3; } @@ -74,12 +84,14 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { }); } + @Test public void testCreateResourceWithTenant() { createUniqueCompositeSp(); + createRequestId(); - addTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); - addTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); + addCreateTenant(myTenantId, myTenantDate); + addCreateTenant(myTenantId, myTenantDate); Organization org = new Organization(); org.setName("org"); @@ -91,75 +103,155 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { p.addIdentifier().setSystem("system").setValue("value"); p.setBirthDate(new Date()); p.getManagingOrganization().setReferenceElement(orgId); - when(mySrd.getRequestId()).thenReturn("REQUEST_ID"); Long patientId = myPatientDao.create(p, mySrd).getId().getIdPartAsLong(); runInTransaction(() -> { // HFJ_RESOURCE ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); - assertEquals(3, resourceTable.getTenantId().getTenantId().intValue()); - assertEquals(LocalDate.of(2020, Month.JANUARY, 14), resourceTable.getTenantId().getTenantDate()); - - resourceTable.getProfile() + assertEquals(myTenantId, resourceTable.getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, resourceTable.getTenantId().getTenantDate()); // HFJ_RES_VER ResourceHistoryTable version = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, 1L); - assertEquals(3, version.getTenantId().getTenantId().intValue()); - assertEquals(LocalDate.of(2020, Month.JANUARY, 14), version.getTenantId().getTenantDate()); + assertEquals(myTenantId, version.getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, version.getTenantId().getTenantDate()); + + // HFJ_RES_VER_PROV + assertNotNull(version.getProvenance()); + assertEquals(myTenantId, version.getProvenance().getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, version.getProvenance().getTenantId().getTenantDate()); // HFJ_SPIDX_STRING List strings = myResourceIndexedSearchParamStringDao.findAllForResourceId(patientId); ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * "))); assertEquals(10, strings.size()); - assertEquals(3, strings.get(0).getTenantId().getTenantId().intValue()); - assertEquals(LocalDate.of(2020, Month.JANUARY, 14), strings.get(0).getTenantId().getTenantDate()); + assertEquals(myTenantId, strings.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, strings.get(0).getTenantId().getTenantDate()); + + // HFJ_SPIDX_DATE + List dates = myResourceIndexedSearchParamDateDao.findAllForResourceId(patientId); + ourLog.info("\n * {}", dates.stream().map(ResourceIndexedSearchParamDate::toString).collect(Collectors.joining("\n * "))); + assertEquals(2, dates.size()); + assertEquals(myTenantId, dates.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, dates.get(0).getTenantId().getTenantDate()); + assertEquals(myTenantId, dates.get(1).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, dates.get(1).getTenantId().getTenantDate()); // HFJ_RES_LINK List resourceLinks = myResourceLinkDao.findAllForResourceId(patientId); assertEquals(1, resourceLinks.size()); - assertEquals(3, resourceLinks.get(0).getTenantId().getTenantId().intValue()); - assertEquals(LocalDate.of(2020, Month.JANUARY, 14), resourceLinks.get(0).getTenantId().getTenantDate()); + assertEquals(myTenantId, resourceLinks.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, resourceLinks.get(0).getTenantId().getTenantDate()); // HFJ_RES_PARAM_PRESENT List presents = mySearchParamPresentDao.findAllForResource(resourceTable); - assertEquals(3, presents.size()); - assertEquals(3, presents.get(0).getTenantId().getTenantId().intValue()); - assertEquals(LocalDate.of(2020, Month.JANUARY, 14), presents.get(0).getTenantId().getTenantDate()); + assertEquals(myTenantId, presents.size()); + assertEquals(myTenantId, presents.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, presents.get(0).getTenantId().getTenantDate()); // HFJ_IDX_CMP_STRING_UNIQ List uniques = myResourceIndexedCompositeStringUniqueDao.findAllForResourceId(patientId); - assertEquals(3, uniques.size()); - assertEquals(3, uniques.get(0).getTenantId().getTenantId().intValue()); - assertEquals(LocalDate.of(2020, Month.JANUARY, 14), uniques.get(0).getTenantId().getTenantDate()); + assertEquals(1, uniques.size()); + assertEquals(myTenantId, uniques.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, uniques.get(0).getTenantId().getTenantDate()); + }); + + } + + @Test + public void testCreateWithForcedId() { + addCreateTenant(myTenantId, myTenantDate); + addCreateTenant(myTenantId, myTenantDate); + + 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 forcedIds = myForcedIdDao.findAll(); + assertEquals(2, forcedIds.size()); + assertEquals(myTenantId, forcedIds.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, forcedIds.get(0).getTenantId().getTenantDate()); + assertEquals(myTenantId, forcedIds.get(1).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, forcedIds.get(1).getTenantId().getTenantDate()); }); } @Test public void testUpdateResourceWithTenant() { - createUniqueCompositeSp(); - - addTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); + createRequestId(); + addCreateTenant(3, LocalDate.of(2020, Month.JANUARY, 14)); + // Create a resource Patient p = new Patient(); p.setActive(true); Long patientId = myPatientDao.create(p).getId().getIdPartAsLong(); + runInTransaction(() -> { + // HFJ_RESOURCE + ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); + assertEquals(myTenantId, resourceTable.getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, resourceTable.getTenantId().getTenantDate()); + }); + // Update that resource p = new Patient(); p.setId("Patient/" + patientId); p.setActive(false); - myPatientDao.update(p); + myPatientDao.update(p, mySrd); runInTransaction(() -> { + // HFJ_RESOURCE + ResourceTable resourceTable = myResourceTableDao.findById(patientId).orElseThrow(IllegalArgumentException::new); + assertEquals(myTenantId, resourceTable.getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, resourceTable.getTenantId().getTenantDate()); + // HFJ_RES_VER - ResourceHistoryTable resVer = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, 2); - assertEquals(tenantId, resVer.getTenantId().getTenantId().intValue()); - assertEquals(tenantDate, resVer.getTenantId().getTenantDate()); + int version = 2; + ResourceHistoryTable resVer = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(patientId, version); + assertEquals(myTenantId, resVer.getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, resVer.getTenantId().getTenantDate()); + + // HFJ_RES_VER_PROV + assertNotNull(resVer.getProvenance()); + assertNotNull(resVer.getTenantId()); + assertEquals(myTenantId, resVer.getProvenance().getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, resVer.getProvenance().getTenantId().getTenantDate()); + + // HFJ_SPIDX_STRING + List strings = myResourceIndexedSearchParamStringDao.findAllForResourceId(patientId); + ourLog.info("\n * {}", strings.stream().map(ResourceIndexedSearchParamString::toString).collect(Collectors.joining("\n * "))); + assertEquals(10, strings.size()); + assertEquals(myTenantId, strings.get(0).getTenantId().getTenantId().intValue()); + assertEquals(myTenantDate, strings.get(0).getTenantId().getTenantDate()); }); } + @Test + public void testReadAcrossTenants() { + IIdType patientId1 = createPatient(1, withActiveTrue()); + IIdType patientId2 = createPatient(1, withActiveTrue()); + + IdType gotId1 = myPatientDao.read(patientId1, mySrd).getIdElement().toUnqualifiedVersionless(); + assertEquals(patientId1, gotId1); + IdType gotId2 = myPatientDao.read(patientId2, mySrd).getIdElement().toUnqualifiedVersionless(); + assertEquals(patientId2, gotId2); + } + + @Test + public void testSearchAcrossAllTenants() { + + } + private void createUniqueCompositeSp() { SearchParameter sp = new SearchParameter(); sp.setId("SearchParameter/patient-birthdate"); @@ -171,7 +263,7 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { mySearchParameterDao.update(sp); sp = new SearchParameter(); - sp.setId("SearchParameter/patient-birthdate"); + sp.setId("SearchParameter/patient-birthdate-unique"); sp.setType(Enumerations.SearchParamType.COMPOSITE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.addBase("Patient"); @@ -186,27 +278,48 @@ public class MultitenantR4Test extends BaseJpaR4SystemTest { mySearchParamRegistry.forceRefresh(); } - public void addTenant(int theTenantId, LocalDate theTenantDate) { + + + private void addCreateTenant(int theTenantId, LocalDate theTenantDate) { if (myTenantInterceptor == null) { myTenantInterceptor = new MyInterceptor(); - myInterceptorRegistry.registerInterceptor(myInterceptor); + myInterceptorRegistry.registerInterceptor(myTenantInterceptor); } - myTenantInterceptor.addTenant(new TenantId(theTenantId, theTenantDate)); + myTenantInterceptor.addCreateTenant(new TenantId(theTenantId, theTenantDate)); + } + + public IIdType createPatient(int theTenantId, Consumer... theModifiers) { + addCreateTenant(theTenantId, null); + Patient p = new Patient(); + for (Consumer next : theModifiers) { + next.accept(p); + } + + return myPatientDao.create(p).getId().toUnqualifiedVersionless(); + } + + public void createRequestId() { + when(mySrd.getRequestId()).thenReturn("REQUEST_ID"); + } + + private Consumer withActiveTrue() { + return t->t.setActive(true); } @Interceptor public static class MyInterceptor { - private final List myTenantIds = new ArrayList<>(); - public void addTenant(TenantId theTenantId) { + private final List myCreateTenantIds = new ArrayList<>(); + + public void addCreateTenant(TenantId theTenantId) { Validate.notNull(theTenantId); - myTenantIds.add(theTenantId); + myCreateTenantIds.add(theTenantId); } @Hook(Pointcut.STORAGE_TENANT_IDENTIFY_CREATE) public TenantId tenantIdentifyCreate() { - TenantId retVal = myTenantIds.remove(0); + TenantId retVal = myCreateTenantIds.remove(0); ourLog.info("Returning tenant ID: {}", retVal); return retVal; } diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 057c28aaafd..814f36c553c 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -70,7 +70,8 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { version.onTable("HFJ_RES_VER").modifyColumn("20200220.1", "RES_ID").nonNullable().failureAllowed().withType(BaseTableColumnTypeTask.ColumnTypeEnum.LONG); // Add mlutiitenancy - version.onTable("HFJ_RESOURCE").dropColumn("20200327.1", "RES_PROFILE"); + version.onTable("HFJ_RESOURCE").dropIndex("20200327.1", "IDX_RES_PROFILE"); + version.onTable("HFJ_RESOURCE").dropColumn("20200327.2", "RES_PROFILE"); } protected void init420() { // 20191015 - 20200217 diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java index 720163bec12..7807d918ecb 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ForcedId.java @@ -62,6 +62,8 @@ public class ForcedId { @ColumnDefault("''") @Column(name = "RESOURCE_TYPE", nullable = true, length = 100, updatable = true) private String myResourceType; + @Embedded + private TenantId myTenantId; /** * Constructor @@ -93,4 +95,12 @@ public class ForcedId { public Long getId() { return myId; } + + public TenantId getTenantId() { + return myTenantId; + } + + public void setTenantId(TenantId theTenantId) { + myTenantId = theTenantId; + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java index 200585874e6..b286d68990e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceHistoryProvenanceEntity.java @@ -51,18 +51,17 @@ public class ResourceHistoryProvenanceEntity { @Embedded private TenantId myTenantId; - public ResourceTable getResourceTable() { - return myResourceTable; + /** + * Constructor + */ + public ResourceHistoryProvenanceEntity() { + super(); } public void setResourceTable(ResourceTable theResourceTable) { myResourceTable = theResourceTable; } - public ResourceHistoryTable getResourceHistoryTable() { - return myResourceHistoryTable; - } - public void setResourceHistoryTable(ResourceHistoryTable theResourceHistoryTable) { myResourceHistoryTable = theResourceHistoryTable; } @@ -86,4 +85,12 @@ public class ResourceHistoryProvenanceEntity { public Long getId() { return myId; } + + public TenantId getTenantId() { + return myTenantId; + } + + public void setTenantId(TenantId theTenantId) { + myTenantId = theTenantId; + } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java index 27870d6e608..ee7aafa2fc2 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceIndexedCompositeStringUnique.java @@ -64,6 +64,7 @@ public class ResourceIndexedCompositeStringUnique implements Comparable Date: Sun, 29 Mar 2020 18:16:04 -0400 Subject: [PATCH 07/63] Work on multitenancy --- .../ca/uhn/fhir/interceptor/api/Pointcut.java | 70 ++- .../ca/uhn/fhir/i18n/hapi-messages.properties | 3 + .../ca/uhn/hapi/fhir/atlas/points.json | 2 + .../hapi/fhir/changelog/4_3_0/changes.yaml | 1 + .../ca/uhn/hapi/fhir/docs/files.properties | 1 + .../hapi/fhir/docs/server_jpa/partitioning.md | 18 + .../fhir/jpa/bulk/BulkDataExportSvcImpl.java | 2 +- .../ca/uhn/fhir/jpa/config/BaseConfig.java | 6 + .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 4 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 131 +++-- .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 14 +- .../dao/FhirResourceDaoSubscriptionDstu2.java | 2 +- .../fhir/jpa/dao/FulltextSearchSvcImpl.java | 10 +- .../ca/uhn/fhir/jpa/dao/ISearchBuilder.java | 7 +- .../ca/uhn/fhir/jpa/dao/SearchBuilder.java | 22 +- .../uhn/fhir/jpa/dao/data/IForcedIdDao.java | 3 + .../FhirResourceDaoSubscriptionDstu3.java | 2 +- .../dao/index/DaoSearchParamSynchronizer.java | 2 +- .../fhir/jpa/dao/index/IdHelperService.java | 31 +- .../RequestPartitionHelperService.java | 101 ++++ .../dao/predicate/BasePredicateBuilder.java | 7 +- .../jpa/dao/predicate/IPredicateBuilder.java | 4 +- .../jpa/dao/predicate/PredicateBuilder.java | 16 +- .../predicate/PredicateBuilderReference.java | 65 ++- .../predicate/PredicateBuilderResourceId.java | 10 +- .../dao/predicate/PredicateBuilderUri.java | 11 +- .../dao/r4/FhirResourceDaoSubscriptionR4.java | 2 +- .../dao/r5/FhirResourceDaoSubscriptionR5.java | 2 +- .../jpa/search/ISearchCoordinatorSvc.java | 2 +- .../jpa/search/SearchCoordinatorSvcImpl.java | 42 +- .../jpa/sp/SearchParamPresenceSvcImpl.java | 2 +- .../term/TermCodeSystemStorageSvcImpl.java | 6 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 2 + .../fhir/jpa/dao/r4/MultitenantR4Test.java | 334 ------------ .../fhir/jpa/dao/r4/PartitioningR4Test.java | 499 ++++++++++++++++++ .../search/SearchCoordinatorSvcImplTest.java | 17 +- .../jpa/model/entity/BaseHasResource.java | 17 +- .../jpa/model/entity/BaseResourceIndex.java | 18 +- .../ca/uhn/fhir/jpa/model/entity/BaseTag.java | 12 + .../uhn/fhir/jpa/model/entity/ForcedId.java | 11 +- .../fhir/jpa/model/entity/PartitionId.java | 66 +++ .../ResourceHistoryProvenanceEntity.java | 10 +- .../model/entity/ResourceHistoryTable.java | 4 +- .../jpa/model/entity/ResourceHistoryTag.java | 3 +- .../ResourceIndexedCompositeStringUnique.java | 14 +- .../fhir/jpa/model/entity/ResourceTable.java | 4 +- .../fhir/jpa/model/entity/ResourceTag.java | 10 +- .../jpa/model/entity/SearchParamPresent.java | 12 +- .../uhn/fhir/jpa/model/entity/TenantId.java | 61 --- pom.xml | 2 +- 50 files changed, 1085 insertions(+), 612 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa/partitioning.md create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/partition/RequestPartitionHelperService.java delete mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/MultitenantR4Test.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningR4Test.java create mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PartitionId.java delete mode 100644 hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/TenantId.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index e569f7ca872..8a5d1a6f034 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -27,7 +27,11 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import javax.annotation.Nonnull; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * Value for {@link Hook#value()} @@ -374,7 +378,6 @@ public enum Pointcut { ), - /** * Server Hook: * This method is called after the server implementation method has been called, but before any attempt @@ -1323,14 +1326,16 @@ public enum Pointcut { /** * Storage Hook: - * Invoked before an $expunge operation on all data (expungeEverything) is called. + * Invoked before FHIR create operation to request the identification of the partition ID to be associated + * with the resource being created. This hook will only be called if partitioning is enabled in the JPA + * server. *

- * Hooks will be passed a reference to a counter containing the current number of records that have been deleted. - * If the hook deletes any records, the hook is expected to increment this counter by the number of records deleted. - *

* Hooks may accept the following parameters: + *

*
    + *
  • * org.hl7.fhir.instance.model.api.IBaseResource - The resource that will be created and needs a tenant ID assigned. + *
  • *
  • * ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been @@ -1346,18 +1351,53 @@ public enum Pointcut { *
  • *
*

- * Hooks should return an instance of ca.uhn.fhir.jpa.model.entity.TenantId or null. + * Hooks should return an instance of ca.uhn.fhir.jpa.model.entity.PartitionId or null. *

*/ - STORAGE_TENANT_IDENTIFY_CREATE ( + STORAGE_PARTITION_IDENTIFY_CREATE( // Return type - "ca.uhn.fhir.jpa.model.entity.TenantId", + "ca.uhn.fhir.jpa.model.entity.PartitionId", // Params "org.hl7.fhir.instance.model.api.IBaseResource", "ca.uhn.fhir.rest.api.server.RequestDetails", "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" ), + /** + * Storage Hook: + * Invoked before FHIR read/access operation (e.g. read/vread, search, history, etc.) operation to request the + * identification of the partition ID to be associated with the resource being created. This hook will only be called if + * partitioning is enabled in the JPA server. + *

+ * Hooks may accept the following parameters: + *

+ *
    + *
  • + * ca.uhn.fhir.rest.api.server.RequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. Note that the bean + * properties are not all guaranteed to be populated, depending on how early during processing the + * exception occurred. + *
  • + *
  • + * ca.uhn.fhir.rest.server.servlet.ServletRequestDetails - A bean containing details about the request that is about to be processed, including details such as the + * resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been + * pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will + * only be populated when operating in a RestfulServer implementation. It is provided as a convenience. + *
  • + *
+ *

+ * Hooks should return an instance of ca.uhn.fhir.jpa.model.entity.PartitionId or null. + *

+ */ + STORAGE_PARTITION_IDENTIFY_READ( + // Return type + "ca.uhn.fhir.jpa.model.entity.PartitionId", + // Params + "ca.uhn.fhir.rest.api.server.RequestDetails", + "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" + ), + /** * Performance Tracing Hook: * This hook is invoked when any informational messages generated by the @@ -1680,12 +1720,12 @@ public enum Pointcut { *

*

* THIS IS AN EXPERIMENTAL HOOK AND MAY BE REMOVED OR CHANGED WITHOUT WARNING. - *

- *

+ *

+ *

* Note that this is a performance tracing hook. Use with caution in production * systems, since calling it may (or may not) carry a cost. - *

- *

+ *

+ *

* Hooks may accept the following parameters: *

*