
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao.expunge; 021 022import ca.uhn.fhir.interceptor.api.HookParams; 023import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 024import ca.uhn.fhir.interceptor.api.Pointcut; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 027import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity; 028import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity; 029import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; 030import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity; 031import ca.uhn.fhir.jpa.entity.MdmLink; 032import ca.uhn.fhir.jpa.entity.PartitionEntity; 033import ca.uhn.fhir.jpa.entity.Search; 034import ca.uhn.fhir.jpa.entity.SearchInclude; 035import ca.uhn.fhir.jpa.entity.SearchResult; 036import ca.uhn.fhir.jpa.entity.SubscriptionTable; 037import ca.uhn.fhir.jpa.entity.TermCodeSystem; 038import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; 039import ca.uhn.fhir.jpa.entity.TermConcept; 040import ca.uhn.fhir.jpa.entity.TermConceptDesignation; 041import ca.uhn.fhir.jpa.entity.TermConceptMap; 042import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; 043import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; 044import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; 045import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink; 046import ca.uhn.fhir.jpa.entity.TermConceptProperty; 047import ca.uhn.fhir.jpa.entity.TermValueSet; 048import ca.uhn.fhir.jpa.entity.TermValueSetConcept; 049import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation; 050import ca.uhn.fhir.jpa.model.entity.IndexedSearchParamIdentity; 051import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity; 052import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; 053import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; 054import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; 055import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 056import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; 057import ca.uhn.fhir.jpa.model.entity.ResourceIdentifierPatientUniqueEntity; 058import ca.uhn.fhir.jpa.model.entity.ResourceIdentifierSystemEntity; 059import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; 060import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique; 061import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; 062import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; 063import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; 064import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; 065import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; 066import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; 067import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 068import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; 069import ca.uhn.fhir.jpa.model.entity.ResourceLink; 070import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity; 071import ca.uhn.fhir.jpa.model.entity.ResourceTable; 072import ca.uhn.fhir.jpa.model.entity.ResourceTag; 073import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity; 074import ca.uhn.fhir.jpa.model.entity.TagDefinition; 075import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 076import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 077import ca.uhn.fhir.jpa.util.MemoryCacheService; 078import ca.uhn.fhir.rest.api.server.RequestDetails; 079import ca.uhn.fhir.rest.server.provider.ProviderConstants; 080import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 081import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 082import ca.uhn.fhir.util.StopWatch; 083import jakarta.annotation.Nullable; 084import jakarta.persistence.EntityManager; 085import jakarta.persistence.PersistenceContext; 086import jakarta.persistence.PersistenceContextType; 087import jakarta.persistence.Query; 088import jakarta.persistence.TypedQuery; 089import jakarta.persistence.criteria.CriteriaBuilder; 090import jakarta.persistence.criteria.CriteriaQuery; 091import jakarta.persistence.metamodel.EntityType; 092import jakarta.persistence.metamodel.Metamodel; 093import jakarta.persistence.metamodel.SingularAttribute; 094import org.slf4j.Logger; 095import org.slf4j.LoggerFactory; 096import org.springframework.beans.factory.annotation.Autowired; 097import org.springframework.stereotype.Service; 098import org.springframework.transaction.annotation.Propagation; 099import org.springframework.util.comparator.Comparators; 100 101import java.util.ArrayList; 102import java.util.Iterator; 103import java.util.List; 104import java.util.Set; 105import java.util.concurrent.atomic.AtomicInteger; 106 107@Service 108public class ExpungeEverythingService implements IExpungeEverythingService { 109 private static final Logger ourLog = LoggerFactory.getLogger(ExpungeEverythingService.class); 110 111 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 112 protected EntityManager myEntityManager; 113 114 @Autowired 115 protected IInterceptorBroadcaster myInterceptorBroadcaster; 116 117 @Autowired 118 private HapiTransactionService myTxService; 119 120 @Autowired 121 private MemoryCacheService myMemoryCacheService; 122 123 @Autowired 124 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 125 126 private int deletedResourceEntityCount; 127 128 @Override 129 public void expungeEverything(@Nullable RequestDetails theRequest) { 130 131 final AtomicInteger counter = new AtomicInteger(); 132 133 // Notify Interceptors about pre-action call 134 IInterceptorBroadcaster compositeBroadcaster = 135 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 136 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING)) { 137 HookParams hooks = new HookParams() 138 .add(AtomicInteger.class, counter) 139 .add(RequestDetails.class, theRequest) 140 .addIfMatchesType(ServletRequestDetails.class, theRequest); 141 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks); 142 } 143 144 ourLog.info("BEGINNING GLOBAL $expunge"); 145 Propagation propagation = Propagation.REQUIRES_NEW; 146 RequestPartitionId requestPartitionId = 147 myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( 148 theRequest, ProviderConstants.OPERATION_EXPUNGE); 149 150 deleteAll(theRequest, propagation, requestPartitionId, counter); 151 152 purgeAllCaches(); 153 154 ourLog.info("COMPLETED GLOBAL $expunge - Deleted {} rows", counter.get()); 155 } 156 157 protected void deleteAll( 158 @Nullable RequestDetails theRequest, 159 Propagation propagation, 160 RequestPartitionId requestPartitionId, 161 AtomicInteger counter) { 162 myTxService 163 .withRequest(theRequest) 164 .withPropagation(propagation) 165 .withRequestPartitionId(requestPartitionId) 166 .execute(() -> { 167 counter.addAndGet(doExpungeEverythingQuery( 168 "UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null")); 169 }); 170 counter.addAndGet( 171 expungeEverythingByTypeWithoutPurging(theRequest, Batch2WorkChunkEntity.class, requestPartitionId)); 172 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 173 theRequest, ResourceIdentifierPatientUniqueEntity.class, requestPartitionId)); 174 counter.addAndGet( 175 expungeEverythingByTypeWithoutPurging(theRequest, Batch2JobInstanceEntity.class, requestPartitionId)); 176 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 177 theRequest, NpmPackageVersionResourceEntity.class, requestPartitionId)); 178 counter.addAndGet( 179 expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionEntity.class, requestPartitionId)); 180 counter.addAndGet( 181 expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageEntity.class, requestPartitionId)); 182 counter.addAndGet( 183 expungeEverythingByTypeWithoutPurging(theRequest, SearchParamPresentEntity.class, requestPartitionId)); 184 counter.addAndGet( 185 expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobFileEntity.class, requestPartitionId)); 186 counter.addAndGet( 187 expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobEntity.class, requestPartitionId)); 188 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 189 theRequest, ResourceIndexedSearchParamDate.class, requestPartitionId)); 190 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 191 theRequest, ResourceIndexedSearchParamNumber.class, requestPartitionId)); 192 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 193 theRequest, ResourceIndexedSearchParamQuantity.class, requestPartitionId)); 194 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 195 theRequest, ResourceIndexedSearchParamQuantityNormalized.class, requestPartitionId)); 196 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 197 theRequest, ResourceIndexedSearchParamString.class, requestPartitionId)); 198 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 199 theRequest, ResourceIndexedSearchParamToken.class, requestPartitionId)); 200 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 201 theRequest, ResourceIndexedSearchParamUri.class, requestPartitionId)); 202 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 203 theRequest, ResourceIndexedSearchParamCoords.class, requestPartitionId)); 204 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 205 theRequest, ResourceIndexedComboStringUnique.class, requestPartitionId)); 206 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 207 theRequest, ResourceIndexedComboTokenNonUnique.class, requestPartitionId)); 208 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 209 theRequest, IndexedSearchParamIdentity.class, requestPartitionId)); 210 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceLink.class, requestPartitionId)); 211 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchResult.class, requestPartitionId)); 212 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchInclude.class, requestPartitionId)); 213 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 214 theRequest, TermValueSetConceptDesignation.class, requestPartitionId)); 215 counter.addAndGet( 216 expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConcept.class, requestPartitionId)); 217 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSet.class, requestPartitionId)); 218 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 219 theRequest, TermConceptParentChildLink.class, requestPartitionId)); 220 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 221 theRequest, TermConceptMapGroupElementTarget.class, requestPartitionId)); 222 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 223 theRequest, TermConceptMapGroupElement.class, requestPartitionId)); 224 counter.addAndGet( 225 expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroup.class, requestPartitionId)); 226 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMap.class, requestPartitionId)); 227 counter.addAndGet( 228 expungeEverythingByTypeWithoutPurging(theRequest, TermConceptProperty.class, requestPartitionId)); 229 counter.addAndGet( 230 expungeEverythingByTypeWithoutPurging(theRequest, TermConceptDesignation.class, requestPartitionId)); 231 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConcept.class, requestPartitionId)); 232 myTxService 233 .withRequest(theRequest) 234 .withPropagation(propagation) 235 .withRequestPartitionId(requestPartitionId) 236 .execute(() -> { 237 for (TermCodeSystem next : myEntityManager 238 .createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class) 239 .getResultList()) { 240 next.setCurrentVersion(null); 241 myEntityManager.merge(next); 242 } 243 }); 244 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 245 theRequest, ResourceIdentifierSystemEntity.class, requestPartitionId)); 246 counter.addAndGet( 247 expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystemVersion.class, requestPartitionId)); 248 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystem.class, requestPartitionId)); 249 counter.addAndGet( 250 expungeEverythingByTypeWithoutPurging(theRequest, SubscriptionTable.class, requestPartitionId)); 251 counter.addAndGet( 252 expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTag.class, requestPartitionId)); 253 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTag.class, requestPartitionId)); 254 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TagDefinition.class, requestPartitionId)); 255 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 256 theRequest, ResourceHistoryProvenanceEntity.class, requestPartitionId)); 257 counter.addAndGet( 258 expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTable.class, requestPartitionId)); 259 counter.addAndGet( 260 expungeEverythingByTypeWithoutPurging(theRequest, ResourceSearchUrlEntity.class, requestPartitionId)); 261 262 int counterBefore = counter.get(); 263 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTable.class, requestPartitionId)); 264 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, PartitionEntity.class, requestPartitionId)); 265 266 deletedResourceEntityCount = counter.get() - counterBefore; 267 268 myTxService 269 .withRequest(theRequest) 270 .withPropagation(propagation) 271 .withRequestPartitionId(requestPartitionId) 272 .execute(() -> { 273 counter.addAndGet(doExpungeEverythingQuery("DELETE from " + Search.class.getSimpleName() + " d")); 274 }); 275 } 276 277 @Override 278 public int getExpungeDeletedEntityCount() { 279 return deletedResourceEntityCount; 280 } 281 282 private void purgeAllCaches() { 283 myMemoryCacheService.invalidateAllCaches(); 284 } 285 286 protected <T> int expungeEverythingByTypeWithoutPurging( 287 RequestDetails theRequest, Class<T> theEntityType, RequestPartitionId theRequestPartitionId) { 288 HapiTransactionService.noTransactionAllowed(); 289 290 int outcome = 0; 291 while (true) { 292 StopWatch sw = new StopWatch(); 293 294 int count = myTxService 295 .withRequest(theRequest) 296 .withPropagation(Propagation.REQUIRES_NEW) 297 .withRequestPartitionId(theRequestPartitionId) 298 .execute(() -> { 299 300 /* 301 * This method uses a nice efficient mechanism where we figure out the PID datatype 302 * and load only the PIDs and delete by PID for all resource types except ResourceTable. 303 * We delete ResourceTable using the entitymanager so that Hibernate Search knows to 304 * delete the corresponding records it manages in ElasticSearch. See 305 * FhirResourceDaoR4SearchWithElasticSearchIT for a test that fails without the 306 * block below. 307 */ 308 if (ResourceTable.class.equals(theEntityType)) { 309 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 310 CriteriaQuery<?> cq = cb.createQuery(theEntityType); 311 cq.from(theEntityType); 312 TypedQuery<?> query = myEntityManager.createQuery(cq); 313 query.setMaxResults(800); 314 List<?> results = query.getResultList(); 315 for (Object result : results) { 316 myEntityManager.remove(result); 317 } 318 return results.size(); 319 } 320 321 Metamodel metamodel = myEntityManager.getMetamodel(); 322 EntityType<T> entity = metamodel.entity(theEntityType); 323 Set<SingularAttribute<? super T, ?>> singularAttributes = entity.getSingularAttributes(); 324 List<String> idProperty = new ArrayList<>(); 325 for (SingularAttribute<? super T, ?> singularAttribute : singularAttributes) { 326 if (singularAttribute.isId()) { 327 idProperty.add(singularAttribute.getName()); 328 } 329 } 330 idProperty.sort(Comparators.comparable()); 331 String idPropertyNames = String.join(",", idProperty); 332 333 Query nativeQuery = myEntityManager.createQuery( 334 "SELECT (" + idPropertyNames + ") FROM " + theEntityType.getSimpleName()); 335 336 // Each ID is 2 parameters in DB partition mode, so this 337 // is the maximum we should allow 338 nativeQuery.setMaxResults(SearchBuilder.getMaximumPageSize() / 2); 339 340 List pids = nativeQuery.getResultList(); 341 if (pids.isEmpty()) { 342 return 0; 343 } 344 345 StringBuilder deleteBuilder = new StringBuilder(); 346 deleteBuilder.append("DELETE FROM "); 347 deleteBuilder.append(theEntityType.getSimpleName()); 348 deleteBuilder.append(" WHERE ("); 349 deleteBuilder.append(idPropertyNames); 350 deleteBuilder.append(") IN "); 351 if (idProperty.size() > 1) { 352 deleteBuilder.append('('); 353 for (Iterator<Object> iter = pids.iterator(); iter.hasNext(); ) { 354 Object[] pid = (Object[]) iter.next(); 355 deleteBuilder.append('('); 356 for (int i = 0; i < pid.length; i++) { 357 if (i > 0) { 358 deleteBuilder.append(','); 359 } 360 deleteBuilder.append(pid[i]); 361 } 362 deleteBuilder.append(')'); 363 if (iter.hasNext()) { 364 deleteBuilder.append(','); 365 } 366 } 367 deleteBuilder.append(')'); 368 } else { 369 deleteBuilder.append("(:pids)"); 370 } 371 String deleteSql = deleteBuilder.toString(); 372 nativeQuery = myEntityManager.createQuery(deleteSql); 373 if (idProperty.size() == 1) { 374 nativeQuery.setParameter("pids", pids); 375 } 376 nativeQuery.executeUpdate(); 377 return pids.size(); 378 }); 379 380 outcome += count; 381 if (count == 0) { 382 break; 383 } 384 385 ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw); 386 } 387 return outcome; 388 } 389 390 @Override 391 public int expungeEverythingByType(Class<?> theEntityType) { 392 int result = expungeEverythingByTypeWithoutPurging(null, theEntityType, RequestPartitionId.allPartitions()); 393 purgeAllCaches(); 394 return result; 395 } 396 397 @Override 398 public int expungeEverythingMdmLinks() { 399 return expungeEverythingByType(MdmLink.class); 400 } 401 402 private int doExpungeEverythingQuery(String theQuery) { 403 StopWatch sw = new StopWatch(); 404 int outcome = myEntityManager.createQuery(theQuery).executeUpdate(); 405 ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw, theQuery); 406 return outcome; 407 } 408}