
001/*- 002 * #%L 003 * HAPI FHIR JPA Model 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.model.entity; 021 022import ca.uhn.fhir.jpa.model.dao.JpaPid; 023import jakarta.persistence.Column; 024import jakarta.persistence.EmbeddedId; 025import jakarta.persistence.Entity; 026import jakarta.persistence.Index; 027import jakarta.persistence.Table; 028import jakarta.persistence.Temporal; 029import jakarta.persistence.TemporalType; 030import org.apache.commons.lang3.builder.ToStringBuilder; 031import org.apache.commons.lang3.builder.ToStringStyle; 032 033import java.time.LocalDate; 034import java.util.Date; 035import java.util.Optional; 036 037/** 038 * This entity is used to enforce uniqueness on a given search URL being 039 * used as a conditional operation URL, e.g. a conditional create or a 040 * conditional update. When we perform a conditional operation that is 041 * creating a new resource, we store an entity with the conditional URL 042 * in this table. The URL is the PK of the table, so the database 043 * ensures that two concurrent threads don't accidentally create two 044 * resources with the same conditional URL. 045 * <p> 046 * Note that this entity is partitioned, but does not always respect the 047 * partition ID of the parent resource entity (it may just use a 048 * hardcoded partition ID of -1 depending on configuration). As a result 049 * we don't have a FK relationship. This table only contains short-lived 050 * rows that get cleaned up and purged periodically, so it should never 051 * grow terribly large anyhow. 052 */ 053@Entity 054@Table( 055 name = ResourceSearchUrlEntity.TABLE_NAME, 056 indexes = { 057 @Index(name = "IDX_RESSEARCHURL_RES", columnList = "RES_ID"), 058 @Index(name = "IDX_RESSEARCHURL_TIME", columnList = "CREATED_TIME") 059 }) 060public class ResourceSearchUrlEntity { 061 062 public static final String RES_SEARCH_URL_COLUMN_NAME = "RES_SEARCH_URL"; 063 public static final String PARTITION_ID = "PARTITION_ID"; 064 public static final String TABLE_NAME = "HFJ_RES_SEARCH_URL"; 065 066 @EmbeddedId 067 private ResourceSearchUrlEntityPK myPk; 068 069 /* 070 * Note: We previously had a foreign key here, but it's just not possible for this to still 071 * work with partition IDs in the PKs since non-partitioned mode currently depends on the 072 * partition ID being a part of the PK and it necessarily has to be stripped out if we're 073 * stripping out others. So we'll leave this without a FK relationship, which does increase 074 * the possibility of dangling records in this table but that's probably an ok compromise. 075 * 076 * Ultimately records in this table get cleaned up based on their CREATED_TIME anyhow, so 077 * it's really not a big deal to not have a FK relationship here. 078 */ 079 080 @Column(name = "RES_ID", updatable = false, nullable = false, insertable = true) 081 private Long myResourcePid; 082 083 @Column(name = "PARTITION_ID", updatable = false, nullable = false, insertable = false) 084 private Integer myPartitionIdValue; 085 086 @Column(name = "PARTITION_DATE", nullable = true, insertable = true, updatable = false) 087 private LocalDate myPartitionDate; 088 089 @Column(name = "CREATED_TIME", nullable = false) 090 @Temporal(TemporalType.TIMESTAMP) 091 private Date myCreatedTime; 092 093 public static ResourceSearchUrlEntity from( 094 String theUrl, ResourceTable theResourceTable, boolean theSearchUrlDuplicateAcrossPartitionsEnabled) { 095 096 return new ResourceSearchUrlEntity() 097 .setPk(ResourceSearchUrlEntityPK.from( 098 theUrl, theResourceTable, theSearchUrlDuplicateAcrossPartitionsEnabled)) 099 .setPartitionDate(Optional.ofNullable(theResourceTable.getPartitionId()) 100 .map(PartitionablePartitionId::getPartitionDate) 101 .orElse(null)) 102 .setResourceTable(theResourceTable) 103 .setCreatedTime(new Date()); 104 } 105 106 public ResourceSearchUrlEntityPK getPk() { 107 return myPk; 108 } 109 110 public ResourceSearchUrlEntity setPk(ResourceSearchUrlEntityPK thePk) { 111 myPk = thePk; 112 return this; 113 } 114 115 public JpaPid getResourcePid() { 116 return JpaPid.fromId(myResourcePid, myPartitionIdValue); 117 } 118 119 public ResourceSearchUrlEntity setResourcePid(Long theResourcePid) { 120 myResourcePid = theResourcePid; 121 return this; 122 } 123 124 public ResourceSearchUrlEntity setResourceTable(ResourceTable theResourceTable) { 125 this.myResourcePid = theResourceTable.getId().getId(); 126 this.myPartitionIdValue = theResourceTable.getPartitionId().getPartitionId(); 127 return this; 128 } 129 130 public Date getCreatedTime() { 131 return myCreatedTime; 132 } 133 134 public ResourceSearchUrlEntity setCreatedTime(Date theCreatedTime) { 135 myCreatedTime = theCreatedTime; 136 return this; 137 } 138 139 public String getSearchUrl() { 140 return myPk.getSearchUrl(); 141 } 142 143 public Integer getPartitionId() { 144 return myPk.getPartitionId(); 145 } 146 147 public LocalDate getPartitionDate() { 148 return myPartitionDate; 149 } 150 151 public ResourceSearchUrlEntity setPartitionDate(LocalDate thePartitionDate) { 152 myPartitionDate = thePartitionDate; 153 return this; 154 } 155 156 @Override 157 public String toString() { 158 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 159 .append("searchUrl", getPk().getSearchUrl()) 160 .append("partitionId", myPartitionIdValue) 161 .append("resourcePid", myResourcePid) 162 .toString(); 163 } 164}