Skip to content

Commit

Permalink
[#12048] Data migration for feedback session entities (#12986)
Browse files Browse the repository at this point in the history
* Add migration and verification script for feedback session

* Optimize data migration (foreign key reference fetch function)

* Add counts verification script

---------

Co-authored-by: YX Z <[email protected]>
Co-authored-by: nicolascwy <[email protected]>
  • Loading branch information
3 people authored Apr 7, 2024
1 parent 44f3e0b commit bb58c58
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package teammates.client.scripts.sql;

import java.time.Duration;

// CHECKSTYLE.OFF:ImportOrder
import com.googlecode.objectify.cmd.Query;
import teammates.common.util.HibernateUtil;
import teammates.storage.entity.FeedbackSession;
import teammates.storage.sqlentity.Course;

// CHECKSTYLE.ON:ImportOrder
/**
* Data migration class for feddback sessions.
*/
@SuppressWarnings("PMD")
public class DataMigrationForFeedbackSessionSql
extends DataMigrationEntitiesBaseScriptSql<FeedbackSession, teammates.storage.sqlentity.FeedbackSession> {

public static void main(String[] args) {
new DataMigrationForFeedbackSessionSql().doOperationRemotely();
}

@Override
protected Query<FeedbackSession> getFilterQuery() {
return ofy().load().type(teammates.storage.entity.FeedbackSession.class);
}

@Override
protected boolean isPreview() {
return false;
}

@Override
protected void setMigrationCriteria() {
// No migration criteria currently needed.
}

@Override
protected boolean isMigrationNeeded(FeedbackSession entity) {
return true;
}

@Override
protected void migrateEntity(FeedbackSession oldEntity) throws Exception {
HibernateUtil.beginTransaction();
Course course = HibernateUtil.getReference(teammates.storage.sqlentity.Course.class, oldEntity.getCourseId());
HibernateUtil.commitTransaction();

teammates.storage.sqlentity.FeedbackSession newFeedbackSession = new teammates.storage.sqlentity.FeedbackSession(
oldEntity.getFeedbackSessionName(),
course,
oldEntity.getCreatorEmail(),
oldEntity.getInstructions(),
oldEntity.getStartTime(),
oldEntity.getEndTime(),
oldEntity.getSessionVisibleFromTime(),
oldEntity.getResultsVisibleFromTime(),
Duration.ofMinutes(oldEntity.getGracePeriod()),
oldEntity.isOpeningEmailEnabled(),
oldEntity.isClosingEmailEnabled(),
oldEntity.isPublishedEmailEnabled()
);

newFeedbackSession.setClosedEmailSent(oldEntity.isSentClosedEmail());
newFeedbackSession.setClosingSoonEmailSent(oldEntity.isSentClosingEmail());
newFeedbackSession.setOpenEmailSent(oldEntity.isSentOpenEmail());
newFeedbackSession.setOpeningSoonEmailSent(oldEntity.isSentOpeningSoonEmail());
newFeedbackSession.setPublishedEmailSent(oldEntity.isSentPublishedEmail());
newFeedbackSession.setDeletedAt(oldEntity.getDeletedTime());

saveEntityDeferred(newFeedbackSession);
}

}
44 changes: 40 additions & 4 deletions src/client/java/teammates/client/scripts/sql/SeedDb.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import teammates.storage.entity.Account;
import teammates.storage.entity.AccountRequest;
import teammates.storage.entity.Course;
import teammates.storage.entity.FeedbackSession;
import teammates.storage.entity.Notification;
import teammates.test.FileHelper;

Expand All @@ -40,6 +41,7 @@
public class SeedDb extends DatastoreClient {

private static final int MAX_ENTITY_SIZE = 10000;
private static final int MAX_FEEDBACKSESSION_FOR_EACH_COURSE_SIZE = 3;
private final LogicExtension logic = new LogicExtension();
private Closeable closeable;

Expand Down Expand Up @@ -106,12 +108,12 @@ protected void persistAdditionalData() {
String[] args = {};
// Each account will have this amount of read notifications
seedNotificationAccountAndAccountRequest(5, 1000);
seedCourse();
seedCourseAndRelatedEntites();

GenerateUsageStatisticsObjects.main(args);
}

private void seedCourse() {
private void seedCourseAndRelatedEntites() {
log("Seeding courses");
for (int i = 0; i < MAX_ENTITY_SIZE; i++) {
if (i % (MAX_ENTITY_SIZE / 5) == 0) {
Expand All @@ -120,19 +122,53 @@ private void seedCourse() {
}

Random rand = new Random();

String courseId = UUID.randomUUID().toString();
try {
String courseName = String.format("Course %s", i);
String courseInstitute = String.format("Institute %s", i);
String courseTimeZone = String.format("Time Zone %s", i);
Course course = new Course(UUID.randomUUID().toString(), courseName, courseTimeZone, courseInstitute,
Course course = new Course(courseId, courseName, courseTimeZone, courseInstitute,
getRandomInstant(),
rand.nextInt(3) > 1 ? null : getRandomInstant(), // set deletedAt randomly at 25% chance
false);
ofy().save().entities(course).now();
} catch (Exception e) {
log(e.toString());
}

seedFeedbackSession(courseId);
}
}

private void seedFeedbackSession(String courseId) {
Random rand = new Random();
for (int i = 0; i < MAX_FEEDBACKSESSION_FOR_EACH_COURSE_SIZE; i++) {
try {
String feedbackSessionName = String.format("Feedback Session %s", i);
String feedbackSessionCourseId = courseId;
String feedbackSessionCreatorEmail = String.format("Creator Email %s", i);
String feedbackSessionInstructions = String.format("Instructions %s", i);
String timezone = String.format("Time Zone %s", i);
Instant feedbackSessionStartTime = getRandomInstant();
Instant feedbackSessionEndTime = getRandomInstant();
Instant feedbackSessionSessionVisibleFromTime = getRandomInstant();
Instant feedbackSessionResultsVisibleFromTime = getRandomInstant();
int feedbackSessionGracePeriod = rand.nextInt(3600);

FeedbackSession feedbackSession = new FeedbackSession(feedbackSessionName, feedbackSessionCourseId,
feedbackSessionCreatorEmail, feedbackSessionInstructions, getRandomInstant(),
rand.nextInt(3) > 1 ? null : getRandomInstant(), // set deletedAt randomly at 25% chance
feedbackSessionStartTime, feedbackSessionEndTime,
feedbackSessionSessionVisibleFromTime, feedbackSessionResultsVisibleFromTime,
timezone, feedbackSessionGracePeriod,
rand.nextBoolean(), rand.nextBoolean(), rand.nextBoolean(), rand.nextBoolean(),
rand.nextBoolean(), rand.nextBoolean(), rand.nextBoolean(), rand.nextBoolean(),
new HashMap<String, Instant>(), new HashMap<String, Instant>());

ofy().save().entities(feedbackSession).now();
} catch (Exception e) {
log(e.toString());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package teammates.client.scripts.sql;

// CHECKSTYLE.OFF:ImportOrder
import java.util.HashMap;
import java.util.Map;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;

import teammates.client.connector.DatastoreClient;
import teammates.client.util.ClientProperties;
import teammates.common.util.HibernateUtil;
import teammates.storage.entity.BaseEntity;
// CHECKSTYLE.ON:ImportOrder

/**
* Verify the counts of non-course entities are correct.
*/
@SuppressWarnings("PMD")
public class VerifyCourseEntityCounts extends DatastoreClient {
private VerifyCourseEntityCounts() {
String connectionUrl = ClientProperties.SCRIPT_API_URL;
String username = ClientProperties.SCRIPT_API_NAME;
String password = ClientProperties.SCRIPT_API_PASSWORD;

HibernateUtil.buildSessionFactory(connectionUrl, username, password);
}

public static void main(String[] args) throws Exception {
new VerifyCourseEntityCounts().doOperationRemotely();
}

private void printEntityVerification(String className, int datastoreCount, long psqlCount) {
System.out.println("========================================");
System.out.println(className);
System.out.println("Objectify count: " + datastoreCount);
System.out.println("Postgres count: " + psqlCount);
System.out.println("Correct number of rows?: " + (datastoreCount == psqlCount));
}

private Long countPostgresEntities(Class<? extends teammates.storage.sqlentity.BaseEntity> entity) {
HibernateUtil.beginTransaction();
CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder();
CriteriaQuery<Long> cr = cb.createQuery(Long.class);
Root<? extends teammates.storage.sqlentity.BaseEntity> root = cr.from(entity);

cr.select(cb.count(root));

Long count = HibernateUtil.createQuery(cr).getSingleResult();
HibernateUtil.commitTransaction();
return count;
}

@Override
protected void doOperation() {
Map<Class<? extends BaseEntity>, Class<? extends teammates.storage.sqlentity.BaseEntity>> entities =
new HashMap<Class<? extends BaseEntity>, Class<? extends teammates.storage.sqlentity.BaseEntity>>();

entities.put(teammates.storage.entity.Course.class, teammates.storage.sqlentity.Course.class);
entities.put(teammates.storage.entity.FeedbackSession.class, teammates.storage.sqlentity.FeedbackSession.class);

// Compare datastore "table" to postgres table for each entity
for (Map.Entry<Class<? extends BaseEntity>, Class<? extends teammates.storage.sqlentity.BaseEntity>> entry : entities
.entrySet()) {
Class<? extends BaseEntity> objectifyClass = entry.getKey();
Class<? extends teammates.storage.sqlentity.BaseEntity> sqlClass = entry.getValue();

int objectifyEntityCount = ofy().load().type(objectifyClass).count();
Long postgresEntityCount = countPostgresEntities(sqlClass);

printEntityVerification(objectifyClass.getSimpleName(), objectifyEntityCount, postgresEntityCount);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package teammates.client.scripts.sql;

import java.time.Duration;

import teammates.common.util.SanitizationHelper;
import teammates.storage.entity.FeedbackSession;

/**
* Verification of the feedback session attributes.
*/
public class VerifyFeedbackSessionAttributes
extends VerifyNonCourseEntityAttributesBaseScript<FeedbackSession, teammates.storage.sqlentity.FeedbackSession> {

public VerifyFeedbackSessionAttributes() {
super(FeedbackSession.class, teammates.storage.sqlentity.FeedbackSession.class);
}

@Override
protected String generateID(teammates.storage.sqlentity.FeedbackSession sqlEntity) {
return FeedbackSession.generateId(sqlEntity.getName(), sqlEntity.getCourse().getId());
}

@Override
protected boolean equals(teammates.storage.sqlentity.FeedbackSession sqlEntity, FeedbackSession datastoreEntity) {
try {
return sqlEntity.getCourse().getId().equals(datastoreEntity.getCourseId())
&& sqlEntity.getName().equals(datastoreEntity.getFeedbackSessionName())
&& sqlEntity.getCreatorEmail().equals(datastoreEntity.getCreatorEmail())
&& sqlEntity.getInstructions()
.equals(SanitizationHelper.sanitizeForRichText(datastoreEntity.getInstructions()))
&& sqlEntity.getStartTime().equals(datastoreEntity.getStartTime())
&& sqlEntity.getEndTime().equals(datastoreEntity.getEndTime())
&& sqlEntity.getSessionVisibleFromTime().equals(datastoreEntity.getSessionVisibleFromTime())
&& sqlEntity.getResultsVisibleFromTime().equals(datastoreEntity.getResultsVisibleFromTime())
&& sqlEntity.getGracePeriod().equals(Duration.ofMinutes(datastoreEntity.getGracePeriod()))
&& sqlEntity.isOpeningEmailEnabled() == datastoreEntity.isOpeningEmailEnabled()
&& sqlEntity.isClosingEmailEnabled() == datastoreEntity.isClosingEmailEnabled()
&& sqlEntity.isOpenEmailSent() == datastoreEntity.isSentOpenEmail()
&& sqlEntity.isOpeningSoonEmailSent() == datastoreEntity.isSentOpeningSoonEmail()
&& sqlEntity.isClosedEmailSent() == datastoreEntity.isSentClosedEmail()
&& sqlEntity.isClosingSoonEmailSent() == datastoreEntity.isSentClosingEmail()
&& sqlEntity.isPublishedEmailSent() == datastoreEntity.isSentPublishedEmail()
&& (sqlEntity.getDeletedAt() == datastoreEntity.getDeletedTime()
|| sqlEntity.getDeletedAt().equals(datastoreEntity.getDeletedTime()));
} catch (IllegalArgumentException iae) {
return false;
}
}

public static void main(String[] args) {
VerifyFeedbackSessionAttributes script = new VerifyFeedbackSessionAttributes();
script.doOperationRemotely();
}

}
11 changes: 11 additions & 0 deletions src/main/java/teammates/common/util/HibernateUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ public static <T extends BaseEntity> T get(Class<T> entityType, Object id) {
return HibernateUtil.getCurrentSession().get(entityType, id);
}

/**
* Return an instance, whose state may be lazily fetched.
* Id is the only field that is fetched eagerly.
* If there is no such persistent instance, EntityNotFoundException is thrown when the instance state
* is first accessed. This method can be used to increase performance, if it is known that the instance exists
* and fetching of other attributes of the entity is not necessary.
*/
public static <T extends BaseEntity> T getReference(Class<T> entityType, Object id) {
return HibernateUtil.getCurrentSession().getReference(entityType, id);
}

/**
* Return the persistent instance of the given entity class with the given natural id,
* or null if there is no such persistent instance.
Expand Down

0 comments on commit bb58c58

Please sign in to comment.