001package io.ebean.config.dbplatform; 002 003import io.ebean.BackgroundExecutor; 004import io.ebean.Transaction; 005import io.ebean.util.JdbcClose; 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009import javax.persistence.PersistenceException; 010import javax.sql.DataSource; 011import java.sql.Connection; 012import java.sql.PreparedStatement; 013import java.sql.ResultSet; 014import java.sql.SQLException; 015import java.util.Collections; 016import java.util.List; 017import java.util.NavigableSet; 018import java.util.TreeSet; 019import java.util.concurrent.atomic.AtomicBoolean; 020import java.util.concurrent.locks.ReentrantLock; 021 022/** 023 * Database sequence based IdGenerator. 024 */ 025public abstract class SequenceIdGenerator implements PlatformIdGenerator { 026 027 protected static final Logger logger = LoggerFactory.getLogger("io.ebean.SEQ"); 028 029 private final ReentrantLock lock = new ReentrantLock(); 030 031 /** 032 * The actual sequence name. 033 */ 034 protected final String seqName; 035 036 protected final DataSource dataSource; 037 038 protected final BackgroundExecutor backgroundExecutor; 039 040 protected final NavigableSet<Long> idList = new TreeSet<>(); 041 042 protected final int allocationSize; 043 044 protected AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false); 045 046 /** 047 * Construct given a dataSource and sql to return the next sequence value. 048 */ 049 protected SequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int allocationSize) { 050 this.backgroundExecutor = be; 051 this.dataSource = ds; 052 this.seqName = seqName; 053 this.allocationSize = allocationSize; 054 } 055 056 public abstract String getSql(int batchSize); 057 058 /** 059 * Returns the sequence name. 060 */ 061 @Override 062 public String getName() { 063 return seqName; 064 } 065 066 /** 067 * Returns true. 068 */ 069 @Override 070 public boolean isDbSequence() { 071 return true; 072 } 073 074 /** 075 * If allocateSize is large load some sequences in a background thread. 076 * <p> 077 * For example, when inserting a bean with a cascade on a OneToMany with many 078 * beans Ebean can call this to ensure . 079 * </p> 080 */ 081 @Override 082 public void preAllocateIds(int requestSize) { 083 // do nothing by default 084 } 085 086 /** 087 * Return the next Id. 088 * <p> 089 * If a Transaction has been passed in use the Connection from it. 090 * </p> 091 */ 092 @Override 093 public Object nextId(Transaction t) { 094 lock.lock(); 095 try { 096 int size = idList.size(); 097 if (size > 0) { 098 maybeLoadMoreInBackground(size); 099 } else { 100 loadMore(allocationSize); 101 } 102 return idList.pollFirst(); 103 } finally { 104 lock.unlock(); 105 } 106 } 107 108 private void maybeLoadMoreInBackground(int currentSize) { 109 if (allocationSize > 1) { 110 if (currentSize <= allocationSize / 2) { 111 loadInBackground(allocationSize); 112 } 113 } 114 } 115 116 private void loadMore(int requestSize) { 117 List<Long> newIds = getMoreIds(requestSize); 118 lock.lock(); 119 try { 120 idList.addAll(newIds); 121 } finally { 122 lock.unlock(); 123 } 124 } 125 126 /** 127 * Load another batch of Id's using a background thread. 128 */ 129 protected void loadInBackground(final int requestSize) { 130 if (currentlyBackgroundLoading.get()) { 131 // skip as already background loading 132 logger.debug("... skip background sequence load (another load in progress)"); 133 return; 134 } 135 currentlyBackgroundLoading.set(true); 136 backgroundExecutor.execute(() -> { 137 loadMore(requestSize); 138 currentlyBackgroundLoading.set(false); 139 }); 140 } 141 142 /** 143 * Read the resultSet returning the list of Id values. 144 */ 145 protected abstract List<Long> readIds(ResultSet resultSet, int loadSize) throws SQLException; 146 147 /** 148 * Get more Id's by executing a query and reading the Id's returned. 149 */ 150 protected List<Long> getMoreIds(int requestSize) { 151 152 String sql = getSql(requestSize); 153 154 Connection connection = null; 155 PreparedStatement statement = null; 156 ResultSet resultSet = null; 157 try { 158 connection = dataSource.getConnection(); 159 160 statement = connection.prepareStatement(sql); 161 resultSet = statement.executeQuery(); 162 163 List<Long> newIds = readIds(resultSet, requestSize); 164 if (logger.isTraceEnabled()) { 165 logger.trace("seq:{} loaded:{} sql:{}", seqName, newIds.size(), sql); 166 } 167 if (newIds.isEmpty()) { 168 throw new PersistenceException("Always expecting more than 1 row from " + sql); 169 } 170 171 return newIds; 172 173 } catch (SQLException e) { 174 if (e.getMessage().contains("Database is already closed")) { 175 String msg = "Error getting SEQ when DB shutting down " + e.getMessage(); 176 logger.error(msg); 177 System.out.println(msg); 178 return Collections.emptyList(); 179 } else { 180 throw new PersistenceException("Error getting sequence nextval", e); 181 } 182 } finally { 183 closeResources(connection, statement, resultSet); 184 } 185 } 186 187 /** 188 * Close the JDBC resources. 189 */ 190 private void closeResources(Connection connection, PreparedStatement statement, ResultSet resultSet) { 191 JdbcClose.close(resultSet); 192 JdbcClose.close(statement); 193 JdbcClose.close(connection); 194 } 195 196}