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

feat: Migrate TEA change logs into new table [DHIS2-18474] #19321

Merged
merged 8 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ public int deleteSoftDeletedTrackedEntities() {
"delete from relationshipitem where trackedentityid in " + teSelect,
"delete from trackedentityattributevalue where trackedentityid in " + teSelect,
"delete from trackedentityattributevalueaudit where trackedentityid in " + teSelect,
"delete from trackedentitychangelog where trackedentityid in " + teSelect,
"delete from trackedentityprogramowner where trackedentityid in " + teSelect,
"delete from programtempowner where trackedentityid in " + teSelect,
"delete from programtempownershipaudit where trackedentityid in " + teSelect,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
<class name="org.hisp.dhis.tracker.export.trackedentity.TrackedEntityChangeLog"
table="trackedentitychangelog">

<id name="id" column="trackedentitychangelogid">
<generator class="sequence">
<param name="sequence_name">trackedentitychangelog_sequence</param>
</generator>
</id>

<many-to-one name="trackedEntity" class="org.hisp.dhis.trackedentity.TrackedEntity"
column="trackedentityid"
foreign-key="fk_trackedentitychangelog_trackedentityinstanceid" not-null="true"/>

<many-to-one name="trackedEntityAttribute" class="org.hisp.dhis.trackedentity.TrackedEntityAttribute"
column="trackedentityattributeid"
foreign-key="fk_trackedentitychangelog_trackedentityattributeid" not-null="true"/>

<property name="previousValue" column="previousvalue" access="property" length="50000"/>

<property name="currentValue" column="currentvalue" access="property" length="50000"/>

<property name="changeLogType" column="changelogtype" length="100" not-null="true">
<type name="org.hibernate.type.EnumType">
<param name="enumClass">org.hisp.dhis.changelog.ChangeLogType</param>
<param name="useNamed">true</param>
<param name="type">12</param>
</type>
</property>

<property name="created" type="timestamp" not-null="true"/>

<property name="createdByUsername" column="createdby" type="string"/>

<many-to-one name="createdBy" class="org.hisp.dhis.user.User" column="createdby" insert="false"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't rememver exactly how the conversation ended, but didn't we agree that we were going to use user UID here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did, but once the platform refactors the user logic so that users can no longer be deleted from the user_info table, right? Only disabling a user should be possible.
One of the requirements for the change logs is that we'll always show the username, regardless of whether the user is deleted or not. We can't achieve that with the current setup if we use the UID here.

update="false" property-ref="username"/>

<many-to-one name="programAttribute" class="org.hisp.dhis.program.ProgramTrackedEntityAttribute"
column="trackedentityattributeid" insert="false" update="false" access="field" property-ref="attribute" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making sure that "trackedentityattributeid" was indeed the intention here. Even for ProgramAttribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's intentional, if not Hibernate uses by default the program_attributes table PK.

</class>
</hibernate-mapping>
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public List<String> canRead(@Nonnull UserDetails user, TrackedEntity trackedEnti
private List<String> canRead(
UserDetails user, TrackedEntity trackedEntity, List<Program> programs) {

if (null == trackedEntity) {
if (trackedEntity == null) {
return List.of();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
package org.hisp.dhis.tracker.export.trackedentity;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.hisp.dhis.changelog.ChangeLogType;
import org.hisp.dhis.common.BaseIdentifiableObject;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.common.UID;
Expand Down Expand Up @@ -63,17 +65,40 @@ public class DefaultTrackedEntityChangeLogService implements TrackedEntityChange

private final TrackerAccessManager trackerAccessManager;

private final JdbcTrackedEntityChangeLogStore jdbcTrackedEntityChangeLogStore;

private final TrackedEntityAttributeValueChangeLogStore attributeValueChangeLogStore;

private final HibernateTrackedEntityChangeLogStore hibernateTrackedEntityChangeLogStore;

@Override
@Transactional
public void addTrackedEntityAttributeValueChangeLog(
TrackedEntityAttributeValueChangeLog attributeValueChangeLog) {
attributeValueChangeLogStore.addTrackedEntityAttributeValueChangeLog(attributeValueChangeLog);
}

@Transactional
@Override
public void addTrackedEntityChangeLog(
TrackedEntity trackedEntity,
TrackedEntityAttribute trackedEntityAttribute,
String previousValue,
String currentValue,
ChangeLogType changeLogType,
String username) {

TrackedEntityChangeLog trackedEntityChangeLog =
new TrackedEntityChangeLog(
trackedEntity,
trackedEntityAttribute,
previousValue,
currentValue,
changeLogType,
new Date(),
username);

hibernateTrackedEntityChangeLogStore.addTrackedEntityChangeLog(trackedEntityChangeLog);
}

@Override
@Transactional(readOnly = true)
public List<TrackedEntityAttributeValueChangeLog> getTrackedEntityAttributeValueChangeLogs(
Expand Down Expand Up @@ -110,8 +135,8 @@ public int countTrackedEntityAttributeValueChangeLogs(

@Override
@Transactional
public void deleteTrackedEntityAttributeValueChangeLogs(TrackedEntity trackedEntity) {
attributeValueChangeLogStore.deleteTrackedEntityAttributeValueChangeLogs(trackedEntity);
public void deleteTrackedEntityChangeLogs(TrackedEntity trackedEntity) {
hibernateTrackedEntityChangeLogStore.deleteTrackedEntityChangeLogs(trackedEntity);
}

@Override
Expand All @@ -137,18 +162,14 @@ public Page<TrackedEntityChangeLog> getTrackedEntityChangeLog(
trackedEntityAttributes = validateTrackedEntityAttributes(trackedEntity);
}

return jdbcTrackedEntityChangeLogStore.getTrackedEntityChangeLog(
trackedEntityUid,
trackedEntityAttributes,
programUid,
operationParams.getOrder(),
pageParams);
return hibernateTrackedEntityChangeLogStore.getTrackedEntityChangeLogs(
trackedEntityUid, programUid, trackedEntityAttributes, operationParams, pageParams);
}

@Override
@Transactional(readOnly = true)
public Set<String> getOrderableFields() {
return jdbcTrackedEntityChangeLogStore.getOrderableFields();
return hibernateTrackedEntityChangeLogStore.getOrderableFields();
}

private Program validateProgram(String programUid) throws NotFoundException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2004-2024, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of the HISP project nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.tracker.export.trackedentity;

import static java.util.Map.entry;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.hibernate.Session;
import org.hisp.dhis.changelog.ChangeLogType;
import org.hisp.dhis.common.SortDirection;
import org.hisp.dhis.common.UID;
import org.hisp.dhis.program.UserInfoSnapshot;
import org.hisp.dhis.trackedentity.TrackedEntity;
import org.hisp.dhis.trackedentity.TrackedEntityAttribute;
import org.hisp.dhis.tracker.export.Order;
import org.hisp.dhis.tracker.export.Page;
import org.hisp.dhis.tracker.export.PageParams;
import org.springframework.stereotype.Repository;

@Repository("org.hisp.dhis.tracker.export.trackedentity.HibernateTrackedEntityChangeLogStore")
public class HibernateTrackedEntityChangeLogStore {
private static final String COLUMN_CHANGELOG_CREATED = "tecl.created";
private static final String DEFAULT_ORDER =
COLUMN_CHANGELOG_CREATED + " " + SortDirection.DESC.getValue();

private static final Map<String, String> ORDERABLE_FIELDS =
Map.ofEntries(entry("createdAt", COLUMN_CHANGELOG_CREATED));

private final EntityManager entityManager;
private final Session session;

public HibernateTrackedEntityChangeLogStore(EntityManager entityManager) {
this.entityManager = entityManager;
this.session = entityManager.unwrap(Session.class);
}

public void addTrackedEntityChangeLog(TrackedEntityChangeLog trackedEntityChangeLog) {
session.save(trackedEntityChangeLog);
}

public Page<TrackedEntityChangeLog> getTrackedEntityChangeLogs(
@Nonnull UID trackedEntity,
@Nullable UID program,
@Nonnull Set<String> attributes,
muilpp marked this conversation as resolved.
Show resolved Hide resolved
@Nonnull TrackedEntityChangeLogOperationParams operationParams,
@Nonnull PageParams pageParams) {

String hql =
"""
select tecl.trackedEntity,
tecl.trackedEntityAttribute,
tecl.previousValue,
tecl.currentValue,
tecl.changeLogType,
tecl.created,
tecl.createdByUsername,
u.firstName,
u.surname,
u.uid
from TrackedEntityChangeLog tecl
join tecl.trackedEntity t
join tecl.trackedEntityAttribute tea
left join tecl.createdBy u
""";

if (program != null) {
hql +=
"""
join tecl.programAttribute pa
join pa.program p
where tecl.changeLogType in ('CREATE', 'UPDATE', 'DELETE')
teleivo marked this conversation as resolved.
Show resolved Hide resolved
and t.uid = :trackedEntity
and p.uid = :program
""";

} else {
hql +=
"""
where tecl.changeLogType in ('CREATE', 'UPDATE', 'DELETE')
and t.uid = :trackedEntity
""";
}

if (!attributes.isEmpty()) {
hql +=
"""
and tea.uid in (:attributes)
""";
}

hql += String.format("order by %s".formatted(sortExpressions(operationParams.getOrder())));
Dismissed Show dismissed Hide dismissed

Query query = entityManager.createQuery(hql);
query.setParameter("trackedEntity", trackedEntity.getValue());

if (program != null) {
query.setParameter("program", program.getValue());
}

if (!attributes.isEmpty()) {
query.setParameter("attributes", attributes);
}

query.setFirstResult((pageParams.getPage() - 1) * pageParams.getPageSize());
query.setMaxResults(pageParams.getPageSize() + 1);

List<Object[]> results = query.getResultList();
List<TrackedEntityChangeLog> trackedEntityChangeLogs =
results.stream()
.map(
row -> {
TrackedEntity t = (TrackedEntity) row[0];
TrackedEntityAttribute trackedEntityAttribute = (TrackedEntityAttribute) row[1];
String previousValue = (String) row[2];
String currentValue = (String) row[3];
ChangeLogType changeLogType = (ChangeLogType) row[4];
Date created = (Date) row[5];

UserInfoSnapshot createdBy =
new UserInfoSnapshot((String) row[6], (String) row[7], (String) row[8]);
createdBy.setUid((String) row[9]);

return new TrackedEntityChangeLog(
t,
trackedEntityAttribute,
previousValue,
currentValue,
changeLogType,
created,
createdBy);
})
.toList();

Integer prevPage = pageParams.getPage() > 1 ? pageParams.getPage() - 1 : null;
if (trackedEntityChangeLogs.size() > pageParams.getPageSize()) {
return Page.withPrevAndNext(
trackedEntityChangeLogs.subList(0, pageParams.getPageSize()),
pageParams.getPage(),
pageParams.getPageSize(),
prevPage,
pageParams.getPage() + 1);
}

return Page.withPrevAndNext(
trackedEntityChangeLogs, pageParams.getPage(), pageParams.getPageSize(), prevPage, null);
}

public void deleteTrackedEntityChangeLogs(TrackedEntity trackedEntity) {
org.hibernate.query.Query<?> query =
session.createQuery("delete TrackedEntityChangeLog where trackedEntity = :trackedEntity");
query.setParameter("trackedEntity", trackedEntity);
query.executeUpdate();
}

public Set<String> getOrderableFields() {
return ORDERABLE_FIELDS.keySet();
}

private static String sortExpressions(Order order) {
if (order == null) {
return DEFAULT_ORDER;
}

StringBuilder orderBuilder = new StringBuilder();
orderBuilder.append(ORDERABLE_FIELDS.get(order.getField()));
orderBuilder.append(" ");
orderBuilder.append(order.getDirection().getValue());

if (!order.getField().equals("createdAt")) {
orderBuilder.append(", ").append(DEFAULT_ORDER);
}

return orderBuilder.toString();
}
}
Loading
Loading