Skip to content

Commit 3b91873

Browse files
authored
delete expunge (hapifhir#2131)
Added delete _expunge=true
1 parent 3738297 commit 3b91873

File tree

49 files changed

+939
-192
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+939
-192
lines changed

hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,95 @@ public enum Pointcut {
916916
"org.hl7.fhir.instance.model.api.IBaseResource"
917917
),
918918

919+
/**
920+
* <b>Storage Hook:</b>
921+
* Invoked when a set of resources are about to be deleted and expunged via url like http://localhost/Patient?active=false&_expunge=true
922+
* <p>
923+
* Hooks may accept the following parameters:
924+
* </p>
925+
* <ul>
926+
* <li>
927+
* 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
928+
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
929+
* pulled out of the servlet request. Note that the bean
930+
* properties are not all guaranteed to be populated, depending on how early during processing the
931+
* exception occurred. <b>Note that this parameter may be null in contexts where the request is not
932+
* known, such as while processing searches</b>
933+
* </li>
934+
* <li>
935+
* 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
936+
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
937+
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
938+
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
939+
* </li>
940+
* <li>
941+
* java.lang.String - Contains the url used to delete and expunge the resources
942+
* </li>
943+
* </ul>
944+
* <p>
945+
* Hooks should return <code>void</code>. They may choose to throw an exception however, in
946+
* which case the delete expunge will not occur.
947+
* </p>
948+
*/
949+
950+
STORAGE_PRE_DELETE_EXPUNGE(
951+
void.class,
952+
"ca.uhn.fhir.rest.api.server.RequestDetails",
953+
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails",
954+
"java.lang.String"
955+
),
956+
957+
/**
958+
* <b>Storage Hook:</b>
959+
* Invoked when a batch of resource pids are about to be deleted and expunged via url like http://localhost/Patient?active=false&_expunge=true
960+
* <p>
961+
* Hooks may accept the following parameters:
962+
* </p>
963+
* <ul>
964+
* <li>
965+
* java.lang.String - the name of the resource type being deleted
966+
* </li>
967+
* <li>
968+
* java.util.List - the list of Long pids of the resources about to be deleted
969+
* </li>
970+
* <li>
971+
* java.util.concurrent.atomic.AtomicLong - holds a running tally of all entities deleted so far.
972+
* If the pointcut callback deletes any entities, then this parameter should be incremented by the total number
973+
* of additional entities deleted.
974+
* </li>
975+
* <li>
976+
* 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
977+
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
978+
* pulled out of the servlet request. Note that the bean
979+
* properties are not all guaranteed to be populated, depending on how early during processing the
980+
* exception occurred. <b>Note that this parameter may be null in contexts where the request is not
981+
* known, such as while processing searches</b>
982+
* </li>
983+
* <li>
984+
* 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
985+
* resource type and logical ID (if any) and other FHIR-specific aspects of the request which have been
986+
* pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will
987+
* only be populated when operating in a RestfulServer implementation. It is provided as a convenience.
988+
* </li>
989+
* <li>
990+
* java.lang.String - Contains the url used to delete and expunge the resources
991+
* </li>
992+
* </ul>
993+
* <p>
994+
* Hooks should return <code>void</code>. They may choose to throw an exception however, in
995+
* which case the delete expunge will not occur.
996+
* </p>
997+
*/
998+
999+
STORAGE_PRE_DELETE_EXPUNGE_PID_LIST(
1000+
void.class,
1001+
"java.lang.String",
1002+
"java.util.List",
1003+
"java.util.concurrent.atomic.AtomicLong",
1004+
"ca.uhn.fhir.rest.api.server.RequestDetails",
1005+
"ca.uhn.fhir.rest.server.servlet.ServletRequestDetails"
1006+
),
1007+
9191008
/**
9201009
* <b>Storage Hook:</b>
9211010
* Invoked when one or more resources may be returned to the user, whether as a part of a READ,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
type: add
3+
issue: 2131
4+
title: "A new _expunge parameter has been added to the DELETE operation when deleting multiple resources via a URL. For
5+
example DELETE http://www.example.com:8000/Observation?_expunge=true or
6+
DELETE http://www.example.com:8000/Observation?status=cancelled&_expunge=true. When the _expunge parameter is provided to DELETE
7+
then the matched resources and all of their history will be both deleted and expunged from the database. This will
8+
perform considerably faster than doing the delete and expunge separately. Note that Expunge must be enabled on the
9+
server for this to work."
10+

hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public class DaoConfig {
144144
private IdStrategyEnum myResourceServerIdStrategy = IdStrategyEnum.SEQUENTIAL_NUMERIC;
145145
private boolean myMarkResourcesForReindexingUponSearchParameterChange;
146146
private boolean myExpungeEnabled;
147+
private boolean myDeleteExpungeEnabled;
147148
private int myExpungeBatchSize = DEFAULT_EXPUNGE_BATCH_SIZE;
148149
private int myReindexThreadCount;
149150
private int myExpungeThreadCount;
@@ -1308,6 +1309,42 @@ public boolean isExpungeEnabled() {
13081309
return myExpungeEnabled;
13091310
}
13101311

1312+
/**
1313+
* If set to <code>true</code> (default is <code>false</code>), the _expunge parameter on the DELETE
1314+
* operation will be enabled on this server. DELETE _expunge removes all data associated with a resource in a highly performant
1315+
* way, skipping most of the the checks that are enforced with usual DELETE operations. The only check
1316+
* that is performed before deleting the resources and their indexes is that no other resources reference the resources about to
1317+
* be deleted. This operation is potentially dangerous since it allows
1318+
* a client to physically delete data in a way that can not be recovered (without resorting
1319+
* to backups).
1320+
* <p>
1321+
* It is recommended to not enable this setting without appropriate security
1322+
* in place on your server to prevent non-administrators from using this
1323+
* operation.
1324+
* </p>
1325+
*/
1326+
public void setDeleteExpungeEnabled(boolean theDeleteExpungeEnabled) {
1327+
myDeleteExpungeEnabled = theDeleteExpungeEnabled;
1328+
}
1329+
1330+
/**
1331+
* If set to <code>true</code> (default is <code>false</code>), the _expunge parameter on the DELETE
1332+
* operation will be enabled on this server. DELETE _expunge removes all data associated with a resource in a highly performant
1333+
* way, skipping most of the the checks that are enforced with usual DELETE operations. The only check
1334+
* that is performed before deleting the data is that no other resources reference the resources about to
1335+
* be deleted. This operation is potentially dangerous since it allows
1336+
* a client to physically delete data in a way that can not be recovered (without resorting
1337+
* to backups).
1338+
* <p>
1339+
* It is recommended to not enable this setting without appropriate security
1340+
* in place on your server to prevent non-administrators from using this
1341+
* operation.
1342+
* </p>
1343+
*/
1344+
public boolean isDeleteExpungeEnabled() {
1345+
return myDeleteExpungeEnabled;
1346+
}
1347+
13111348
/**
13121349
* If set to <code>true</code> (default is <code>false</code>), the $expunge operation
13131350
* will be enabled on this server. This operation is potentially dangerous since it allows

hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/model/DeleteMethodOutcome.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
public class DeleteMethodOutcome extends MethodOutcome {
3333

3434
private List<ResourceTable> myDeletedEntities;
35+
private long myExpungedResourcesCount;
36+
private long myExpungedEntitiesCount;
3537

3638
public List<ResourceTable> getDeletedEntities() {
3739
return myDeletedEntities;
@@ -42,4 +44,21 @@ public DeleteMethodOutcome setDeletedEntities(List<ResourceTable> theDeletedEnti
4244
return this;
4345
}
4446

47+
public long getExpungedResourcesCount() {
48+
return myExpungedResourcesCount;
49+
}
50+
51+
public DeleteMethodOutcome setExpungedResourcesCount(long theExpungedResourcesCount) {
52+
myExpungedResourcesCount = theExpungedResourcesCount;
53+
return this;
54+
}
55+
56+
public long getExpungedEntitiesCount() {
57+
return myExpungedEntitiesCount;
58+
}
59+
60+
public DeleteMethodOutcome setExpungedEntitiesCount(long theExpungedEntitiesCount) {
61+
myExpungedEntitiesCount = theExpungedEntitiesCount;
62+
return this;
63+
}
4564
}

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
3434
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
3535
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
36+
import ca.uhn.fhir.jpa.dao.expunge.DeleteExpungeService;
3637
import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
3738
import ca.uhn.fhir.jpa.delete.DeleteConflictService;
3839
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
@@ -51,6 +52,7 @@
5152
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
5253
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
5354
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
55+
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
5456
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
5557
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
5658
import ca.uhn.fhir.model.api.IQueryParameterType;
@@ -102,6 +104,7 @@
102104
import org.jetbrains.annotations.Nullable;
103105
import org.springframework.beans.factory.annotation.Autowired;
104106
import org.springframework.beans.factory.annotation.Required;
107+
import org.springframework.data.domain.SliceImpl;
105108
import org.springframework.transaction.PlatformTransactionManager;
106109
import org.springframework.transaction.TransactionDefinition;
107110
import org.springframework.transaction.annotation.Propagation;
@@ -151,6 +154,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
151154
private HapiTransactionService myTransactionService;
152155
@Autowired(required = false)
153156
protected IFulltextSearchSvc mySearchDao;
157+
@Autowired
158+
private MatchUrlService myMatchUrlService;
159+
@Autowired
160+
private DeleteExpungeService myDeleteExpungeService;
154161

155162
private IInstanceValidatorModule myInstanceValidator;
156163
private String myResourceName;
@@ -456,6 +463,7 @@ public DaoMethodOutcome delete(IIdType theId, DeleteConflictList theDeleteConfli
456463
.add(RequestDetails.class, theRequestDetails)
457464
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
458465
.add(TransactionDetails.class, theTransactionDetails);
466+
459467
if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts()) {
460468
theTransactionDetails.addDeferredInterceptorBroadcast(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
461469
} else {
@@ -499,14 +507,31 @@ public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteC
499507

500508
@Nonnull
501509
private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) {
502-
Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType, theRequest);
510+
RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(myResourceType);
511+
SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theUrl, resourceDef);
512+
paramMap.setLoadSynchronous(true);
513+
514+
Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequest);
515+
503516
if (resourceIds.size() > 1) {
504517
if (!myDaoConfig.isAllowMultipleDelete()) {
505518
throw new PreconditionFailedException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size()));
506519
}
507520
}
508521

509-
return deletePidList(theUrl, resourceIds, deleteConflicts, theRequest);
522+
if (paramMap.isDeleteExpunge()) {
523+
return deleteExpunge(theUrl, theRequest, resourceIds);
524+
} else {
525+
return deletePidList(theUrl, resourceIds, deleteConflicts, theRequest);
526+
}
527+
}
528+
529+
private DeleteMethodOutcome deleteExpunge(String theUrl, RequestDetails theTheRequest, Set<ResourcePersistentId> theResourceIds) {
530+
if (!myDaoConfig.isExpungeEnabled() || !myDaoConfig.isDeleteExpungeEnabled()) {
531+
throw new MethodNotAllowedException("_expunge is not enabled on this server");
532+
}
533+
534+
return myDeleteExpungeService.expungeByResourcePids(theUrl, myResourceName, new SliceImpl<>(ResourcePersistentId.toLongList(theResourceIds)), theTheRequest);
510535
}
511536

512537
@NotNull

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/MatchResourceUrlService.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
import ca.uhn.fhir.interceptor.api.Pointcut;
2828
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
2929
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
30-
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
3130
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
3231
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
3332
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
3433
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
3534
import ca.uhn.fhir.rest.api.server.RequestDetails;
35+
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
3636
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
3737
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
3838
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@@ -55,23 +55,24 @@ public class MatchResourceUrlService {
5555
private IInterceptorBroadcaster myInterceptorBroadcaster;
5656

5757
public <R extends IBaseResource> Set<ResourcePersistentId> processMatchUrl(String theMatchUrl, Class<R> theResourceType, RequestDetails theRequest) {
58-
StopWatch sw = new StopWatch();
59-
6058
RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceType);
61-
6259
SearchParameterMap paramMap = myMatchUrlService.translateMatchUrl(theMatchUrl, resourceDef);
63-
paramMap.setLoadSynchronous(true);
64-
6560
if (paramMap.isEmpty() && paramMap.getLastUpdated() == null) {
6661
throw new InvalidRequestException("Invalid match URL[" + theMatchUrl + "] - URL has no search parameters");
6762
}
63+
paramMap.setLoadSynchronous(true);
6864

65+
return search(paramMap, theResourceType, theRequest);
66+
}
67+
68+
public <R extends IBaseResource> Set<ResourcePersistentId> search(SearchParameterMap theParamMap, Class<R> theResourceType, RequestDetails theRequest) {
69+
StopWatch sw = new StopWatch();
6970
IFhirResourceDao<R> dao = myDaoRegistry.getResourceDao(theResourceType);
7071
if (dao == null) {
7172
throw new InternalErrorException("No DAO for resource type: " + theResourceType.getName());
7273
}
7374

74-
Set<ResourcePersistentId> retVal = dao.searchForIds(paramMap, theRequest);
75+
Set<ResourcePersistentId> retVal = dao.searchForIds(theParamMap, theRequest);
7576

7677
// Interceptor broadcast: JPA_PERFTRACE_INFO
7778
if (JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IEmpiLinkDao.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* #L%
2121
*/
2222

23+
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
2324
import ca.uhn.fhir.jpa.entity.EmpiLink;
2425
import org.springframework.data.jpa.repository.JpaRepository;
2526
import org.springframework.data.jpa.repository.Modifying;
@@ -32,4 +33,8 @@ public interface IEmpiLinkDao extends JpaRepository<EmpiLink, Long> {
3233
@Modifying
3334
@Query("DELETE FROM EmpiLink f WHERE myPersonPid = :pid OR myTargetPid = :pid")
3435
int deleteWithAnyReferenceToPid(@Param("pid") Long thePid);
36+
37+
@Modifying
38+
@Query("DELETE FROM EmpiLink f WHERE (myPersonPid = :pid OR myTargetPid = :pid) AND myMatchResult <> :matchResult")
39+
int deleteWithAnyReferenceToPidAndMatchResultNot(@Param("pid") Long thePid, @Param("matchResult")EmpiMatchResultEnum theMatchResult);
3540
}

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@ public interface IResourceLinkDao extends JpaRepository<ResourceLink, Long> {
3535
void deleteByResourceId(@Param("resId") Long theResourcePid);
3636

3737
@Query("SELECT t FROM ResourceLink t WHERE t.mySourceResourcePid = :resId")
38-
List<ResourceLink> findAllForResourceId(@Param("resId") Long thePatientId);
38+
List<ResourceLink> findAllForSourceResourceId(@Param("resId") Long thePatientId);
39+
40+
@Query("SELECT t FROM ResourceLink t WHERE t.myTargetResourcePid in :resIds")
41+
List<ResourceLink> findWithTargetPidIn(@Param("resIds") List<Long> thePids);
3942
}

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* #L%
2121
*/
2222

23+
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
2324
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
2425
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
2526
import org.hl7.fhir.instance.model.api.IBaseResource;
@@ -50,4 +51,13 @@ public int deleteWithAnyReferenceTo(IBaseResource theResource) {
5051
}
5152
return removed;
5253
}
54+
55+
public int deleteNonRedirectWithWithAnyReferenceTo(IBaseResource theResource) {
56+
Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement(), null);
57+
int removed = myEmpiLinkDao.deleteWithAnyReferenceToPidAndMatchResultNot(pid, EmpiMatchResultEnum.REDIRECT);
58+
if (removed > 0) {
59+
ourLog.info("Removed {} non-redirect EMPI links with references to {}", removed, theResource.getIdElement().toVersionless());
60+
}
61+
return removed;
62+
}
5363
}

0 commit comments

Comments
 (0)