mirror of
https://github.com/corda/corda.git
synced 2025-06-18 23:28:21 +00:00
Merge pull request #861 from corda/feature/ENT-1932/move_MigrationExporter
ENT-1932 Move MigrationExporter to the "node" module
This commit is contained in:
@ -1,106 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.internal.MigrationHelpers.migrationResourceNameForSchema
|
||||
import net.corda.core.internal.objectOrNewInstance
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.HibernateConfiguration.Companion.buildHibernateMetadata
|
||||
import org.hibernate.boot.Metadata
|
||||
import org.hibernate.boot.MetadataSources
|
||||
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
|
||||
import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl
|
||||
import org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER
|
||||
import org.hibernate.cfg.Configuration
|
||||
import org.hibernate.cfg.Environment
|
||||
import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl
|
||||
import org.hibernate.tool.hbm2ddl.SchemaExport
|
||||
import org.hibernate.tool.schema.TargetType
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Converter
|
||||
import javax.sql.DataSource
|
||||
|
||||
/**
|
||||
* This is useful for CorDapp developers who want to enable migrations for
|
||||
* standard "Open Source" Corda CorDapps
|
||||
*/
|
||||
class MigrationExporter(val parent: Path, val datasourceProperties: Properties, val cordappClassLoader: ClassLoader, val dataSource: DataSource) {
|
||||
|
||||
companion object {
|
||||
const val LIQUIBASE_HEADER = "--liquibase formatted sql"
|
||||
const val CORDA_USER = "R3.Corda.Generated"
|
||||
}
|
||||
|
||||
fun generateMigrationForCorDapp(schemaName: String): Path {
|
||||
val schemaClass = cordappClassLoader.loadClass(schemaName)
|
||||
val schemaObject = schemaClass.kotlin.objectOrNewInstance() as MappedSchema
|
||||
return generateMigrationForCorDapp(schemaObject)
|
||||
}
|
||||
|
||||
fun generateMigrationForCorDapp(mappedSchema: MappedSchema): Path {
|
||||
|
||||
//create hibernate metadata for MappedSchema
|
||||
val metadata = createHibernateMetadataForSchema(mappedSchema)
|
||||
|
||||
//create output file and add liquibase headers
|
||||
val outputFile = File(parent.toFile(), "${migrationResourceNameForSchema(mappedSchema)}.sql")
|
||||
outputFile.apply {
|
||||
parentFile.mkdirs()
|
||||
delete()
|
||||
createNewFile()
|
||||
appendText(LIQUIBASE_HEADER)
|
||||
appendText("\n\n")
|
||||
appendText("--changeset $CORDA_USER:initial_schema_for_${mappedSchema::class.simpleName!!}")
|
||||
appendText("\n")
|
||||
}
|
||||
|
||||
//export the schema to that file
|
||||
SchemaExport().apply {
|
||||
setDelimiter(";")
|
||||
setFormat(true)
|
||||
setOutputFile(outputFile.absolutePath)
|
||||
execute(EnumSet.of(TargetType.SCRIPT), SchemaExport.Action.CREATE, metadata)
|
||||
}
|
||||
return outputFile.toPath()
|
||||
}
|
||||
|
||||
private fun createHibernateMetadataForSchema(mappedSchema: MappedSchema): Metadata {
|
||||
val metadataSources = MetadataSources(BootstrapServiceRegistryBuilder().build())
|
||||
val config = Configuration(metadataSources)
|
||||
.setProperty(CONNECTION_PROVIDER, DatasourceConnectionProviderImpl::class.java.name)
|
||||
|
||||
mappedSchema.mappedTypes.forEach { config.addAnnotatedClass(it) }
|
||||
|
||||
val registryBuilder = config.standardServiceRegistryBuilder
|
||||
.addService(org.hibernate.boot.registry.classloading.spi.ClassLoaderService::class.java, ClassLoaderServiceImpl(cordappClassLoader))
|
||||
.applySettings(config.properties)
|
||||
.applySetting(Environment.DATASOURCE, dataSource)
|
||||
|
||||
val metadataBuilder = metadataSources.getMetadataBuilder(registryBuilder.build())
|
||||
|
||||
return buildHibernateMetadata(metadataBuilder, datasourceProperties.getProperty(CordaPersistence.DataSourceConfigTag.DATA_SOURCE_URL),
|
||||
listOf(DummyAbstractPartyToX500NameAsStringConverter()))
|
||||
}
|
||||
|
||||
/**
|
||||
* used just for generating columns
|
||||
*/
|
||||
@Converter(autoApply = true)
|
||||
class DummyAbstractPartyToX500NameAsStringConverter : AttributeConverter<AbstractParty, String> {
|
||||
override fun convertToDatabaseColumn(party: AbstractParty?) = null
|
||||
override fun convertToEntityAttribute(dbData: String?) = null
|
||||
}
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
/*
|
||||
* R3 Proprietary and Confidential
|
||||
*
|
||||
* Copyright (c) 2018 R3 Limited. All rights reserved.
|
||||
*
|
||||
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
|
||||
*
|
||||
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
|
||||
*/
|
||||
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.math.BigInteger
|
||||
import java.net.URL
|
||||
import javax.persistence.*
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
class SchemaMigrationTest {
|
||||
|
||||
@Test
|
||||
fun `Ensure that runMigration is disabled by default`() {
|
||||
assertThat(DatabaseConfig().runMigration).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Migration is run when runMigration is disabled, and database is H2`() {
|
||||
val dataSourceProps = MockServices.makeTestDataSourceProperties()
|
||||
val db = configureDatabase(dataSourceProps, DatabaseConfig(runMigration = false), rigorousMock())
|
||||
checkMigrationRun(db)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Migration is run when runMigration is enabled`() {
|
||||
val dataSourceProps = MockServices.makeTestDataSourceProperties()
|
||||
val db = configureDatabase(dataSourceProps, DatabaseConfig(runMigration = true), rigorousMock())
|
||||
checkMigrationRun(db)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Verification passes when migration is run as a separate step`() {
|
||||
val schemaService = NodeSchemaService()
|
||||
val dataSourceProps = MockServices.makeTestDataSourceProperties()
|
||||
|
||||
//run the migration on the database
|
||||
val migration = SchemaMigration(schemaService.schemaOptions.keys, HikariDataSource(HikariConfig(dataSourceProps)), true, DatabaseConfig())
|
||||
migration.runMigration()
|
||||
|
||||
//start the node with "runMigration = false" and check that it started correctly
|
||||
val db = configureDatabase(dataSourceProps, DatabaseConfig(runMigration = false), rigorousMock(), schemaService)
|
||||
checkMigrationRun(db)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `The migration picks up migration files on the classpath if they follow the convention`() {
|
||||
val dataSourceProps = MockServices.makeTestDataSourceProperties()
|
||||
|
||||
// create a migration file for the DummyTestSchemaV1 and add it to the classpath
|
||||
val tmpFolder = Files.createTempDirectory("test")
|
||||
val fileName = MigrationExporter(tmpFolder, dataSourceProps, Thread.currentThread().contextClassLoader, HikariDataSource(HikariConfig(dataSourceProps))).generateMigrationForCorDapp(DummyTestSchemaV1).fileName
|
||||
addToClassPath(tmpFolder)
|
||||
|
||||
// run the migrations for DummyTestSchemaV1, which should pick up the migration file
|
||||
val db = configureDatabase(dataSourceProps, DatabaseConfig(runMigration = true), rigorousMock(), NodeSchemaService(extraSchemas = setOf(DummyTestSchemaV1)))
|
||||
|
||||
// check that the file was picked up
|
||||
val nrOfChangesOnDiscoveredFile = db.dataSource.connection.use {
|
||||
it.createStatement().executeQuery("select count(*) from DATABASECHANGELOG where filename ='migration/${fileName}'").use { rs ->
|
||||
rs.next()
|
||||
rs.getInt(1)
|
||||
}
|
||||
}
|
||||
assertThat(nrOfChangesOnDiscoveredFile).isGreaterThan(0)
|
||||
|
||||
//clean up
|
||||
FileUtils.deleteDirectory(tmpFolder.toFile())
|
||||
}
|
||||
|
||||
private fun checkMigrationRun(db: CordaPersistence) {
|
||||
//check that the hibernate_sequence was created which means the migration was run
|
||||
db.transaction {
|
||||
val value = this.session.createNativeQuery("SELECT NEXT VALUE FOR hibernate_sequence").uniqueResult() as BigInteger
|
||||
assertThat(value).isGreaterThan(BigInteger.ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
//hacky way to add a folder to the classpath
|
||||
fun addToClassPath(file: Path) = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java).apply {
|
||||
isAccessible = true
|
||||
invoke(ClassLoader.getSystemClassLoader(), file.toFile().toURL())
|
||||
}
|
||||
|
||||
object DummyTestSchema
|
||||
object DummyTestSchemaV1 : MappedSchema(schemaFamily = DummyTestSchema.javaClass, version = 1, mappedTypes = listOf(PersistentDummyTestState::class.java)) {
|
||||
|
||||
@Entity
|
||||
@Table(name = "dummy_test_states")
|
||||
class PersistentDummyTestState(
|
||||
|
||||
@ElementCollection
|
||||
@Column(name = "participants")
|
||||
@CollectionTable(name = "dummy_deal_states_participants", joinColumns = arrayOf(
|
||||
JoinColumn(name = "output_index", referencedColumnName = "output_index"),
|
||||
JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id")))
|
||||
override var participants: MutableSet<AbstractParty>? = null,
|
||||
|
||||
@Transient
|
||||
val uid: UniqueIdentifier
|
||||
|
||||
) : CommonSchemaV1.LinearState(uuid = uid.id, externalId = uid.externalId, participants = participants)
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user