#414 - Enh info: @History support for H2

This commit is contained in:
Robin Bygrave
2015-09-18 17:16:46 +12:00
parent fcb50ea13a
commit 606e1e6d3a
12 changed files with 304 additions and 7 deletions
+5 -3
View File
@@ -147,12 +147,14 @@
<version>4.7.1</version>
<scope>test</scope>
</dependency>
<!-- Provided scope so that the H2HistoryTrigger can live in Ebean core
and not require a separate module for it -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.182</version>
<scope>test</scope>
<version>1.4.189</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -55,7 +55,7 @@ public abstract class DbViewHistorySupport implements DbHistorySupport {
public String getAsOfPredicate(String asOfTableAlias, String asOfSysPeriod) {
// (sys_period_start < ? and (sys_period_end is null or sys_period_end > ?));
return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " < ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
return "(" + asOfTableAlias + "." + asOfSysPeriod + "_start" + " <= ? and (" + asOfTableAlias + "." + asOfSysPeriod + "_end" + " is null or " + asOfTableAlias + "." + asOfSysPeriod + "_end" + " > ?))";
}
/**
@@ -0,0 +1,8 @@
package com.avaje.ebean.config.dbplatform;
/**
* Runtime support for @History with H2.
*/
public class H2HistorySupport extends DbViewHistorySupport {
}
@@ -0,0 +1,127 @@
package com.avaje.ebean.config.dbplatform;
import org.h2.api.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* H2 database trigger used to populate history tables to support the @History feature.
*/
public class H2HistoryTrigger implements Trigger {
private static final Logger logger = LoggerFactory.getLogger(H2HistoryTrigger.class);
/**
* Hardcoding the column and history table suffix for now. Not sure how to get that
* configuration into the trigger instance nicely as it is instantiated by H2.
*/
private static final String SYS_PERIOD_START = "SYS_PERIOD_START";
private static final String SYS_PERIOD_END = "SYS_PERIOD_END";
private static final String HISTORY_SUFFIX = "_history";
/**
* SQL to insert into the history table.
*/
private String insertHistorySql;
/**
* Position of SYS_PERIOD_START column in the Object[].
*/
private int effectStartPosition;
/**
* Position of SYS_PERIOD_END column in the Object[].
*/
private int effectEndPosition;
@Override
public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
// get the columns for the table
ResultSet rs = conn.getMetaData().getColumns(null, schemaName, tableName, null);
// build the insert into history table SQL
StringBuilder insertSql = new StringBuilder(150);
insertSql.append("insert into ").append(tableName).append(HISTORY_SUFFIX).append(" (");
int count = 0;
List<String> columns = new ArrayList<String>();
while (rs.next()) {
if (++count > 1) {
insertSql.append(",");
}
String columnName = rs.getString("COLUMN_NAME");
if (columnName.equalsIgnoreCase(SYS_PERIOD_START)) {
this.effectStartPosition = count - 1;
} else if (columnName.equalsIgnoreCase(SYS_PERIOD_END)) {
this.effectEndPosition = count - 1;
}
insertSql.append(columnName);
columns.add(columnName);
}
insertSql.append(") values (");
for (int i = 0; i < count; i++) {
if (i > 0) {
insertSql.append(",");
}
insertSql.append("?");
}
insertSql.append(");");
this.insertHistorySql = insertSql.toString();
logger.debug("History table insert sql: {}", insertHistorySql);
}
@Override
public void fire(Connection connection, Object[] oldRow, Object[] newRow) throws SQLException {
if (oldRow != null) {
// a delete or update event
Timestamp now = new Timestamp(System.currentTimeMillis());
oldRow[effectEndPosition] = now;
if (newRow != null) {
// update event. Set the effective start timestamp to now.
newRow[effectStartPosition] = now;
}
if (logger.isDebugEnabled()) {
logger.debug("History insert: {}", Arrays.toString(oldRow));
}
insertIntoHistory(connection, oldRow);
}
}
/**
* Insert the data into the history table.
*/
private void insertIntoHistory(Connection connection, Object[] oldRow) throws SQLException {
PreparedStatement stmt = connection.prepareStatement(insertHistorySql);
try {
for (int i = 0; i < oldRow.length; i++) {
stmt.setObject(i + 1, oldRow[i]);
}
stmt.executeUpdate();
} finally {
stmt.close();
}
}
@Override
public void close() throws SQLException {
}
@Override
public void remove() throws SQLException {
}
}
@@ -15,6 +15,7 @@ public class H2Platform extends DatabasePlatform {
this.name = "h2";
this.dbEncrypt = new H2DbEncrypt();
this.platformDdl = new H2Ddl(this.dbTypeMap, dbIdentity);
this.historySupport = new H2HistorySupport();
// only support getGeneratedKeys with non-batch JDBC
// so generally use SEQUENCE instead of IDENTITY for H2
@@ -10,6 +10,7 @@ public class H2Ddl extends PlatformDdl {
public H2Ddl(DbTypeMap platformTypes, DbIdentity dbIdentity) {
super(platformTypes, dbIdentity);
this.historyDdl = new H2HistoryDdl();
}
}
@@ -0,0 +1,73 @@
package com.avaje.ebean.dbmigration.ddlgeneration.platform;
import com.avaje.ebean.config.dbplatform.H2HistoryTrigger;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlBuffer;
import com.avaje.ebean.dbmigration.ddlgeneration.DdlWrite;
import com.avaje.ebean.dbmigration.model.MTable;
import java.io.IOException;
import java.util.List;
/**
* H2 history support using DB triggers to maintain a history table.
*/
public class H2HistoryDdl extends DbTriggerBasedHistoryDdl {
private static final String TRIGGER_CLASS = H2HistoryTrigger.class.getName();
public H2HistoryDdl() {
}
@Override
protected void dropTriggers(DdlBuffer buffer, String baseTable) throws IOException {
buffer.append("drop trigger ").append(updateTriggerName(baseTable)).endOfStatement();
}
@Override
protected void createTriggers(DdlWrite writer, MTable table) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
}
@Override
protected void regenerateHistoryTriggers(DdlWrite writer, MTable table, HistoryTableUpdate update) throws IOException {
String baseTableName = table.getName();
String historyTableName = historyTableName(baseTableName);
List<String> includedColumns = includedColumnNames(table);
DdlBuffer apply = writer.applyHistory();
apply.append("-- Regenerated ").newLine();
apply.append("-- changes: ").append(update.description()).newLine();
dropTriggers(apply, baseTableName);
addCreateTrigger(apply, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
// put a reverted version into the rollback buffer
update.toRevertedColumns(includedColumns);
DdlBuffer rollback = writer.rollback();
rollback.append("-- Revert regenerated ").newLine();
rollback.append("-- revert changes: ").append(update.description()).newLine();
dropTriggers(rollback, baseTableName);
addCreateTrigger(rollback, updateTriggerName(baseTableName), baseTableName, historyTableName, includedColumns);
}
private void addCreateTrigger(DdlBuffer apply, String triggerName, String baseTable, String historyTable, List<String> includedColumns) throws IOException {
// Note that this does not take into account the historyTable name (excepts _history suffix) and
// does not take into account excluded columns (all columns included in history)
apply
.append("create trigger ").append(triggerName).append(" before update,delete on ").append(baseTable)
.append(" for each row call \"" + TRIGGER_CLASS + "\";").newLine();
}
}
@@ -35,7 +35,7 @@ public class ModelContainerApplyTest {
assertThat(foo.getComment()).isEqualTo("comment");
assertThat(foo.getTablespace()).isEqualTo("fooSpace");
assertThat(foo.getIndexTablespace()).isEqualTo("fooIndexSpace");
assertThat(foo.isWithHistory()).isEqualTo(true);
assertThat(foo.isWithHistory()).isEqualTo(false);
assertThat(foo.getColumns()).containsKeys("col1", "col3", "added_to_foo");
}
}
@@ -0,0 +1,83 @@
package com.avaje.tests.history;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.SqlQuery;
import com.avaje.ebean.SqlRow;
import com.avaje.ebean.Version;
import com.avaje.tests.model.converstation.User;
import org.junit.Test;
import java.sql.Timestamp;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestHistoryInsert extends BaseTestCase {
@Test
public void test() {
User user = new User();
user.setName("Jim");
user.setEmail("one@email.com");
Ebean.save(user);
Timestamp afterInsert = new Timestamp(System.currentTimeMillis());
List<SqlRow> history = fetchHistory(user);
assertThat(history).isEmpty();
List<Version<User>> versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(1);
user.setName("Jim v2");
Ebean.save(user);
history = fetchHistory(user);
assertThat(history).hasSize(1);
assertThat(history.get(0).getString("name")).isEqualTo("Jim");
versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(2);
assertThat(versions.get(0).getDiff()).containsKeys("name", "version", "whenModified");
user.setName("Jim v3");
user.setEmail("three@email.com");
Ebean.save(user);
history = fetchHistory(user);
assertThat(history).hasSize(2);
assertThat(history.get(1).getString("name")).isEqualTo("Jim v2");
assertThat(history.get(1).getString("email")).isEqualTo("one@email.com");
versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(3);
assertThat(versions.get(0).getDiff()).containsKeys("name", "email", "version", "whenModified");
Ebean.delete(user);
User earlyVersion = Ebean.find(User.class).setId(user.getId()).asOf(afterInsert).findUnique();
assertThat(earlyVersion.getName()).isEqualTo("Jim");
assertThat(earlyVersion.getEmail()).isEqualTo("one@email.com");
history = fetchHistory(user);
assertThat(history).hasSize(3);
assertThat(history.get(2).getString("name")).isEqualTo("Jim v3");
assertThat(history.get(2).getString("email")).isEqualTo("three@email.com");
versions = Ebean.find(User.class).setId(user.getId()).findVersions();
assertThat(versions).hasSize(3);
}
/**
* Use SqlQuery to query the history table directly.
*/
private List<SqlRow> fetchHistory(User user) {
SqlQuery sqlQuery = Ebean.createSqlQuery("select * from c_user_history where id = :id order by sys_period_start");
sqlQuery.setParameter("id", user.getId());
return sqlQuery.findList();
}
}
@@ -4,8 +4,10 @@ import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import com.avaje.ebean.annotation.History;
import com.avaje.tests.model.BaseModel;
@History
@Entity
@Table(name="c_user")
public class User extends BaseModel {
@@ -4,7 +4,7 @@
<changeSet type="apply">
<createTable name="foo" pkName="pk_foo" withHistory="true" tablespace="fooSpace" indexTablespace="fooIndexSpace" comment="comment">
<createTable name="foo" pkName="pk_foo" withHistory="false" tablespace="fooSpace" indexTablespace="fooIndexSpace" comment="comment">
<column name="col1" type="varchar(4)" notnull="true" primaryKey="true"/>
<column name="col2" type="varchar(30)" notnull="true"/>
<column name="col3" type="varchar(30)" notnull="true"/>
+1 -1
View File
@@ -86,6 +86,6 @@
<logger name="org.avaje.ebean.cache.NATKEY" level="TRACE"/>
<logger name="com.avaje.tests" level="DEBUG"/>
<logger name="com.avaje.ebean.config.dbplatform.H2HistoryTrigger" level="DEBUG"/>
</configuration>