001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.NpmPackageEntity; 051import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; 052import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; 053import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; 054import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 055import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag; 056import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique; 057import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique; 058import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords; 059import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; 060import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber; 061import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; 062import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized; 063import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; 064import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 065import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri; 066import ca.uhn.fhir.jpa.model.entity.ResourceLink; 067import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity; 068import ca.uhn.fhir.jpa.model.entity.ResourceTable; 069import ca.uhn.fhir.jpa.model.entity.ResourceTag; 070import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity; 071import ca.uhn.fhir.jpa.model.entity.TagDefinition; 072import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 073import ca.uhn.fhir.jpa.util.MemoryCacheService; 074import ca.uhn.fhir.rest.api.server.RequestDetails; 075import ca.uhn.fhir.rest.server.provider.ProviderConstants; 076import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 077import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 078import ca.uhn.fhir.util.StopWatch; 079import jakarta.annotation.Nullable; 080import jakarta.persistence.EntityManager; 081import jakarta.persistence.PersistenceContext; 082import jakarta.persistence.PersistenceContextType; 083import jakarta.persistence.Query; 084import jakarta.persistence.TypedQuery; 085import jakarta.persistence.criteria.CriteriaBuilder; 086import jakarta.persistence.criteria.CriteriaQuery; 087import jakarta.persistence.metamodel.EntityType; 088import jakarta.persistence.metamodel.Metamodel; 089import jakarta.persistence.metamodel.SingularAttribute; 090import org.slf4j.Logger; 091import org.slf4j.LoggerFactory; 092import org.springframework.beans.factory.annotation.Autowired; 093import org.springframework.stereotype.Service; 094import org.springframework.transaction.annotation.Propagation; 095 096import java.util.List; 097import java.util.Set; 098import java.util.concurrent.atomic.AtomicInteger; 099 100@Service 101public class ExpungeEverythingService implements IExpungeEverythingService { 102 private static final Logger ourLog = LoggerFactory.getLogger(ExpungeEverythingService.class); 103 104 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 105 protected EntityManager myEntityManager; 106 107 @Autowired 108 protected IInterceptorBroadcaster myInterceptorBroadcaster; 109 110 @Autowired 111 private HapiTransactionService myTxService; 112 113 @Autowired 114 private MemoryCacheService myMemoryCacheService; 115 116 @Autowired 117 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 118 119 private int deletedResourceEntityCount; 120 121 @Override 122 public void expungeEverything(@Nullable RequestDetails theRequest) { 123 124 final AtomicInteger counter = new AtomicInteger(); 125 126 // Notify Interceptors about pre-action call 127 HookParams hooks = new HookParams() 128 .add(AtomicInteger.class, counter) 129 .add(RequestDetails.class, theRequest) 130 .addIfMatchesType(ServletRequestDetails.class, theRequest); 131 CompositeInterceptorBroadcaster.doCallHooks( 132 myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks); 133 134 ourLog.info("BEGINNING GLOBAL $expunge"); 135 Propagation propagation = Propagation.REQUIRES_NEW; 136 RequestPartitionId requestPartitionId = 137 myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation( 138 theRequest, ProviderConstants.OPERATION_EXPUNGE); 139 140 deleteAll(theRequest, propagation, requestPartitionId, counter); 141 142 purgeAllCaches(); 143 144 ourLog.info("COMPLETED GLOBAL $expunge - Deleted {} rows", counter.get()); 145 } 146 147 protected void deleteAll( 148 @Nullable RequestDetails theRequest, 149 Propagation propagation, 150 RequestPartitionId requestPartitionId, 151 AtomicInteger counter) { 152 myTxService 153 .withRequest(theRequest) 154 .withPropagation(propagation) 155 .withRequestPartitionId(requestPartitionId) 156 .execute(() -> { 157 counter.addAndGet(doExpungeEverythingQuery( 158 "UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null")); 159 }); 160 counter.addAndGet( 161 expungeEverythingByTypeWithoutPurging(theRequest, Batch2WorkChunkEntity.class, requestPartitionId)); 162 counter.addAndGet( 163 expungeEverythingByTypeWithoutPurging(theRequest, Batch2JobInstanceEntity.class, requestPartitionId)); 164 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 165 theRequest, NpmPackageVersionResourceEntity.class, requestPartitionId)); 166 counter.addAndGet( 167 expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionEntity.class, requestPartitionId)); 168 counter.addAndGet( 169 expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageEntity.class, requestPartitionId)); 170 counter.addAndGet( 171 expungeEverythingByTypeWithoutPurging(theRequest, SearchParamPresentEntity.class, requestPartitionId)); 172 counter.addAndGet( 173 expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobFileEntity.class, requestPartitionId)); 174 counter.addAndGet( 175 expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobEntity.class, requestPartitionId)); 176 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 177 theRequest, ResourceIndexedSearchParamDate.class, requestPartitionId)); 178 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 179 theRequest, ResourceIndexedSearchParamNumber.class, requestPartitionId)); 180 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 181 theRequest, ResourceIndexedSearchParamQuantity.class, requestPartitionId)); 182 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 183 theRequest, ResourceIndexedSearchParamQuantityNormalized.class, requestPartitionId)); 184 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 185 theRequest, ResourceIndexedSearchParamString.class, requestPartitionId)); 186 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 187 theRequest, ResourceIndexedSearchParamToken.class, requestPartitionId)); 188 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 189 theRequest, ResourceIndexedSearchParamUri.class, requestPartitionId)); 190 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 191 theRequest, ResourceIndexedSearchParamCoords.class, requestPartitionId)); 192 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 193 theRequest, ResourceIndexedComboStringUnique.class, requestPartitionId)); 194 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 195 theRequest, ResourceIndexedComboTokenNonUnique.class, requestPartitionId)); 196 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceLink.class, requestPartitionId)); 197 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchResult.class, requestPartitionId)); 198 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchInclude.class, requestPartitionId)); 199 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 200 theRequest, TermValueSetConceptDesignation.class, requestPartitionId)); 201 counter.addAndGet( 202 expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConcept.class, requestPartitionId)); 203 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSet.class, requestPartitionId)); 204 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 205 theRequest, TermConceptParentChildLink.class, requestPartitionId)); 206 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 207 theRequest, TermConceptMapGroupElementTarget.class, requestPartitionId)); 208 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 209 theRequest, TermConceptMapGroupElement.class, requestPartitionId)); 210 counter.addAndGet( 211 expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroup.class, requestPartitionId)); 212 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMap.class, requestPartitionId)); 213 counter.addAndGet( 214 expungeEverythingByTypeWithoutPurging(theRequest, TermConceptProperty.class, requestPartitionId)); 215 counter.addAndGet( 216 expungeEverythingByTypeWithoutPurging(theRequest, TermConceptDesignation.class, requestPartitionId)); 217 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConcept.class, requestPartitionId)); 218 myTxService 219 .withRequest(theRequest) 220 .withPropagation(propagation) 221 .withRequestPartitionId(requestPartitionId) 222 .execute(() -> { 223 for (TermCodeSystem next : myEntityManager 224 .createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class) 225 .getResultList()) { 226 next.setCurrentVersion(null); 227 myEntityManager.merge(next); 228 } 229 }); 230 counter.addAndGet( 231 expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystemVersion.class, requestPartitionId)); 232 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystem.class, requestPartitionId)); 233 counter.addAndGet( 234 expungeEverythingByTypeWithoutPurging(theRequest, SubscriptionTable.class, requestPartitionId)); 235 counter.addAndGet( 236 expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTag.class, requestPartitionId)); 237 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTag.class, requestPartitionId)); 238 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TagDefinition.class, requestPartitionId)); 239 counter.addAndGet(expungeEverythingByTypeWithoutPurging( 240 theRequest, ResourceHistoryProvenanceEntity.class, requestPartitionId)); 241 counter.addAndGet( 242 expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTable.class, requestPartitionId)); 243 counter.addAndGet( 244 expungeEverythingByTypeWithoutPurging(theRequest, ResourceSearchUrlEntity.class, requestPartitionId)); 245 246 int counterBefore = counter.get(); 247 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTable.class, requestPartitionId)); 248 counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, PartitionEntity.class, requestPartitionId)); 249 250 deletedResourceEntityCount = counter.get() - counterBefore; 251 252 myTxService 253 .withRequest(theRequest) 254 .withPropagation(propagation) 255 .withRequestPartitionId(requestPartitionId) 256 .execute(() -> { 257 counter.addAndGet(doExpungeEverythingQuery("DELETE from " + Search.class.getSimpleName() + " d")); 258 }); 259 } 260 261 @Override 262 public int getExpungeDeletedEntityCount() { 263 return deletedResourceEntityCount; 264 } 265 266 private void purgeAllCaches() { 267 myMemoryCacheService.invalidateAllCaches(); 268 } 269 270 protected <T> int expungeEverythingByTypeWithoutPurging( 271 RequestDetails theRequest, Class<T> theEntityType, RequestPartitionId theRequestPartitionId) { 272 HapiTransactionService.noTransactionAllowed(); 273 274 int outcome = 0; 275 while (true) { 276 StopWatch sw = new StopWatch(); 277 278 int count = myTxService 279 .withRequest(theRequest) 280 .withPropagation(Propagation.REQUIRES_NEW) 281 .withRequestPartitionId(theRequestPartitionId) 282 .execute(() -> { 283 284 /* 285 * This method uses a nice efficient mechanism where we figure out the PID datatype 286 * and load only the PIDs and delete by PID for all resource types except ResourceTable. 287 * We delete ResourceTable using the entitymanager so that Hibernate Search knows to 288 * delete the corresponding records it manages in ElasticSearch. See 289 * FhirResourceDaoR4SearchWithElasticSearchIT for a test that fails without the 290 * block below. 291 */ 292 if (ResourceTable.class.equals(theEntityType)) { 293 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 294 CriteriaQuery<?> cq = cb.createQuery(theEntityType); 295 cq.from(theEntityType); 296 TypedQuery<?> query = myEntityManager.createQuery(cq); 297 query.setMaxResults(800); 298 List<?> results = query.getResultList(); 299 for (Object result : results) { 300 myEntityManager.remove(result); 301 } 302 return results.size(); 303 } 304 305 Metamodel metamodel = myEntityManager.getMetamodel(); 306 EntityType<T> entity = metamodel.entity(theEntityType); 307 Set<SingularAttribute<? super T, ?>> singularAttributes = entity.getSingularAttributes(); 308 String idProperty = null; 309 for (SingularAttribute<? super T, ?> singularAttribute : singularAttributes) { 310 if (singularAttribute.isId()) { 311 idProperty = singularAttribute.getName(); 312 break; 313 } 314 } 315 316 Query nativeQuery = myEntityManager.createQuery( 317 "SELECT " + idProperty + " FROM " + theEntityType.getSimpleName()); 318 nativeQuery.setMaxResults(800); 319 List pids = nativeQuery.getResultList(); 320 321 nativeQuery = myEntityManager.createQuery("DELETE FROM " + theEntityType.getSimpleName() 322 + " WHERE " + idProperty + " IN (:pids)"); 323 nativeQuery.setParameter("pids", pids); 324 nativeQuery.executeUpdate(); 325 return pids.size(); 326 }); 327 328 outcome += count; 329 if (count == 0) { 330 break; 331 } 332 333 ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw); 334 } 335 return outcome; 336 } 337 338 @Override 339 public int expungeEverythingByType(Class<?> theEntityType) { 340 int result = expungeEverythingByTypeWithoutPurging(null, theEntityType, RequestPartitionId.allPartitions()); 341 purgeAllCaches(); 342 return result; 343 } 344 345 @Override 346 public int expungeEverythingMdmLinks() { 347 return expungeEverythingByType(MdmLink.class); 348 } 349 350 private int doExpungeEverythingQuery(String theQuery) { 351 StopWatch sw = new StopWatch(); 352 int outcome = myEntityManager.createQuery(theQuery).executeUpdate(); 353 ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw, theQuery); 354 return outcome; 355 } 356}