Skip to content

Commit

Permalink
Remove now-redundant Repository.migrate method
Browse files Browse the repository at this point in the history
  • Loading branch information
hermannm committed Sep 18, 2024
1 parent 1d145ad commit 6ec82dc
Show file tree
Hide file tree
Showing 3 changed files with 0 additions and 302 deletions.
39 changes: 0 additions & 39 deletions src/main/kotlin/no/liflig/documentstore/repository/Repository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,40 +113,6 @@ interface Repository<EntityIdT : EntityId, EntityT : Entity<EntityIdT>> {
}
}

/**
* When using a document store, the application must take care to stay backwards-compatible with
* entities that are stored in the database. Thus, new fields added to entitites must always have
* a default value, so that entities stored before the field was added can still be deserialized.
*
* Sometimes we may add a field with a default value, but also want to query on that field in e.g.
* [RepositoryJdbi.getByPredicate]. In this case, it's not enough to add a default value to the
* field that is populated on deserializing from the database - we actually have to migrate the
* stored entity. This method exists for those cases.
*
* In the implementation for [RepositoryJdbi.migrate], all entities are read from the database
* table, in a streaming fashion to avoid reading them all into memory. It then updates the
* entities in batches (see [OPTIMAL_BATCH_SIZE]).
*
* It is important that the migration is done in an idempotent manner, i.e. that it may be
* executed repeatedly with the same results. This is because we call this method from application
* code, and if for example there are multiple instances of the service running, [migrate] will be
* called by each one.
*
* Any new fields on the entity with default values will be stored in the database through the
* process of deserializing and re-serializing here. If you want to do further transforms, you can
* use the [transformEntity] parameter.
*/
@ExperimentalMigrationApi
fun migrate(transformEntity: ((Versioned<EntityT>) -> EntityT)? = null) {
// A default implementation is provided here on the interface, so that implementors don't have
// to implement this themselves (for e.g. mock repositories).
var entities = listAll()
if (transformEntity != null) {
entities = entities.map { entity -> entity.copy(item = transformEntity(entity)) }
}
batchUpdate(entities)
}

/**
* Initiates a database transaction, and executes the given [block] inside of it. Any calls to
* other repository methods inside this block will use the same transaction, and roll back if an
Expand All @@ -161,8 +127,3 @@ interface Repository<EntityIdT : EntityId, EntityT : Entity<EntityIdT>> {
return block()
}
}

@RequiresOptIn(
"The migration API of Liflig Document Store is currently under development, and may change",
)
internal annotation class ExperimentalMigrationApi
Original file line number Diff line number Diff line change
Expand Up @@ -405,70 +405,6 @@ open class RepositoryJdbi<EntityIdT : EntityId, EntityT : Entity<EntityIdT>>(
}
}

@ExperimentalMigrationApi
override fun migrate(transformEntity: ((Versioned<EntityT>) -> EntityT)?) {
transactional {
useHandle(jdbi) { handle ->
/**
* The [org.jdbi.v3.core.result.ResultIterable] must be closed after iterating - but that is
* automatically done after iterating through all results, which we do in
* [executeBatchOperation] below.
*/
val entities =
handle
.createQuery(
"""
SELECT id, data, version, created_at, modified_at
FROM "${tableName}"
FOR UPDATE
"""
.trimIndent(),
)
// If we don't specify fetch size, the JDBC driver for Postgres fetches all results
// by default:
// https://jdbc.postgresql.org/documentation/query/#getting-results-based-on-a-cursor
// This can lead to out-of-memory errors here, since we fetch the entire table.
// We really only want to fetch out a single batch at a time, since we process
// entities in batches. To do that, we set the fetch size, which is the number of
// rows to fetch at a time. Solution found here:
// https://www.postgresql.org/message-id/[email protected]
.setFetchSize(100)
.map(rowMapper)

val modifiedAt = Instant.now()

executeBatchOperation(
handle,
entities,
// We don't have to check version here, since we use FOR UPDATE above, so we know we
// have the latest version
statement =
"""
UPDATE "${tableName}"
SET
version = :nextVersion,
data = :data::jsonb,
modified_at = :modifiedAt
WHERE
id = :id
"""
.trimIndent(),
bindParameters = { batch, entity ->
val nextVersion = entity.version.next()
val updatedEntity =
if (transformEntity == null) entity.item else transformEntity(entity)

batch
.bind("nextVersion", nextVersion)
.bind("data", serializationAdapter.toJson(updatedEntity))
.bind("id", entity.item.id)
.bind("modifiedAt", modifiedAt)
},
)
}
}
}

override fun <ReturnT> transactional(block: () -> ReturnT): ReturnT {
return transactional(jdbi, block)
}
Expand Down
199 changes: 0 additions & 199 deletions src/test/kotlin/no/liflig/documentstore/repository/MigrationTest.kt

This file was deleted.

0 comments on commit 6ec82dc

Please sign in to comment.