Finish docs
This commit is contained in:
parent
c412339a05
commit
7248b4e18e
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.interceptor.api;
|
|||
*/
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
|
|
@ -157,4 +157,4 @@ ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantCreateDuplicatePartitionNam
|
|||
ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantDeleteDefaultPartition=Can not delete default partition
|
||||
ca.uhn.fhir.jpa.partition.PartitionConfigSvcImpl.cantRenameDefaultPartition=Can not rename default partition
|
||||
|
||||
ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0}
|
||||
ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor.unknownTenantName=Unknown tenant: {0}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
package ca.uhn.hapi.fhir.docs;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Docs
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 University Health Network
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
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.interceptor.model.PartitionId;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
@SuppressWarnings("InnerClassMayBeStatic")
|
||||
public class PartitionExamples {
|
||||
|
||||
public void multitenantServer() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// START SNIPPET: partitionInterceptorRequestPartition
|
||||
@Interceptor
|
||||
public class RequestTenantPartitionInterceptor {
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
|
||||
public PartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) {
|
||||
return extractPartitionIdFromRequest(theRequestDetails);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
|
||||
public PartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) {
|
||||
return extractPartitionIdFromRequest(theRequestDetails);
|
||||
}
|
||||
|
||||
private PartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) {
|
||||
// We will use the tenant ID that came from the request as the partition name
|
||||
String tenantId = theRequestDetails.getTenantId();
|
||||
return PartitionId.forPartitionName(tenantId);
|
||||
}
|
||||
|
||||
}
|
||||
// END SNIPPET: partitionInterceptorRequestPartition
|
||||
|
||||
|
||||
// START SNIPPET: partitionInterceptorHeaders
|
||||
@Interceptor
|
||||
public class CustomHeaderBasedPartitionInterceptor {
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
|
||||
public PartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) {
|
||||
String partitionName = theRequestDetails.getHeader("X-Partition-Name");
|
||||
return PartitionId.forPartitionName(partitionName);
|
||||
}
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
|
||||
public PartitionId PartitionIdentifyRead(ServletRequestDetails theRequestDetails) {
|
||||
String partitionName = theRequestDetails.getHeader("X-Partition-Name");
|
||||
return PartitionId.forPartitionName(partitionName);
|
||||
}
|
||||
|
||||
}
|
||||
// END SNIPPET: partitionInterceptorHeaders
|
||||
|
||||
|
||||
// START SNIPPET: partitionInterceptorResourceContents
|
||||
@Interceptor
|
||||
public class ResourceTypePartitionInterceptor {
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
|
||||
public PartitionId PartitionIdentifyCreate(IBaseResource theResource) {
|
||||
if (theResource instanceof Patient) {
|
||||
return PartitionId.forPartitionName("PATIENT");
|
||||
} else if (theResource instanceof Observation) {
|
||||
return PartitionId.forPartitionName("OBSERVATION");
|
||||
} else {
|
||||
return PartitionId.forPartitionName("OTHER");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// END SNIPPET: partitionInterceptorResourceContents
|
||||
|
||||
|
||||
// START SNIPPET: multitenantServer
|
||||
public class MultitenantServer extends RestfulServer {
|
||||
|
||||
@Autowired
|
||||
private PartitionConfig myPartitionConfig;
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
|
||||
// Enable partitioning
|
||||
myPartitionConfig.setPartitioningEnabled(true);
|
||||
|
||||
// Set the tenant identification strategy
|
||||
setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
|
||||
|
||||
// Use the tenant ID supplied by the tenant identification strategy
|
||||
// to serve as the partitioning ID
|
||||
registerInterceptor(new RequestTenantPartitionInterceptor());
|
||||
|
||||
// ....Register some providers and other things....
|
||||
|
||||
}
|
||||
}
|
||||
// END SNIPPET: multitenantServer
|
||||
|
||||
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 61 KiB |
|
@ -24,6 +24,16 @@ This interceptor will then produce output similar to the following:
|
|||
2014-09-04 03:30:00.443 Source[127.0.0.1] Operation[search-type Organization] UA[Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)] Params[]
|
||||
```
|
||||
|
||||
<a name="request-tenant-partition-interceptor"/>
|
||||
|
||||
# Partitioning: Multitenant Request Partition
|
||||
|
||||
If the JPA server has [partitioning](/docs/server_jpa/partitioning.html) enabled, the RequestTenantPartitionInterceptor can be used in combination with a [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) in order to achieve a multitenant solution. See [JPA Server Partitioning](/docs/server_jpa/partitioning.html) for more information on partitioning.
|
||||
|
||||
* [RequestTenantPartitionInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.html)
|
||||
* [RequestTenantPartitionInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/partition/RequestTenantPartitionInterceptor.java)
|
||||
|
||||
|
||||
# Response Customizing: Syntax Highlighting
|
||||
|
||||
The ResponseHighlighterInterceptor detects when a request is coming from a browser and returns HTML with syntax highlighted XML/JSON instead of just the raw text. In other words, if a user uses a browser to request `http://foo/Patient/1` by typing this address into their URL bar, they will get a nicely formatted HTML back with a human readable version of the content. This is particularly helpful for testers and public/development APIs where users are likely to invoke the API directly to see how it works.
|
||||
|
@ -150,8 +160,8 @@ Some security audit tools require that servers return an HTTP 405 if an unsuppor
|
|||
|
||||
When using Subscriptions, the debug log interceptor can be used to add a number of additional lines to the server logs showing the internals of the subscription processing pipeline.
|
||||
|
||||
* [SubscriptionDebugLogInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.html)
|
||||
* [SubscriptionDebugLogInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master//hapi-fhir-jpaserver-base/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java)
|
||||
* [SubscriptionDebugLogInterceptor JavaDoc](/apidocs/hapi-fhir-jpaserver-subscription/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.html)
|
||||
* [SubscriptionDebugLogInterceptor Source](https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java)
|
||||
|
||||
|
||||
# Request Pre-Processing: Override Meta.source
|
||||
|
|
|
@ -21,13 +21,13 @@ Partitioning involves the use of two dedicated columns to many tables within the
|
|||
* **PARTITION_ID** – This is an integer indicating the specific partition that a given resource is placed in. This column can also be *NULL*, meaning that the given resource is in the **Default Partition**.
|
||||
* **PARTITION_DATE** – This is a date/time column that can be assigned an arbitrary value depending on your use case. Typically, this would be used for use cases where data should be automatically dropped after a certain time period using native database partition drops.
|
||||
|
||||
When partitioning is used, these two columns will be populated with the same value on all resource-specific tables (this includes [HFJ_RESOURCE](./schema.html#HFJ_RESOURCE) and all tables that have a foreign key relationship to it including [HFJ_RES_VER](./schema.html#HFJ_RES_VER), [HFJ_RESLINK](./schema.html#HFJ_RES_LINK), [HFJ_SPIDX_*](./schema.html#indexes), etc.)
|
||||
When partitioning is used, these two columns will be populated with the same value on all resource-specific tables (this includes [HFJ_RESOURCE](./schema.html#HFJ_RESOURCE) and all tables that have a foreign key relationship to it including [HFJ_RES_VER](./schema.html#HFJ_RES_VER), [HFJ_RESLINK](./schema.html#HFJ_RES_LINK), [HFJ_SPIDX_*](./schema.html#search-indexes), etc.)
|
||||
|
||||
At the time that a resource is being **created**, an [interceptor hook](#partition-iInterceptors) is invoked in order to request the partition ID and date, and these will be written to the resource.
|
||||
At the time that a resource is being **created**, an [interceptor hook](#partition-interceptors) is invoked in order to request the partition ID and date, and these will be written to the resource.
|
||||
|
||||
At the time that a resource is being **updated**, the partition ID and date from the previous version will be used.
|
||||
|
||||
When a **read operation** is being performed (e.g. a read, search, history, etc.), a separate [interceptor hook](#partition-iInterceptors) is invoked in order to determine whether the operation should target a specific partition. The outcome of this hook determines how the partitioning manifests itself to the end user:
|
||||
When a **read operation** is being performed (e.g. a read, search, history, etc.), a separate [interceptor hook](#partition-interceptors) is invoked in order to determine whether the operation should target a specific partition. The outcome of this hook determines how the partitioning manifests itself to the end user:
|
||||
|
||||
* If all read operations are scoped by the interceptor to only apply to a single partition, then the partitioning behaves as a **multitenant** solution.
|
||||
* If read operations are scopes to all partitions, then the partitioning is simply partitioning the data into logical segments.
|
||||
|
@ -40,7 +40,9 @@ The [PartitionConfig](/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/c
|
|||
|
||||
The following settings can be enabled:
|
||||
|
||||
* Include Partition in Search Hashes:
|
||||
* **Include Partition in Search Hashes** ([JavaDoc](/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionConfig.html#setIncludePartitionInSearchHashes(boolean))): If this feature is enabled, partition IDs will be factored into [Search Hashes](./schema.html#search-hashes). When this flag is not set (as is the default), when a search requests a specific partition, an additional SQL WHERE predicate is added to the query to explicitly request the given partition ID. When this flag is set, this additional WHERE predicate is not necessary since the partition is factored into the hash value being searched on. Setting this flag avoids the need to manually adjust indexes against the HFJ_SPIDX tables. Note that this flag should **not be used in environments where partitioning is being used for security purposes**, since it is possible for a user to reverse engineer false hash collisions.
|
||||
|
||||
* **Cross-Partition Reference Mode**: ([JavaDoc](/apidocs/hapi-fhir-jpaserver-model/ca/uhn/fhir/jpa/model/config/PartitionConfig.html#setAllowReferencesAcrossPartitions(ca.uhn.fhir.jpa.model.config.PartitionConfig.CrossPartitionReferenceMode))): This setting controls whether resources in one partition should be allowed to create references to resources in other partitions.
|
||||
|
||||
|
||||
# Partition Interceptors
|
||||
|
@ -57,14 +59,54 @@ The criteria for determining the partition will depend on your use case. For exa
|
|||
|
||||
* If you are implementing multi-tenancy the partition might be determined by using the [Request Tenant ID](/docs/server_plain/multitenancy.html). It could also be determined by looking at request headers, or the authorized user/session context, etc.
|
||||
|
||||
* If you are implementing segmented data partitioning, the partition might be determined by examining the actual resource being created, by the identity of the sending system, etc.
|
||||
* If you are implementing segmented data partitioning, the partition might be determined by examining the actpartitionInterceptorHeadersual resource being created, by the identity of the sending system, etc.
|
||||
|
||||
## Identify Partition for Read (Optional)
|
||||
|
||||
A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE`](/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/api/Pointcut.html#STORAGE_PARTITION_IDENTIFY_CREATE) pointcut must be registered, and this hook method will be invoked every time a resource is being created in order to determine the partition to create the resource in.
|
||||
|
||||
## Example: Using Request Tenants
|
||||
## Example: Partitioning based on Tenant ID
|
||||
|
||||
The [RequestTenantPartitionInterceptor](/docs/interceptors/built_in_server_interceptors.html#request-tenant-partition-interceptor) uses the request tenant ID to determine the partition name. A simplified version of its source is shown below:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|partitionInterceptorHeaders}}
|
||||
```
|
||||
|
||||
## Example: Partitioning based on headers
|
||||
|
||||
If requests are coming from a trusted system, that system might be relied on to determine the partition for reads and writes.
|
||||
|
||||
The following example shows a simple partition interceptor that determines the partition name by looking at a custom HTTP header:
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|partitionInterceptorHeaders}}
|
||||
```
|
||||
|
||||
## Example: Using Resource Contents
|
||||
|
||||
When creating resources, the contents of the resource can also be factored into the decision on which tenant to use. The following example shows a very simple algorithm, placing resources into one of three partitions based on the resource type. Other contents in the resource could also be used instead.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|partitionInterceptorResourceContents}}
|
||||
```
|
||||
|
||||
|
||||
# Complete Example: Using Request Tenants
|
||||
|
||||
In order to achieve a multitenant configuration, the following configuration steps must be taken:
|
||||
|
||||
* Partitioning must be enabled.
|
||||
* A [Tenant Identification Strategy](/docs/server_plain/multitenancy.html) must be enabled on the RestfulServer.
|
||||
* A [RequestTenantPartitionInterceptor](/docs/interceptors/built_in_server_interceptors.html#request-tenant-partition-interceptor) instance must be registered as an interceptor.
|
||||
|
||||
Additionally, indexes will likely need to be tuned in order to support the partition-aware queries.
|
||||
|
||||
The following snippet shows a server with this configuration.
|
||||
|
||||
```java
|
||||
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/PartitionExamples.java|multitenantServer}}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ The HFJ_RESOURCE table indicates a single resource of any type in the database.
|
|||
<td></td>
|
||||
<td>
|
||||
This column contains the FHIR version associated with this resource, using a constant drawn
|
||||
from <a href="/apidocs/hapi-fhir-base/ca/uhn/fhir/context/FhirVersionEnum.html">FhirVersionEnum</a>.
|
||||
from <a href="/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/context/FhirVersionEnum.html">FhirVersionEnum</a>.
|
||||
Not to be confused with <b>RES_VER</b> above.
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -407,9 +407,119 @@ When a resource is created or updated, it is indexed for searching. Any search p
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<a name="indexes"/>
|
||||
<a name="search-indexes"/>
|
||||
|
||||
# Background: Search Indexes
|
||||
|
||||
The HFJ_SPIDX (Search Parameter Index) tables are used to index resources for searching. When a resource is created or updated, a set of rows in these tables will be added. These are used for finding appropriate rows to return when performing FHIR searches.
|
||||
The HFJ_SPIDX (Search Parameter Index) tables are used to index resources for searching. When a resource is created or updated, a set of rows in these tables will be added. These are used for finding appropriate rows to return when performing FHIR searches. There are dedicated tables for supporting each of the non-reference [FHIR Search Datatypes](http://hl7.org/fhir/search.html): Date, Number, Quantity, String, Token, and URI. Note that Reference search parameters are implemented using the [HFJ_RES_LINK](#HFJ_RES_LINK) table above.
|
||||
|
||||
<a name="search-hashes"/>
|
||||
|
||||
## Search Hashes
|
||||
|
||||
The SPIDX tables leverage "hash columns", which contain a hash of multiple columns in order to reduce index size and improve search performance. Hashes currently use the [MurmurHash3_x64_128](https://en.wikipedia.org/wiki/MurmurHash) hash algorithm, keeping only the first 64 bits in order to produce a LongInt value.
|
||||
|
||||
For example, all search index tables have columns for storing the search parameter name (**SP_NAME**) and resource type (**RES_TYPE**). An additional column which hashes these two values is provided, called **HASH_IDENTITY**.
|
||||
|
||||
In some configurations, the partition ID is also factored into the hashes.
|
||||
|
||||
## Tables
|
||||
|
||||
<img src="/hapi-fhir/docs/images/jpa_erd_search_indexes.svg" alt="Search Indexes" style="width: 100%; max-width: 900px;"/>
|
||||
|
||||
## Columns
|
||||
|
||||
The following columns are common to **all HFJ_SPIDX_xxx tables**.
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Relationships</th>
|
||||
<th>Datatype</th>
|
||||
<th>Nullable</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PARTITION_ID</td>
|
||||
<td></td>
|
||||
<td>Integer</td>
|
||||
<td>Nullable</td>
|
||||
<td>
|
||||
This is the optional partition ID, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
|
||||
Note that the partition indicated by the <b>PARTITION_ID</b> and <b>PARTITION_DATE</b> columns refers to the partition
|
||||
of the <i>SOURCE</i> resource, and not necessarily the <i>TARGET</i>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PARTITION_DATE</td>
|
||||
<td></td>
|
||||
<td>Timestamp</td>
|
||||
<td>Nullable</td>
|
||||
<td>
|
||||
This is the optional partition date, if the resource is in a partition. See <a href="./partitioning.html">Partitioning</a>.
|
||||
Note that the partition indicated by the <b>PARTITION_ID</b> and <b>PARTITION_DATE</b> columns refers to the partition
|
||||
of the <i>SOURCE</i> resource, and not necessarily the <i>TARGET</i>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SP_ID</td>
|
||||
<td></td>
|
||||
<td>Long</td>
|
||||
<td></td>
|
||||
<td>
|
||||
Holds the persistent ID
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RES_ID</td>
|
||||
<td>FK to <a href="#HFJ_RESOURCE">HFJ_RESOURCE</a></td>
|
||||
<td>String</td>
|
||||
<td></td>
|
||||
<td>
|
||||
Contains the PID of the resource being indexed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SP_NAME</td>
|
||||
<td></td>
|
||||
<td>String</td>
|
||||
<td></td>
|
||||
<td>
|
||||
This is the name of the search parameter being indexed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RES_TYPE</td>
|
||||
<td></td>
|
||||
<td>String</td>
|
||||
<td></td>
|
||||
<td>
|
||||
This is the name of the resource being indexed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SP_UPDATED</td>
|
||||
<td></td>
|
||||
<td>Timestamp</td>
|
||||
<td></td>
|
||||
<td>
|
||||
This is the time that this row was last updated.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SP_MISSING</td>
|
||||
<td></td>
|
||||
<td>boolean</td>
|
||||
<td></td>
|
||||
<td>
|
||||
If this row represents a search parameter that is **not** populated at all in the resource being indexed,
|
||||
this will be populated with the value `true`. Otherwise it will be populated with `false`.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import ca.uhn.fhir.jpa.entity.Search;
|
|||
import ca.uhn.fhir.jpa.graphql.JpaStorageServices;
|
||||
import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices;
|
||||
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
|
||||
import ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
|
||||
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
|
||||
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
|
||||
import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory;
|
||||
|
@ -42,8 +42,6 @@ import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
|
|||
import ca.uhn.fhir.jpa.search.reindex.ResourceReindexingSvcImpl;
|
||||
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider;
|
||||
|
|
|
@ -24,6 +24,7 @@ import ca.uhn.fhir.interceptor.model.PartitionId;
|
|||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
|
||||
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperService;
|
||||
|
@ -283,6 +284,9 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
@Autowired
|
||||
private IRequestPartitionHelperService myRequestPartitionHelperService;
|
||||
|
||||
@Autowired
|
||||
private PartitionConfig myPartitionConfig;
|
||||
|
||||
@Transactional()
|
||||
@Override
|
||||
public List<Suggestion> suggestKeywords(String theContext, String theSearchParam, String theText, RequestDetails theRequest) {
|
||||
|
@ -297,8 +301,10 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
throw new InvalidRequestException("Invalid context: " + theContext);
|
||||
}
|
||||
|
||||
// FIXME: this method should require a resource type
|
||||
PartitionId partitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, null);
|
||||
// Partitioning is not supported for this operation
|
||||
Validate.isTrue(myPartitionConfig.isPartitioningEnabled() == false, "Suggest keywords not supported for partitioned system");
|
||||
PartitionId partitionId = null;
|
||||
|
||||
ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(partitionId, contextParts[0], contextParts[1]);
|
||||
|
||||
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
|
||||
|
@ -315,7 +321,6 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
|||
.sentence(theText.toLowerCase()).createQuery();
|
||||
|
||||
Query query = qb.bool()
|
||||
// .must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(pid).createQuery())
|
||||
.must(qb.keyword().onField("myResourceLinksField").matching(pid.toString()).createQuery())
|
||||
.must(textQuery)
|
||||
.createQuery();
|
||||
|
|
|
@ -360,7 +360,11 @@ public class SearchBuilder implements ISearchBuilder {
|
|||
}
|
||||
myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myDeleted")));
|
||||
if (myPartitionId != null) {
|
||||
myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue"), myPartitionId.getPartitionId()));
|
||||
if (myPartitionId.getPartitionId() != null) {
|
||||
myQueryRoot.addPredicate(myCriteriaBuilder.equal(myQueryRoot.get("myPartitionIdValue").as(Integer.class), myPartitionId.getPartitionId()));
|
||||
} else {
|
||||
myQueryRoot.addPredicate(myCriteriaBuilder.isNull(myQueryRoot.get("myPartitionIdValue").as(Integer.class)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,11 +64,11 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Override
|
||||
public IResourceLookup findTargetResource(PartitionId thePartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theTypeString, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
|
||||
public IResourceLookup findTargetResource(PartitionId thePartitionId, RuntimeSearchParam theSearchParam, String theSourcePath, IIdType theSourceResourceId, String theResourceType, Class<? extends IBaseResource> theType, IBaseReference theReference, RequestDetails theRequest) {
|
||||
IResourceLookup resolvedResource;
|
||||
String idPart = theSourceResourceId.getIdPart();
|
||||
try {
|
||||
resolvedResource = myIdHelperService.resolveResourceIdentity(thePartitionId, theTypeString, idPart, theRequest);
|
||||
resolvedResource = myIdHelperService.resolveResourceIdentity(thePartitionId, theResourceType, idPart, theRequest);
|
||||
ourLog.trace("Translated {}/{} to resource PID {}", theType, idPart, resolvedResource);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
|
||||
|
@ -89,8 +89,8 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
}
|
||||
|
||||
ourLog.trace("Resolved resource of type {} as PID: {}", resolvedResource.getResourceType(), resolvedResource.getResourceId());
|
||||
if (!theTypeString.equals(resolvedResource.getResourceType())) {
|
||||
ourLog.error("Resource with PID {} was of type {} and wanted {}", resolvedResource.getResourceId(), theTypeString, resolvedResource.getResourceType());
|
||||
if (!theResourceType.equals(resolvedResource.getResourceType())) {
|
||||
ourLog.error("Resource with PID {} was of type {} and wanted {}", resolvedResource.getResourceId(), theResourceType, resolvedResource.getResourceType());
|
||||
throw new UnprocessableEntityException("Resource contains reference to unknown resource ID " + theSourceResourceId.getValue());
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
|
|||
throw new InvalidRequestException("Resource " + resName + "/" + idPart + " is deleted, specified in path: " + theSourcePath);
|
||||
}
|
||||
|
||||
if (!theSearchParam.hasTargets() && theSearchParam.getTargets().contains(theTypeString)) {
|
||||
if (!theSearchParam.hasTargets() && theSearchParam.getTargets().contains(theResourceType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,7 @@ package ca.uhn.fhir.jpa.dao.index;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.interceptor.model.PartitionId;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
|
||||
|
@ -32,13 +30,10 @@ import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
|
|||
import ca.uhn.fhir.jpa.model.cross.ResourceLookup;
|
||||
import ca.uhn.fhir.jpa.model.cross.ResourcePersistentId;
|
||||
import ca.uhn.fhir.jpa.model.entity.ForcedId;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
|
@ -68,6 +63,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/**
|
||||
* This class is used to convert between PIDs (the internal primary key for a particular resource as
|
||||
|
@ -122,9 +118,9 @@ public class IdHelperService {
|
|||
* @throws ResourceNotFoundException If the ID can not be found
|
||||
*/
|
||||
@Nonnull
|
||||
public IResourceLookup resolveResourceIdentity(PartitionId thePartitionId, String theResourceName, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException {
|
||||
public IResourceLookup resolveResourceIdentity(PartitionId thePartitionId, String theResourceType, String theResourceId, RequestDetails theRequestDetails) throws ResourceNotFoundException {
|
||||
// We only pass 1 input in so only 0..1 will come back
|
||||
IdDt id = new IdDt(theResourceName, theResourceId);
|
||||
IdDt id = new IdDt(theResourceType, theResourceId);
|
||||
Collection<IResourceLookup> matches = translateForcedIdToPids(thePartitionId, theRequestDetails, Collections.singletonList(id));
|
||||
assert matches.size() <= 1;
|
||||
if (matches.isEmpty()) {
|
||||
|
@ -344,22 +340,16 @@ public class IdHelperService {
|
|||
|
||||
if (nextIds.size() > 0) {
|
||||
Collection<Object[]> views;
|
||||
if (isBlank(nextResourceType)) {
|
||||
warnAboutUnqualifiedForcedIdResolution(theRequest);
|
||||
assert isNotBlank(nextResourceType);
|
||||
|
||||
// FIXME: deal with partition here
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextIds);
|
||||
|
||||
} else {
|
||||
if (thePartitionId != null) {
|
||||
if (thePartitionId.getPartitionId() != null) {
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, thePartitionId.getPartitionId());
|
||||
} else {
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds);
|
||||
}
|
||||
if (thePartitionId != null) {
|
||||
if (thePartitionId.getPartitionId() != null) {
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartition(nextResourceType, nextIds, thePartitionId.getPartitionId());
|
||||
} else {
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds);
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoTypeInPartitionNull(nextResourceType, nextIds);
|
||||
}
|
||||
} else {
|
||||
views = myForcedIdDao.findAndResolveByForcedIdWithNoType(nextResourceType, nextIds);
|
||||
}
|
||||
|
||||
for (Object[] next : views) {
|
||||
|
@ -382,17 +372,6 @@ public class IdHelperService {
|
|||
return retVal;
|
||||
}
|
||||
|
||||
private void warnAboutUnqualifiedForcedIdResolution(RequestDetails theRequest) {
|
||||
StorageProcessingMessage msg = new StorageProcessingMessage()
|
||||
.setMessage("This search uses unqualified resource IDs (an ID without a resource type). This is less efficient than using a qualified type.");
|
||||
ourLog.debug(msg.getMessage());
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||
.add(StorageProcessingMessage.class, msg);
|
||||
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
myPersistentIdCache.invalidateAll();
|
||||
myResourceLookupCache.invalidateAll();
|
||||
|
|
|
@ -27,7 +27,6 @@ import ca.uhn.fhir.jpa.dao.SearchBuilder;
|
|||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
|
||||
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
|
||||
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.entity.SearchParamPresent;
|
||||
|
@ -146,16 +145,23 @@ abstract class BasePredicateBuilder {
|
|||
}
|
||||
|
||||
Predicate combineParamIndexPredicateWithParamNamePredicate(String theResourceName, String theParamName, From<?, ? extends BaseResourceIndexedSearchParam> theFrom, Predicate thePredicate, PartitionId thePartitionId) {
|
||||
List<Predicate> andPredicates = new ArrayList<>();
|
||||
addPartitionIdPredicate(thePartitionId, theFrom, andPredicates);
|
||||
|
||||
if (myDontUseHashesForSearch) {
|
||||
Predicate resourceTypePredicate = myCriteriaBuilder.equal(theFrom.get("myResourceType"), theResourceName);
|
||||
Predicate paramNamePredicate = myCriteriaBuilder.equal(theFrom.get("myParamName"), theParamName);
|
||||
Predicate outerPredicate = myCriteriaBuilder.and(resourceTypePredicate, paramNamePredicate, thePredicate);
|
||||
return outerPredicate;
|
||||
andPredicates.add(resourceTypePredicate);
|
||||
andPredicates.add(paramNamePredicate);
|
||||
andPredicates.add(thePredicate);
|
||||
} else {
|
||||
long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionConfig, thePartitionId, theResourceName, theParamName);
|
||||
Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
|
||||
andPredicates.add(hashIdentityPredicate);
|
||||
andPredicates.add(thePredicate);
|
||||
}
|
||||
|
||||
long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionConfig, thePartitionId, theResourceName, theParamName);
|
||||
Predicate hashIdentityPredicate = myCriteriaBuilder.equal(theFrom.get("myHashIdentity"), hashIdentity);
|
||||
return myCriteriaBuilder.and(hashIdentityPredicate, thePredicate);
|
||||
return myCriteriaBuilder.and(toArray(andPredicates));
|
||||
}
|
||||
|
||||
public PartitionConfig getPartitionConfig() {
|
||||
|
@ -217,7 +223,7 @@ abstract class BasePredicateBuilder {
|
|||
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, num, thePartitionId);
|
||||
}
|
||||
|
||||
void addPartitionIdPredicate(PartitionId thePartitionId, Join<ResourceTable, ? extends BasePartitionable> theJoin, List<Predicate> theCodePredicates) {
|
||||
void addPartitionIdPredicate(PartitionId thePartitionId, From<?, ? extends BasePartitionable> theJoin, List<Predicate> theCodePredicates) {
|
||||
if (thePartitionId != null) {
|
||||
Integer partitionId = thePartitionId.getPartitionId();
|
||||
Predicate partitionPredicate;
|
||||
|
|
|
@ -82,17 +82,13 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
|||
}
|
||||
|
||||
List<Predicate> codePredicates = new ArrayList<>();
|
||||
addPartitionIdPredicate(thePartitionId, join, codePredicates);
|
||||
|
||||
for (IQueryParameterType nextOr : theList) {
|
||||
IQueryParameterType params = nextOr;
|
||||
Predicate p = createPredicateDate(params,
|
||||
theResourceName,
|
||||
theParamName,
|
||||
Predicate p = createPredicateDate(nextOr,
|
||||
myCriteriaBuilder,
|
||||
join,
|
||||
operation,
|
||||
thePartitionId);
|
||||
operation
|
||||
);
|
||||
codePredicates.add(p);
|
||||
}
|
||||
|
||||
|
@ -116,22 +112,17 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
|||
From<?, ResourceIndexedSearchParamDate> theFrom,
|
||||
PartitionId thePartitionId) {
|
||||
Predicate predicateDate = createPredicateDate(theParam,
|
||||
theResourceName,
|
||||
theParamName,
|
||||
theBuilder,
|
||||
theFrom,
|
||||
null,
|
||||
thePartitionId);
|
||||
null
|
||||
);
|
||||
return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, predicateDate, thePartitionId);
|
||||
}
|
||||
|
||||
private Predicate createPredicateDate(IQueryParameterType theParam,
|
||||
String theResourceName,
|
||||
String theParamName,
|
||||
CriteriaBuilder theBuilder,
|
||||
From<?, ResourceIndexedSearchParamDate> theFrom,
|
||||
SearchFilterParser.CompareOperation theOperation,
|
||||
PartitionId thePartitionId) {
|
||||
SearchFilterParser.CompareOperation theOperation) {
|
||||
|
||||
Predicate p;
|
||||
if (theParam instanceof DateParam) {
|
||||
|
@ -159,15 +150,14 @@ public class PredicateBuilderDate extends BasePredicateBuilder implements IPredi
|
|||
return p;
|
||||
}
|
||||
|
||||
// FIXME: does this need a partition ID?
|
||||
private Predicate createPredicateDateFromRange(CriteriaBuilder theBuilder,
|
||||
From<?, ResourceIndexedSearchParamDate> theFrom,
|
||||
DateRangeParam theRange,
|
||||
SearchFilterParser.CompareOperation operation) {
|
||||
Date lowerBound = theRange.getLowerBoundAsInstant();
|
||||
Date upperBound = theRange.getUpperBoundAsInstant();
|
||||
Predicate lt = null;
|
||||
Predicate gt = null;
|
||||
Predicate lt;
|
||||
Predicate gt;
|
||||
Predicate lb = null;
|
||||
Predicate ub = null;
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ import org.springframework.context.annotation.Scope;
|
|||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.persistence.criteria.From;
|
||||
import javax.persistence.criteria.Join;
|
||||
import javax.persistence.criteria.JoinType;
|
||||
|
@ -193,6 +194,12 @@ class PredicateBuilderReference extends BasePredicateBuilder {
|
|||
List<Predicate> codePredicates = new ArrayList<>();
|
||||
addPartitionIdPredicate(thePartitionId, join, codePredicates);
|
||||
|
||||
for (IIdType next : targetIds) {
|
||||
if (!next.hasResourceType()) {
|
||||
warnAboutPerformanceOnUnqualifiedResources(theParamName, theRequest, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Resources by ID
|
||||
List<ResourcePersistentId> targetPids = myIdHelperService.resolveResourcePersistentIdsWithCache(thePartitionId, targetIds, theRequest);
|
||||
if (!targetPids.isEmpty()) {
|
||||
|
@ -419,13 +426,20 @@ class PredicateBuilderReference extends BasePredicateBuilder {
|
|||
return predicate;
|
||||
}
|
||||
|
||||
private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, List<Class<? extends IBaseResource>> theCandidateTargetTypes) {
|
||||
String message = new StringBuilder()
|
||||
.append("This search uses an unqualified resource(a parameter in a chain without a resource type). ")
|
||||
.append("This is less efficient than using a qualified type. ")
|
||||
.append("[" + theParamName + "] resolves to [" + theCandidateTargetTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(",")) + "].")
|
||||
.append("If you know what you're looking for, try qualifying it like this: ")
|
||||
.append(theCandidateTargetTypes.stream().map(cls -> "[" + cls.getSimpleName() + ":" + theParamName + "]").collect(Collectors.joining(" or ")))
|
||||
private void warnAboutPerformanceOnUnqualifiedResources(String theParamName, RequestDetails theRequest, @Nullable List<Class<? extends IBaseResource>> theCandidateTargetTypes) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("This search uses an unqualified resource(a parameter in a chain without a resource type). ");
|
||||
builder.append("This is less efficient than using a qualified type. ");
|
||||
if (theCandidateTargetTypes != null) {
|
||||
builder.append("[" + theParamName + "] resolves to [" + theCandidateTargetTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(",")) + "].");
|
||||
builder.append("If you know what you're looking for, try qualifying it using the form ");
|
||||
builder.append(theCandidateTargetTypes.stream().map(cls -> "[" + cls.getSimpleName() + ":" + theParamName + "]").collect(Collectors.joining(" or ")));
|
||||
} else {
|
||||
builder.append("If you know what you're looking for, try qualifying it using the form: '");
|
||||
builder.append(theParamName).append(":[resourceType]");
|
||||
builder.append("'");
|
||||
}
|
||||
String message = builder
|
||||
.toString();
|
||||
StorageProcessingMessage msg = new StorageProcessingMessage()
|
||||
.setMessage(message);
|
||||
|
|
|
@ -134,7 +134,6 @@ class PredicateBuilderTag extends BasePredicateBuilder {
|
|||
continue;
|
||||
}
|
||||
|
||||
// FIXME: add test for :missing
|
||||
if (paramInverted) {
|
||||
ourLog.debug("Searching for _tag:not");
|
||||
|
||||
|
@ -160,6 +159,11 @@ class PredicateBuilderTag extends BasePredicateBuilder {
|
|||
defJoin.where(tagListPredicate);
|
||||
|
||||
continue;
|
||||
|
||||
} else {
|
||||
|
||||
myQueryRoot.setHasIndexJoins();
|
||||
|
||||
}
|
||||
|
||||
Join<ResourceTable, ResourceTag> tagJoin = myQueryRoot.join("myTags", JoinType.LEFT);
|
||||
|
@ -172,7 +176,6 @@ class PredicateBuilderTag extends BasePredicateBuilder {
|
|||
addPartitionIdPredicate(thePartitionId, tagJoin, predicates);
|
||||
}
|
||||
|
||||
myQueryRoot.setHasIndexJoins();
|
||||
myQueryRoot.addPredicates(predicates);
|
||||
|
||||
}
|
||||
|
|
|
@ -104,10 +104,6 @@ public class TestUtil {
|
|||
Subselect subselect = theClazz.getAnnotation(Subselect.class);
|
||||
boolean isView = (subselect != null);
|
||||
|
||||
// FIXME: remove?
|
||||
Embeddable embeddable = theClazz.getAnnotation(Embeddable.class);
|
||||
boolean isEmbeddable = (embeddable != null);
|
||||
|
||||
scan(theClazz, theNames, theIsSuperClass, isView);
|
||||
|
||||
for (Field nextField : theClazz.getDeclaredFields()) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package ca.uhn.fhir.jpa.dao.r4;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.jpa.api.config.DaoConfig;
|
||||
import ca.uhn.fhir.jpa.entity.Search;
|
||||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
|
@ -12,6 +15,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
|||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
|
||||
|
@ -46,6 +50,8 @@ import org.junit.AfterClass;
|
|||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.TransactionCallback;
|
||||
|
@ -81,6 +87,8 @@ import static org.junit.Assert.assertThat;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@SuppressWarnings({"unchecked", "Duplicates"})
|
||||
public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
||||
|
@ -3507,6 +3515,44 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchReferenceUntyped() {
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
p.setId("PAT");
|
||||
myPatientDao.update(p);
|
||||
|
||||
AuditEvent audit = new AuditEvent();
|
||||
audit.setId("AUDIT");
|
||||
audit.addEntity().getWhat().setReference("Patient/PAT");
|
||||
myAuditEventDao.update(audit);
|
||||
|
||||
IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class);
|
||||
try {
|
||||
myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.JPA_PERFTRACE_WARNING, interceptor);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.setLoadSynchronous(true);
|
||||
map.add(AuditEvent.SP_ENTITY, new ReferenceParam("PAT"));
|
||||
IBundleProvider outcome = myAuditEventDao.search(map);
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome), contains("AuditEvent/AUDIT"));
|
||||
|
||||
myCaptureQueriesListener.logSelectQueriesForCurrentThread();
|
||||
|
||||
} finally {
|
||||
myInterceptorRegistry.unregisterInterceptor(interceptor);
|
||||
}
|
||||
|
||||
ArgumentCaptor<HookParams> captor = ArgumentCaptor.forClass(HookParams.class);
|
||||
verify(interceptor, times(1)).invoke(ArgumentMatchers.eq(Pointcut.JPA_PERFTRACE_WARNING), captor.capture());
|
||||
StorageProcessingMessage message = captor.getValue().get(StorageProcessingMessage.class);
|
||||
assertEquals("This search uses an unqualified resource(a parameter in a chain without a resource type). This is less efficient than using a qualified type. If you know what you're looking for, try qualifying it using the form: 'entity:[resourceType]'", message.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearchWithDateAndReusesExistingJoin() {
|
||||
// Add a search parameter to Observation.issued, so that between that one
|
||||
|
|
|
@ -12,7 +12,10 @@ import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
|
|||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.rest.api.Constants;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.DateAndListParam;
|
||||
import ca.uhn.fhir.rest.param.DateOrListParam;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
|
@ -1156,7 +1159,239 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
|
|||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
}
|
||||
|
||||
// FIXME: add DATE and DATE RANGE test
|
||||
@Test
|
||||
public void testSearch_DateParam_SearchAllPartitions() {
|
||||
myPartitionConfig.setIncludePartitionInSearchHashes(false);
|
||||
|
||||
IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20"));
|
||||
IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20"));
|
||||
IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20"));
|
||||
createPatient(null, withBirthdate("2021-04-20"));
|
||||
createPatient(1, withBirthdate("2021-04-20"));
|
||||
createPatient(2, withBirthdate("2021-04-20"));
|
||||
|
||||
// Date param
|
||||
|
||||
addReadPartition(null);
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20"));
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
|
||||
|
||||
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// Date OR param
|
||||
|
||||
addReadPartition(null);
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22")));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// Date AND param
|
||||
|
||||
addReadPartition(null);
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20"))));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// DateRangeParam
|
||||
|
||||
addReadPartition(null);
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25")));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearch_DateParam_SearchSpecificPartitions() {
|
||||
myPartitionConfig.setIncludePartitionInSearchHashes(false);
|
||||
|
||||
IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20"));
|
||||
IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20"));
|
||||
IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20"));
|
||||
createPatient(null, withBirthdate("2021-04-20"));
|
||||
createPatient(1, withBirthdate("2021-04-20"));
|
||||
createPatient(2, withBirthdate("2021-04-20"));
|
||||
|
||||
// Date param
|
||||
|
||||
addReadPartition(1);
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20"));
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientId1));
|
||||
|
||||
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// Date OR param
|
||||
|
||||
addReadPartition(1);
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22")));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientId1));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// Date AND param
|
||||
|
||||
addReadPartition(1);
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20"))));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientId1));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// DateRangeParam
|
||||
|
||||
addReadPartition(1);
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25")));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientId1));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearch_DateParam_SearchDefaultPartitions() {
|
||||
myPartitionConfig.setIncludePartitionInSearchHashes(false);
|
||||
|
||||
IIdType patientIdNull = createPatient(null, withBirthdate("2020-04-20"));
|
||||
IIdType patientId1 = createPatient(1, withBirthdate("2020-04-20"));
|
||||
IIdType patientId2 = createPatient(2, withBirthdate("2020-04-20"));
|
||||
createPatient(null, withBirthdate("2021-04-20"));
|
||||
createPatient(1, withBirthdate("2021-04-20"));
|
||||
createPatient(2, withBirthdate("2021-04-20"));
|
||||
|
||||
// Date param
|
||||
|
||||
addDefaultReadPartition();
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateParam("2020-04-20"));
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull));
|
||||
|
||||
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// Date OR param
|
||||
|
||||
addDefaultReadPartition();
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateOrListParam().addOr(new DateParam("2020-04-20")).addOr(new DateParam("2020-04-22")));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// Date AND param
|
||||
|
||||
addDefaultReadPartition();
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateAndListParam().addAnd(new DateOrListParam().addOr(new DateParam("2020"))).addAnd(new DateOrListParam().addOr(new DateParam("2020-04-20"))));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
// DateRangeParam
|
||||
|
||||
addDefaultReadPartition();
|
||||
myCaptureQueriesListener.clear();
|
||||
map = new SearchParameterMap();
|
||||
map.add(Patient.SP_BIRTHDATE, new DateRangeParam(new DateParam("2020-01-01"), new DateParam("2020-04-25")));
|
||||
map.setLoadSynchronous(true);
|
||||
results = myPatientDao.search(map);
|
||||
ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull));
|
||||
|
||||
searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(2, StringUtils.countMatches(searchSql, "SP_VALUE_LOW"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSearch_StringParam_SearchAllPartitions() {
|
||||
|
@ -1298,6 +1533,80 @@ public class PartitioningR4Test extends BaseJpaR4SystemTest {
|
|||
assertEquals(1, StringUtils.countMatches(searchSql, "SP_VALUE_NORMALIZED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch_TagNotParam_SearchAllPartitions() {
|
||||
IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code"));
|
||||
IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code"));
|
||||
IIdType patientId2 = createPatient(2, withActiveTrue(), withTag("http://system", "code"));
|
||||
createPatient(null, withActiveTrue(), withTag("http://system", "code2"));
|
||||
createPatient(1, withActiveTrue(), withTag("http://system", "code2"));
|
||||
createPatient(2, withActiveTrue(), withTag("http://system", "code2"));
|
||||
|
||||
addReadPartition(null);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT));
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientIdNull, patientId1, patientId2));
|
||||
|
||||
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(0, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch_TagNotParam_SearchDefaultPartition() {
|
||||
IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code"));
|
||||
createPatient(1, withActiveTrue(), withTag("http://system", "code"));
|
||||
createPatient(2, withActiveTrue(), withTag("http://system", "code"));
|
||||
|
||||
addDefaultReadPartition();
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT));
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
|
||||
|
||||
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID is null"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'"));
|
||||
|
||||
assertThat(ids.toString(), ids, Matchers.contains(patientIdNull));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch_TagNotParam_SearchOnePartition() {
|
||||
createPatient(null, withActiveTrue(), withTag("http://system", "code"));
|
||||
IIdType patientId1 = createPatient(1, withActiveTrue(), withTag("http://system", "code"));
|
||||
createPatient(2, withActiveTrue(), withTag("http://system", "code"));
|
||||
createPatient(null, withActiveTrue(), withTag("http://system", "code2"));
|
||||
createPatient(1, withActiveTrue(), withTag("http://system", "code2"));
|
||||
createPatient(2, withActiveTrue(), withTag("http://system", "code2"));
|
||||
|
||||
addReadPartition(1);
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap map = new SearchParameterMap();
|
||||
map.add(Constants.PARAM_TAG, new TokenParam("http://system", "code2").setModifier(TokenParamModifier.NOT));
|
||||
map.setLoadSynchronous(true);
|
||||
IBundleProvider results = myPatientDao.search(map);
|
||||
List<IIdType> ids = toUnqualifiedVersionlessIds(results);
|
||||
assertThat(ids, Matchers.contains(patientId1));
|
||||
|
||||
String searchSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, true);
|
||||
ourLog.info("Search SQL:\n{}", searchSql);
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "PARTITION_ID"));
|
||||
assertEquals(1, StringUtils.countMatches(searchSql, "TAG_SYSTEM='http://system'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearch_TagParam_SearchAllPartitions() {
|
||||
IIdType patientIdNull = createPatient(null, withActiveTrue(), withTag("http://system", "code"));
|
||||
|
|
|
@ -3,7 +3,7 @@ package ca.uhn.fhir.jpa.provider.r4;
|
|||
import ca.uhn.fhir.jpa.model.config.PartitionConfig;
|
||||
import ca.uhn.fhir.jpa.model.util.ProviderConstants;
|
||||
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
|
||||
import ca.uhn.fhir.jpa.partition.RequestTenantPartitionInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
|
||||
import ca.uhn.fhir.jpa.util.TestUtil;
|
||||
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
|
||||
import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor;
|
||||
|
|
|
@ -688,9 +688,8 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
|
|||
" <system value=\"http://healthcare.example.org/identifiers/encounter\"/>\n" +
|
||||
" <value value=\"845962.8975469\"/>\n" +
|
||||
" </identifier>\n" +
|
||||
// FIXME: restore
|
||||
// " <status value=\"in-progress\"/>\n" +
|
||||
// " <class value=\"inpatient\"/>\n" +
|
||||
" <status value=\"in-progress\"/>\n" +
|
||||
" <class value=\"inpatient\"/>\n" +
|
||||
" <patient>\n" +
|
||||
" <reference value=\"Patient?family=van%20de%20Heuvelcx85ioqWJbI&given=Pietercx85ioqWJbI\"/>\n" +
|
||||
" </patient>\n" +
|
||||
|
@ -710,9 +709,6 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
|
|||
HttpPost req = new HttpPost(ourServerBase);
|
||||
req.setEntity(new StringEntity(input, ContentType.parse(Constants.CT_FHIR_XML + "; charset=utf-8")));
|
||||
|
||||
// FIXME: remove
|
||||
myDaoConfig.setIndexMissingFields(DaoConfig.IndexEnabledEnum.DISABLED);
|
||||
|
||||
CloseableHttpResponse resp = ourHttpClient.execute(req);
|
||||
try {
|
||||
String encoded = IOUtils.toString(resp.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
|
|
|
@ -201,6 +201,7 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
|
|||
return b.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public boolean matches(IQueryParameterType theParam) {
|
||||
if (!(theParam instanceof DateParam)) {
|
||||
|
@ -216,7 +217,6 @@ public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchPar
|
|||
return false;
|
||||
}
|
||||
|
||||
// FIXME: below is always true
|
||||
boolean result = true;
|
||||
if (lowerBound != null) {
|
||||
result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package ca.uhn.fhir.jpa.partition;
|
||||
package ca.uhn.fhir.rest.server.interceptor.partition;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR JPA Server
|
||||
* HAPI FHIR - Server Framework
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2020 University Health Network
|
||||
* %%
|
||||
|
@ -26,19 +26,28 @@ import ca.uhn.fhir.interceptor.api.Pointcut;
|
|||
import ca.uhn.fhir.interceptor.model.PartitionId;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
/**
|
||||
* This interceptor uses the request tenant ID (as supplied to the server using
|
||||
* {@link ca.uhn.fhir.rest.server.RestfulServer#setTenantIdentificationStrategy(ITenantIdentificationStrategy)}
|
||||
* to indicate the partition ID. With this interceptor registered, The server treats the tenant name
|
||||
* supplied by the {@link ITenantIdentificationStrategy tenant identification strategy} as a partition name.
|
||||
* <p>
|
||||
* Partition names (aka tenant IDs) must be registered in advance using the partition management operations.
|
||||
* </p>
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
@Interceptor
|
||||
public class RequestTenantPartitionInterceptor {
|
||||
|
||||
@Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
|
||||
public PartitionId PartitionIdentifyCreate(IBaseResource theResource, ServletRequestDetails theRequestDetails) {
|
||||
public PartitionId PartitionIdentifyCreate(ServletRequestDetails theRequestDetails) {
|
||||
return extractPartitionIdFromRequest(theRequestDetails);
|
||||
}
|
||||
|
||||
|
@ -47,7 +56,7 @@ public class RequestTenantPartitionInterceptor {
|
|||
return extractPartitionIdFromRequest(theRequestDetails);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Nonnull
|
||||
private PartitionId extractPartitionIdFromRequest(ServletRequestDetails theRequestDetails) {
|
||||
|
||||
// We will use the tenant ID that came from the request as the partition name
|
|
@ -33,7 +33,16 @@ import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
|
|||
|
||||
/**
|
||||
* This class is a tenant identification strategy which assumes that a single path
|
||||
* element will be present between the server base URL and the beginning
|
||||
* element will be present between the server base URL and individual request.
|
||||
* <p>
|
||||
* For example,
|
||||
* with this strategy enabled, given the following URL on a server with base URL <code>http://example.com/base</code>,
|
||||
* the server will extract the <code>TENANT-A</code> portion of the URL and use it as the tenant identifier. The
|
||||
* request will then proceed to read the resource with ID <code>Patient/123</code>.
|
||||
* </p>
|
||||
* <p>
|
||||
* GET http://example.com/base/TENANT-A/Patient/123
|
||||
* </p>
|
||||
*/
|
||||
public class UrlBaseTenantIdentificationStrategy implements ITenantIdentificationStrategy {
|
||||
|
||||
|
|
Loading…
Reference in New Issue