001package io.ebean.config.dbplatform.h2; 002 003import org.h2.api.Trigger; 004import org.slf4j.Logger; 005import org.slf4j.LoggerFactory; 006 007import java.sql.*; 008import java.time.LocalDateTime; 009import java.util.Arrays; 010 011/** 012 * H2 database trigger used to populate history tables to support the @History feature. 013 */ 014public class H2HistoryTrigger implements Trigger { 015 016 private static final Logger log = LoggerFactory.getLogger(H2HistoryTrigger.class); 017 018 /** 019 * Hardcoding the column and history table suffix for now. Not sure how to get that 020 * configuration into the trigger instance nicely as it is instantiated by H2. 021 */ 022 private static final String SYS_PERIOD_START = "SYS_PERIOD_START"; 023 private static final String SYS_PERIOD_END = "SYS_PERIOD_END"; 024 private static final String HISTORY_SUFFIX = "_history"; 025 026 /** 027 * SQL to insert into the history table. 028 */ 029 private String insertHistorySql; 030 031 /** 032 * Position of SYS_PERIOD_START column in the Object[]. 033 */ 034 private int effectStartPosition; 035 036 /** 037 * Position of SYS_PERIOD_END column in the Object[]. 038 */ 039 private int effectEndPosition; 040 041 @Override 042 public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { 043 // get the columns for the table 044 ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null); 045 046 // build the insert into history table SQL 047 StringBuilder insertSql = new StringBuilder(150); 048 insertSql.append("insert into ").append(schemaName).append(".").append(tableName).append(HISTORY_SUFFIX).append(" ("); 049 050 int count = 0; 051 while (rs.next()) { 052 if (++count > 1) { 053 insertSql.append(","); 054 } 055 String columnName = rs.getString("COLUMN_NAME"); 056 if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) { 057 this.effectStartPosition = count - 1; 058 } else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) { 059 this.effectEndPosition = count - 1; 060 } 061 insertSql.append(columnName); 062 } 063 insertSql.append(") values ("); 064 for (int i = 0; i < count; i++) { 065 if (i > 0) { 066 insertSql.append(","); 067 } 068 insertSql.append("?"); 069 } 070 insertSql.append(");"); 071 072 this.insertHistorySql = insertSql.toString(); 073 log.debug("History table insert sql: {}", insertHistorySql); 074 } 075 076 @Override 077 public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException { 078 if (oldRow != null) { 079 // a delete or update event 080 LocalDateTime now = LocalDateTime.now(); 081 oldRow[effectEndPosition] = now; 082 if (newRow != null) { 083 // update event. Set the effective start timestamp to now. 084 newRow[effectStartPosition] = now; 085 } 086 if (log.isTraceEnabled()) { 087 log.trace("History insert: {}", Arrays.toString(oldRow)); 088 } 089 insertIntoHistory(connection, oldRow); 090 } 091 } 092 093 /** 094 * Insert the data into the history table. 095 */ 096 private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException { 097 try (PreparedStatement stmt = connection.prepareStatement(insertHistorySql)) { 098 for (int i = 0; i < oldRow.length; i++) { 099 stmt.setObject(i + 1, oldRow[i]); 100 } 101 stmt.executeUpdate(); 102 } 103 } 104 105 @Override 106 public void close() throws SQLException { 107 // do nothing 108 } 109 110 @Override 111 public void remove() throws SQLException { 112 // do nothing 113 } 114}