Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement batching statements in Hibernate UPDATE mode #174

Merged
merged 2 commits into from Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -22,6 +22,7 @@
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.hql.spi.id.inline.InlineIdsOrClauseBulkIdStrategy;
import org.hibernate.service.spi.ServiceContributor;
import org.hibernate.tool.hbm2ddl.UniqueConstraintSchemaUpdateStrategy;

/**
* An implementation of a Hibernate {@link ServiceContributor} which provides custom settings
Expand All @@ -46,6 +47,10 @@ public void contribute(StandardServiceRegistryBuilder serviceRegistryBuilder) {
.applySetting("hibernate.connection.userAgent", HIBERNATE_API_CLIENT_LIB_TOKEN)
// The custom Hibernate schema management tool for Spanner.
.applySetting("hibernate.schema_management_tool", SCHEMA_MANAGEMENT_TOOL)
// Create a unique index for a table if it does not already exist when in UPDATE mode.
.applySetting(
"hibernate.schema_update.unique_constraint_strategy",
UniqueConstraintSchemaUpdateStrategy.RECREATE_QUIETLY)
meltsufin marked this conversation as resolved.
Show resolved Hide resolved
// Allows entities to be used with InheritanceType.JOINED in Spanner.
.applySetting("hibernate.hql.bulk_id_strategy", InlineIdsOrClauseBulkIdStrategy.INSTANCE);
}
Expand Down
Expand Up @@ -21,13 +21,20 @@
import com.google.cloud.spanner.hibernate.SpannerDialect;
import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject;
import org.hibernate.dialect.Dialect;
import org.hibernate.tool.schema.Action;

/**
* Custom {@link AuxiliaryDatabaseObject} which generates the RUN BATCH statement.
*/
public class RunBatchDdl implements AuxiliaryDatabaseObject {
private static final long serialVersionUID = 1L;

private final Action schemaAction;

public RunBatchDdl(Action schemaAction) {
this.schemaAction = schemaAction;
}

@Override
public String getExportIdentifier() {
return "RUN_BATCH_DDL";
Expand All @@ -40,7 +47,7 @@ public boolean appliesToDialect(Dialect dialect) {

@Override
public boolean beforeTablesOnCreation() {
return false;
return schemaAction == Action.UPDATE;
}

@Override
Expand All @@ -50,6 +57,10 @@ public String[] sqlCreateStrings(Dialect dialect) {

@Override
public String[] sqlDropStrings(Dialect dialect) {
return new String[] {"RUN BATCH"};
if (schemaAction == Action.UPDATE) {
return new String[]{};
meltsufin marked this conversation as resolved.
Show resolved Hide resolved
} else {
return new String[]{"RUN BATCH"};
}
}
}
Expand Up @@ -48,8 +48,8 @@ public void doCreation(
TargetDescriptor targetDescriptor) {

// Add auxiliary database objects to batch DDL statements
metadata.getDatabase().addAuxiliaryDatabaseObject(new StartBatchDdl());
metadata.getDatabase().addAuxiliaryDatabaseObject(new RunBatchDdl());
metadata.getDatabase().addAuxiliaryDatabaseObject(new StartBatchDdl(Action.CREATE));
metadata.getDatabase().addAuxiliaryDatabaseObject(new RunBatchDdl(Action.CREATE));

// Initialize exporters with interleave dependencies so tables are created in the right order.
tool.getSpannerTableExporter(options).initializeTableExporter(metadata, Action.CREATE);
Expand Down
Expand Up @@ -49,8 +49,8 @@ public void doDrop(
TargetDescriptor targetDescriptor) {

// Initialize auxiliary database objects to enable DDL statement batching.
metadata.getDatabase().addAuxiliaryDatabaseObject(new StartBatchDdl());
metadata.getDatabase().addAuxiliaryDatabaseObject(new RunBatchDdl());
metadata.getDatabase().addAuxiliaryDatabaseObject(new StartBatchDdl(Action.DROP));
metadata.getDatabase().addAuxiliaryDatabaseObject(new RunBatchDdl(Action.DROP));

// Initialize exporters with drop table dependencies so tables are dropped in the right order.
tool.getSpannerTableExporter(options).initializeTableExporter(metadata, Action.DROP);
Expand Down
Expand Up @@ -44,7 +44,11 @@ public SpannerSchemaMigrator(SpannerSchemaManagementTool tool, SchemaMigrator sc
public void doMigration(
Metadata metadata, ExecutionOptions options, TargetDescriptor targetDescriptor) {

tool.getSpannerTableExporter(options).initializeTableExporter(metadata, Action.CREATE);
// Add auxiliary database objects to batch DDL statements
metadata.getDatabase().addAuxiliaryDatabaseObject(new StartBatchDdl(Action.UPDATE));
metadata.getDatabase().addAuxiliaryDatabaseObject(new RunBatchDdl(Action.UPDATE));

tool.getSpannerTableExporter(options).initializeTableExporter(metadata, Action.UPDATE);

schemaMigrator.doMigration(metadata, options, targetDescriptor);
}
Expand Down
Expand Up @@ -21,13 +21,20 @@
import com.google.cloud.spanner.hibernate.SpannerDialect;
import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject;
import org.hibernate.dialect.Dialect;
import org.hibernate.tool.schema.Action;

/**
* Custom {@link AuxiliaryDatabaseObject} which generates the START BATCH DDL statement.
*/
public class StartBatchDdl implements AuxiliaryDatabaseObject {
private static final long serialVersionUID = 1L;

private final Action schemaAction;

public StartBatchDdl(Action schemaAction) {
this.schemaAction = schemaAction;
}

@Override
public String getExportIdentifier() {
return "START_BATCH_DDL";
Expand All @@ -40,7 +47,7 @@ public boolean appliesToDialect(Dialect dialect) {

@Override
public boolean beforeTablesOnCreation() {
return true;
return schemaAction != Action.UPDATE;
}

@Override
Expand All @@ -50,6 +57,10 @@ public String[] sqlCreateStrings(Dialect dialect) {

@Override
public String[] sqlDropStrings(Dialect dialect) {
return new String[] {"START BATCH DDL"};
if (schemaAction == Action.UPDATE) {
return new String[]{};
} else {
return new String[]{"START BATCH DDL"};
}
}
}
Expand Up @@ -56,7 +56,7 @@ public void initializeDependencies(Metadata metadata, Action schemaAction) {
for (Table childTable : metadata.collectTableMappings()) {
Interleaved interleaved = SchemaUtils.getInterleaveAnnotation(childTable, metadata);
if (interleaved != null) {
if (schemaAction == Action.CREATE) {
if (schemaAction == Action.CREATE || schemaAction == Action.UPDATE) {
// If creating tables, the parent blocks the child.
dependencies.put(childTable, SchemaUtils.getTable(interleaved.parentEntity(), metadata));
} else {
Expand Down
@@ -0,0 +1,117 @@
/*
* Copyright 2019 Google LLC
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/

package com.google.cloud.spanner.hibernate;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.cloud.spanner.hibernate.entities.Employee;
import com.mockrunner.mock.jdbc.JDBCMockObjectFactory;
import com.mockrunner.mock.jdbc.MockConnection;
import java.sql.SQLException;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.junit.Before;
import org.junit.Test;

public class GeneratedUpdateTableStatementsTests {

private StandardServiceRegistry registry;

private JDBCMockObjectFactory jdbcMockObjectFactory;

private MockConnection mockConnection;

/**
* Set up the metadata for Hibernate to generate schema statements.
*/
@Before
public void setup() {
this.jdbcMockObjectFactory = new JDBCMockObjectFactory();
this.jdbcMockObjectFactory.registerMockDriver();

mockConnection = this.jdbcMockObjectFactory.createMockConnection();

this.jdbcMockObjectFactory
.getMockDriver()
.setupConnection(mockConnection);

this.registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.dialect", SpannerDialect.class.getName())
// must NOT set a driver class name so that Hibernate will use java.sql.DriverManager
// and discover the only mock driver we have set up.
.applySetting("hibernate.connection.url", "unused")
.applySetting("hibernate.connection.username", "unused")
.applySetting("hibernate.connection.password", "unused")
.applySetting("hibernate.hbm2ddl.auto", "update")
.build();
}

@Test
public void testUpdateStatements_createTables() throws SQLException {
setupTestTables("Hello");

List<String> sqlStrings = mockConnection.getStatementResultSetHandler().getExecutedStatements();
assertThat(sqlStrings).containsExactly(
"START BATCH DDL",
"create table Employee (id INT64 not null,name STRING(255),manager_id INT64) "
+ "PRIMARY KEY (id)",
"create table hibernate_sequence (next_val INT64) PRIMARY KEY ()",
"INSERT INTO hibernate_sequence (next_val) VALUES(1)",
"create index name_index on Employee (name)",
"RUN BATCH"
);
}

@Test
public void testUpdateStatements_alterTables() throws SQLException {
setupTestTables("Employee");

List<String> sqlStrings = mockConnection.getStatementResultSetHandler().getExecutedStatements();
assertThat(sqlStrings).containsExactly(
"START BATCH DDL",
"alter table Employee ADD COLUMN id INT64 not null",
"alter table Employee ADD COLUMN name STRING(255)",
"alter table Employee ADD COLUMN manager_id INT64",
"create table hibernate_sequence (next_val INT64) PRIMARY KEY ()",
"INSERT INTO hibernate_sequence (next_val) VALUES(1)",
"create index name_index on Employee (name)",
"RUN BATCH"
);
}

/**
* Sets up which pre-existing tables that Hibernate sees.
*/
private void setupTestTables(String... tables) throws SQLException {
mockConnection.setMetaData(MockJdbcUtils.createMockDatabaseMetaData(tables));

Metadata metadata =
new MetadataSources(this.registry)
.addAnnotatedClass(Employee.class)
.buildMetadata();

Session session = metadata.buildSessionFactory().openSession();
session.beginTransaction();
session.close();
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2019 Google LLC
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/

package com.google.cloud.spanner.hibernate;

import com.mockrunner.mock.jdbc.MockDatabaseMetaData;
import com.mockrunner.mock.jdbc.MockResultSet;
import java.sql.ResultSet;
import java.sql.Types;
import java.util.Arrays;
import java.util.UUID;

/**
* Helper class to building mock objects for the mock JDBC driver.
*/
public class MockJdbcUtils {

private static final String[] TABLE_METADATA_COLUMNS = new String[]{
"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "TABLE_TYPE", "REMARKS", "TYPE_CAT",
"TYPE_SCHEM", "TYPE_NAME", "SELF_REFERENCING_COL_NAME", "REF"
};

private static final String[] COLUMN_METADATA_LABELS = new String[]{
"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "DATA_TYPE",
"TYPE_NAME", "COLUMN_SIZE", "DECIMAL_DIGITS", "IS_NULLABLE",
};

/**
* Creates the metadata object read by Hibernate to determine which tables already exist.
*/
public static MockDatabaseMetaData createMockDatabaseMetaData(String... tables) {
MockDatabaseMetaData mockDatabaseMetaData = new MockDatabaseMetaData();
mockDatabaseMetaData.setColumns(createColumnMetadataResultSet(tables));
mockDatabaseMetaData.setTables(createTableMetadataResultSet(tables));
mockDatabaseMetaData.setIndexInfo(new MockResultSet(UUID.randomUUID().toString()));
mockDatabaseMetaData.setImportedKeys(new MockResultSet(UUID.randomUUID().toString()));
return mockDatabaseMetaData;
}

/**
* Constructs a mock Column metadata {@link ResultSet} describing fake columns of the tables.
*/
private static ResultSet createColumnMetadataResultSet(String... tableNames) {
MockResultSet mockResultSet = initResultSet(COLUMN_METADATA_LABELS);

for (int i = 0; i < tableNames.length; i++) {
Object[] row = new Object[COLUMN_METADATA_LABELS.length];
Arrays.fill(row, "");
row[2] = tableNames[i];
row[3] = "column";
row[4] = Types.VARCHAR;
row[5] = "string(255)";
row[6] = 255;
row[7] = 0;
row[8] = 0;
mockResultSet.addRow(row);
}

return mockResultSet;
}


/**
* Constructs a mock Table metadata {@link ResultSet} describing table names.
*/
private static ResultSet createTableMetadataResultSet(String... tableNames) {
MockResultSet mockResultSet = initResultSet(TABLE_METADATA_COLUMNS);

for (int i = 0; i < tableNames.length; i++) {
String[] row = new String[TABLE_METADATA_COLUMNS.length];
Arrays.fill(row, "");
row[2] = tableNames[i];
row[3] = "TABLE";
mockResultSet.addRow(row);
}

return mockResultSet;
}

private static MockResultSet initResultSet(String[] columnLabels) {
MockResultSet mockResultSet = new MockResultSet(UUID.randomUUID().toString());
for (int i = 0; i < columnLabels.length; i++) {
mockResultSet.addColumn(columnLabels[i]);
}

return mockResultSet;
}
}
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -13,7 +13,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<hibernate.version>5.4.9.Final</hibernate.version>
<spanner-jdbc-driver.version>1.11.0</spanner-jdbc-driver.version>
<spanner-jdbc-driver.version>1.12.0</spanner-jdbc-driver.version>
<log4j.version>1.2.17</log4j.version>
<google-cloud-spanner.version>1.43.0</google-cloud-spanner.version>
</properties>
Expand Down