From 88ef8bf8b18ef4e85e8ac9a1249f14b452ebbae9 Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <code@dgerhardt.net> Date: Mon, 9 Oct 2017 17:56:48 +0200 Subject: [PATCH] Implement database migrations to v3 --- .../thm/arsnova/config/PersistanceConfig.java | 16 ++- .../thm/arsnova/entities/MigrationState.java | 18 ++- .../serialization/CouchDbDocumentModule.java | 1 + .../couchdb/InitializingCouchDbConnector.java | 23 ++++ .../couchdb/migrations/Migration.java | 6 + .../couchdb/migrations/MigrationExecutor.java | 44 +++++++ .../couchdb/migrations/V2ToV3Migration.java | 107 ++++++++++++++++++ .../support/MangoCouchDbConnector.java | 4 +- src/main/resources/arsnova.properties.example | 3 +- src/test/resources/arsnova.properties.example | 3 +- 10 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/migrations/Migration.java create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/migrations/MigrationExecutor.java create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/migrations/V2ToV3Migration.java diff --git a/src/main/java/de/thm/arsnova/config/PersistanceConfig.java b/src/main/java/de/thm/arsnova/config/PersistanceConfig.java index d278ee91..8add6949 100644 --- a/src/main/java/de/thm/arsnova/config/PersistanceConfig.java +++ b/src/main/java/de/thm/arsnova/config/PersistanceConfig.java @@ -3,14 +3,19 @@ package de.thm.arsnova.config; import de.thm.arsnova.entities.serialization.CouchDbObjectMapperFactory; import de.thm.arsnova.persistance.*; import de.thm.arsnova.persistance.couchdb.*; -import org.ektorp.CouchDbConnector; +import de.thm.arsnova.persistance.couchdb.support.MangoCouchDbConnector; import org.ektorp.impl.StdCouchDbInstance; import org.ektorp.spring.HttpClientFactoryBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; +@ComponentScan({ + "de.thm.arsnova.persistance.couchdb.migrations" +}) @Configuration @Profile("!test") public class PersistanceConfig { @@ -19,12 +24,19 @@ public class PersistanceConfig { @Value("${couchdb.port}") private int couchDbPort; @Value("${couchdb.username:}") private String couchDbUsername; @Value("${couchdb.password:}") private String couchDbPassword; + @Value("${couchdb.migrate-from:}") private String couchDbMigrateFrom; @Bean - public CouchDbConnector couchDbConnector() throws Exception { + @Primary + public MangoCouchDbConnector couchDbConnector() throws Exception { return new InitializingCouchDbConnector(couchDbName, couchDbInstance(), new CouchDbObjectMapperFactory()); } + @Bean + public MangoCouchDbConnector couchDbMigrationConnector() throws Exception { + return new MangoCouchDbConnector(couchDbMigrateFrom, couchDbInstance(), new CouchDbObjectMapperFactory()); + } + @Bean public StdCouchDbInstance couchDbInstance() throws Exception { return new StdCouchDbInstance(couchDbHttpClientFactory().getObject()); diff --git a/src/main/java/de/thm/arsnova/entities/MigrationState.java b/src/main/java/de/thm/arsnova/entities/MigrationState.java index 3d1d27b0..34428009 100644 --- a/src/main/java/de/thm/arsnova/entities/MigrationState.java +++ b/src/main/java/de/thm/arsnova/entities/MigrationState.java @@ -3,6 +3,7 @@ package de.thm.arsnova.entities; import com.fasterxml.jackson.annotation.JsonView; import de.thm.arsnova.entities.serialization.View; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -11,6 +12,10 @@ public class MigrationState implements Entity { private String id; private Date start; + public Migration() { + + } + public Migration(String id, Date start) { this.id = id; this.start = start; @@ -25,25 +30,30 @@ public class MigrationState implements Entity { public Date getStart() { return start; } + + @Override + public String toString() { + return "Migration " + id + " started at " + start; + } } - private String id = "MigrationState"; + public static final String ID = "MigrationState"; private String rev; private Date creationTimestamp; private Date updateTimestamp; private Migration active; - private List<String> completed; + private List<String> completed = new ArrayList<>(); @Override @JsonView(View.Persistence.class) public String getId() { - return id; + return ID; } @Override @JsonView(View.Persistence.class) public void setId(final String id) { - if (!id.equals(this.id)) { + if (!id.equals(this.ID)) { throw new IllegalArgumentException("ID of this entity must not be changed."); }; } diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java b/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java index 1579458a..4e71d9d2 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java +++ b/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java @@ -28,5 +28,6 @@ public class CouchDbDocumentModule extends SimpleModule { @Override public void setupModule(SetupContext context) { context.setMixInAnnotations(Entity.class, CouchDbDocumentMixIn.class); + context.setMixInAnnotations(de.thm.arsnova.entities.migration.v2.Entity.class, CouchDbDocumentMixIn.class); } } diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java b/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java index fbe08d2d..acb81dfa 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java @@ -1,6 +1,8 @@ package de.thm.arsnova.persistance.couchdb; import com.fasterxml.jackson.core.JsonProcessingException; +import de.thm.arsnova.entities.MigrationState; +import de.thm.arsnova.persistance.couchdb.migrations.MigrationExecutor; import de.thm.arsnova.persistance.couchdb.support.MangoCouchDbConnector; import org.ektorp.CouchDbInstance; import org.ektorp.DocumentNotFoundException; @@ -8,6 +10,7 @@ import org.ektorp.impl.ObjectMapperFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -29,6 +32,7 @@ public class InitializingCouchDbConnector extends MangoCouchDbConnector implemen private final List<Bindings> docs = new ArrayList<>(); private ResourceLoader resourceLoader; + private MigrationExecutor migrationExecutor; public InitializingCouchDbConnector(final String databaseName, final CouchDbInstance dbInstance) { super(databaseName, dbInstance); @@ -73,14 +77,33 @@ public class InitializingCouchDbConnector extends MangoCouchDbConnector implemen }); } + protected void migrate() { + MigrationState state; + try { + state = get(MigrationState.class, MigrationState.ID); + } catch (DocumentNotFoundException e) { + logger.debug("No migration state found in database."); + state = new MigrationState(); + } + if (migrationExecutor != null && migrationExecutor.runMigrations(state)) { + update(state); + } + } + @Override public void afterPropertiesSet() throws Exception { loadDesignDocFiles(); createDesignDocs(); + migrate(); } @Override public void setResourceLoader(final ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } + + @Autowired + public void setMigrationExecutor(final MigrationExecutor migrationExecutor) { + this.migrationExecutor = migrationExecutor; + } } diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/Migration.java b/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/Migration.java new file mode 100644 index 00000000..ea71ece6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/Migration.java @@ -0,0 +1,6 @@ +package de.thm.arsnova.persistance.couchdb.migrations; + +public interface Migration { + String getId(); + void migrate(); +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/MigrationExecutor.java b/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/MigrationExecutor.java new file mode 100644 index 00000000..06d54e53 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/MigrationExecutor.java @@ -0,0 +1,44 @@ +package de.thm.arsnova.persistance.couchdb.migrations; + +import de.thm.arsnova.entities.MigrationState; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MigrationExecutor { + private static final Logger logger = LoggerFactory.getLogger(MigrationExecutor.class); + + private List<Migration> migrations; + + public MigrationExecutor(List<Migration> migrations) { + this.migrations = migrations.stream() + .sorted(Comparator.comparing(Migration::getId)).collect(Collectors.toList()); + } + + public boolean runMigrations(@NonNull final MigrationState migrationState) { + List<Migration> pendingMigrations = migrations.stream() + .filter(m -> !migrationState.getCompleted().contains(m.getId())).collect(Collectors.toList()); + boolean stateChange = false; + if (migrationState.getActive() != null) { + throw new IllegalStateException("An migration is already active: " + migrationState.getActive()); + } + logger.debug("Pending migrations: " + pendingMigrations.stream() + .map(Migration::getId).collect(Collectors.joining())); + for (Migration migration : pendingMigrations) { + stateChange = true; + migrationState.setActive(migration.getId(), new Date()); + migration.migrate(); + migrationState.getCompleted().add(migration.getId()); + migrationState.setActive(null); + } + + return stateChange; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/V2ToV3Migration.java b/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/V2ToV3Migration.java new file mode 100644 index 00000000..2ed128af --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/migrations/V2ToV3Migration.java @@ -0,0 +1,107 @@ +package de.thm.arsnova.persistance.couchdb.migrations; + +import de.thm.arsnova.entities.Room; +import de.thm.arsnova.entities.UserProfile; +import de.thm.arsnova.entities.migration.FromV2Migrator; +import de.thm.arsnova.entities.migration.v2.DbUser; +import de.thm.arsnova.entities.migration.v2.LoggedIn; +import de.thm.arsnova.entities.migration.v2.MotdList; +import de.thm.arsnova.persistance.UserRepository; +import de.thm.arsnova.persistance.couchdb.support.MangoCouchDbConnector; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Service +public class V2ToV3Migration implements Migration { + private static final String ID = "20170914131300"; + private static final int LIMIT = 200; + + private FromV2Migrator migrator; + private MangoCouchDbConnector toConnector; + private MangoCouchDbConnector fromConnector; + private UserRepository userRepository; + + public V2ToV3Migration( + final FromV2Migrator migrator, + final MangoCouchDbConnector toConnector, + @Qualifier("couchDbMigrationConnector") final MangoCouchDbConnector fromConnector, + final UserRepository userRepository) { + this.migrator = migrator; + this.toConnector = toConnector; + this.fromConnector = fromConnector; + this.userRepository = userRepository; + } + + public String getId() { + return ID; + } + + public void migrate() { + migrateUsers(); + migrateRooms(); + } + + private void migrateUsers() { + HashMap<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "userdetails"); + MangoCouchDbConnector.MangoQuery query = fromConnector.new MangoQuery(queryOptions); + query.setLimit(LIMIT); + + for (int skip = 0;; skip += LIMIT) { + query.setSkip(skip); + List<UserProfile> profilesV3 = new ArrayList<>(); + List<DbUser> dbUsersV2 = fromConnector.query(query, DbUser.class); + if (dbUsersV2.size() == 0) { + break; + } + + for (DbUser userV2 : dbUsersV2) { + HashMap<String, Object> loggedInQueryOptions = new HashMap<>(); + loggedInQueryOptions.put("type", "logged_in"); + loggedInQueryOptions.put("user", userV2.getUsername()); + MangoCouchDbConnector.MangoQuery loggedInQuery = fromConnector.new MangoQuery(loggedInQueryOptions); + List<LoggedIn> loggedInList = fromConnector.query(loggedInQuery, LoggedIn.class); + LoggedIn loggedIn = loggedInList.size() > 0 ? loggedInList.get(0) : null; + + HashMap<String, Object> motdListQueryOptions = new HashMap<>(); + motdListQueryOptions.put("type", "motdlist"); + motdListQueryOptions.put("username", userV2.getUsername()); + MangoCouchDbConnector.MangoQuery motdlistQuery = fromConnector.new MangoQuery(motdListQueryOptions); + List<MotdList> motdListList = fromConnector.query(motdlistQuery, MotdList.class); + MotdList motdList = motdListList.size() > 0 ? motdListList.get(0) : null; + + profilesV3.add(migrator.migrate(userV2, loggedIn, motdList)); + } + + toConnector.executeBulk(profilesV3); + } + } + + private void migrateRooms() { + HashMap<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "session"); + MangoCouchDbConnector.MangoQuery query = fromConnector.new MangoQuery(queryOptions); + query.setLimit(LIMIT); + + for (int skip = 0;; skip += LIMIT) { + query.setSkip(skip); + List<Room> roomsV3 = new ArrayList<>(); + List<de.thm.arsnova.entities.migration.v2.Room> roomsV2 = fromConnector.query(query, + de.thm.arsnova.entities.migration.v2.Room.class); + if (roomsV2.size() == 0) { + break; + } + + for (de.thm.arsnova.entities.migration.v2.Room roomV2 : roomsV2) { + UserProfile profile = userRepository.findByUsername(roomV2.getCreator()); + roomsV3.add(migrator.migrate(roomV2, profile)); + } + + toConnector.executeBulk(roomsV3); + } + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java index 4f4eb619..154b360b 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java @@ -209,7 +209,9 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { } catch (JsonProcessingException e) { throw new DbAccessException("Could not serialize Mango query."); } - List<T> result = restTemplate.post(dbURI.append("_find").toString(), queryString, rh); + List<T> result = restTemplate.postUncached(dbURI.append("_find").toString(), queryString, rh); + //List<T> result = restTemplate.post(dbURI.append("_find").toString(), new JacksonableEntity(query, objectMapper), rh); + logger.debug("Answer from CouchDB Mango query: {}", result); return result; diff --git a/src/main/resources/arsnova.properties.example b/src/main/resources/arsnova.properties.example index 34fa9b0a..b1135e4c 100644 --- a/src/main/resources/arsnova.properties.example +++ b/src/main/resources/arsnova.properties.example @@ -38,9 +38,10 @@ security.admin-accounts= ################################################################################ couchdb.host=localhost couchdb.port=5984 -couchdb.name=arsnova +couchdb.name=arsnova3 couchdb.username=admin couchdb.password= +#couchdb.migrate-from=arsnova ################################################################################ diff --git a/src/test/resources/arsnova.properties.example b/src/test/resources/arsnova.properties.example index 34fa9b0a..b1135e4c 100644 --- a/src/test/resources/arsnova.properties.example +++ b/src/test/resources/arsnova.properties.example @@ -38,9 +38,10 @@ security.admin-accounts= ################################################################################ couchdb.host=localhost couchdb.port=5984 -couchdb.name=arsnova +couchdb.name=arsnova3 couchdb.username=admin couchdb.password= +#couchdb.migrate-from=arsnova ################################################################################ -- GitLab