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