Fix glossary page performance issue with large number of terms(#19761) (#19771)

* Glossary Page Performance Issue with Large Number of Terms(#19761)

* Add glossary page performance unittest(#19761)

* Fix dup setFieldsInternal(#19761)

* Add fetch childrenCount (#19761)

* add comment about setFieldsInBulk(#19761)

* rename to countFindTo(#19761)

* fix findByIds return both id and json (#19761)

* turn up UT timeout (#19761)

---------

Co-authored-by: sonika-shah <58761340+sonika-shah@users.noreply.github.com>
This commit is contained in:
Juntao Zhang 2025-03-04 12:32:48 +08:00 committed by GitHub
parent 9e13ff6be8
commit 67a0795f7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 452 additions and 30 deletions

View File

@ -44,6 +44,7 @@ import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.jdbi.v3.core.Jdbi;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.EntityTimeSeriesInterface;
@ -397,6 +398,13 @@ public final class Entity {
return repository.getReference(id, include);
}
public static List<EntityReference> getEntityReferencesByIds(
@NonNull String entityType, @NonNull List<UUID> ids, Include include) {
EntityRepository<? extends EntityInterface> repository = getEntityRepository(entityType);
include = repository.supportsSoftDelete ? Include.ALL : include;
return repository.getReferences(ids, include);
}
public static EntityReference getEntityReferenceByName(
@NonNull String entityType, String fqn, Include include) {
if (fqn == null) {
@ -437,6 +445,23 @@ public final class Entity {
: getEntityByName(ref.getType(), ref.getFullyQualifiedName(), fields, include);
}
public static <T> List<T> getEntities(
List<EntityReference> refs, String fields, Include include) {
if (CollectionUtils.isEmpty(refs)) {
return new ArrayList<>();
}
EntityRepository<?> entityRepository = Entity.getEntityRepository(refs.get(0).getType());
@SuppressWarnings("unchecked")
List<T> entities =
(List<T>)
entityRepository.get(
null,
refs.stream().map(EntityReference::getId).toList(),
entityRepository.getFields(fields),
include);
return entities;
}
public static <T> T getEntityOrNull(
EntityReference entityReference, String field, Include include) {
if (entityReference == null) return null;
@ -478,6 +503,16 @@ public final class Entity {
return getEntityByName(entityType, fqn, fields, include, true);
}
public static <T> List<T> getEntityByNames(
String entityType, List<String> tagFQNs, String fields, Include include) {
EntityRepository<?> entityRepository = Entity.getEntityRepository(entityType);
@SuppressWarnings("unchecked")
List<T> entities =
(List<T>)
entityRepository.getByNames(null, tagFQNs, entityRepository.getFields(fields), include);
return entities;
}
/** Retrieve the corresponding entity repository for a given entity name. */
public static EntityRepository<? extends EntityInterface> getEntityRepository(
@NonNull String entityType) {

View File

@ -876,6 +876,13 @@ public interface CollectionDAO {
private String json;
}
@Getter
@Builder
class EntityRelationshipCount {
private UUID id;
private Integer count;
}
@Getter
@Builder
class EntityRelationshipObject {
@ -1009,6 +1016,19 @@ public interface CollectionDAO {
@Bind("fromEntityType") String fromEntityType,
@Bind("toEntityType") String toEntityType);
@SqlQuery(
"SELECT fromId, toId, fromEntity, toEntity, relation "
+ "FROM entity_relationship "
+ "WHERE fromId IN (<fromIds>) "
+ "AND relation = :relation "
+ "AND toEntity = :toEntityType "
+ "AND deleted = FALSE")
@UseRowMapper(RelationshipObjectMapper.class)
List<EntityRelationshipObject> findToBatch(
@BindList("fromIds") List<String> fromIds,
@Bind("relation") int relation,
@Bind("toEntityType") String toEntityType);
@SqlQuery(
"SELECT toId, toEntity, json FROM entity_relationship "
+ "WHERE fromId = :fromId AND fromEntity = :fromEntity AND relation = :relation AND toEntity = :toEntity")
@ -1019,6 +1039,17 @@ public interface CollectionDAO {
@Bind("relation") int relation,
@Bind("toEntity") String toEntity);
@SqlQuery(
"SELECT fromId, COUNT(toId) FROM entity_relationship "
+ "WHERE fromId IN (<fromIds>) AND fromEntity = :fromEntity AND relation = :relation AND toEntity = :toEntity "
+ "GROUP BY fromId")
@RegisterRowMapper(ToRelationshipCountMapper.class)
List<EntityRelationshipCount> countFindTo(
@BindList("fromIds") List<String> fromIds,
@Bind("fromEntity") String fromEntity,
@Bind("relation") int relation,
@Bind("toEntity") String toEntity);
@SqlQuery(
"SELECT COUNT(toId) FROM entity_relationship WHERE fromId = :fromId AND fromEntity = :fromEntity "
+ "AND relation IN (<relation>)")
@ -1300,6 +1331,16 @@ public interface CollectionDAO {
}
}
class ToRelationshipCountMapper implements RowMapper<EntityRelationshipCount> {
@Override
public EntityRelationshipCount map(ResultSet rs, StatementContext ctx) throws SQLException {
return EntityRelationshipCount.builder()
.id(UUID.fromString(rs.getString(1)))
.count(rs.getInt(2))
.build();
}
}
class RelationshipObjectMapper implements RowMapper<EntityRelationshipObject> {
@Override
public EntityRelationshipObject map(ResultSet rs, StatementContext ctx) throws SQLException {

View File

@ -19,11 +19,18 @@ import static org.openmetadata.service.jdbi3.ListFilter.escapeApostrophe;
import static org.openmetadata.service.jdbi3.locator.ConnectionType.MYSQL;
import static org.openmetadata.service.jdbi3.locator.ConnectionType.POSTGRES;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.SneakyThrows;
import org.apache.commons.collections4.CollectionUtils;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.customizer.BindMap;
import org.jdbi.v3.sqlobject.customizer.Define;
import org.jdbi.v3.sqlobject.statement.BatchChunkSize;
@ -148,6 +155,13 @@ public interface EntityDAO<T extends EntityInterface> {
String findById(
@Define("table") String table, @BindUUID("id") UUID id, @Define("cond") String cond);
@SqlQuery("SELECT id, json FROM <table> WHERE id IN (<ids>) <cond>")
@RegisterRowMapper(EntityIdJsonPairMapper.class)
List<EntityIdJsonPair> findByIds(
@Define("table") String table,
@BindList("ids") List<String> ids,
@Define("cond") String cond);
@SqlQuery("SELECT json FROM <table> WHERE <nameColumnHash> = :name <cond>")
String findByName(
@Define("table") String table,
@ -155,6 +169,14 @@ public interface EntityDAO<T extends EntityInterface> {
@BindFQN("name") String name,
@Define("cond") String cond);
@SqlQuery("SELECT <nameColumnHash>, json FROM <table> WHERE <nameColumnHash> IN (<names>) <cond>")
@RegisterRowMapper(EntityNameColumnHashJsonPairMapper.class)
List<EntityNameColumnHashJsonPair> findByNames(
@Define("table") String table,
@Define("nameColumnHash") String nameColumn,
@BindList("names") List<String> names,
@Define("cond") String cond);
@SqlQuery("SELECT count(<nameHashColumn>) FROM <table> <cond>")
int listCount(
@Define("table") String table,
@ -448,6 +470,19 @@ public interface EntityDAO<T extends EntityInterface> {
return findEntityById(id, Include.NON_DELETED);
}
default List<T> findEntitiesByIds(List<UUID> ids, Include include) {
if (CollectionUtils.isEmpty(ids)) {
return List.of();
}
return findByIds(
getTableName(),
ids.stream().map(UUID::toString).distinct().toList(),
getCondition(include))
.stream()
.map(pair -> jsonToEntity(pair.json, pair.id))
.toList();
}
default T findEntityByName(String fqn) {
return findEntityByName(fqn, Include.NON_DELETED);
}
@ -464,6 +499,17 @@ public interface EntityDAO<T extends EntityInterface> {
findByName(getTableName(), nameHashColumn, fqn, getCondition(include)), fqn);
}
@SneakyThrows
default List<T> findEntityByNames(List<String> entityFQNs, Include include) {
if (CollectionUtils.isEmpty(entityFQNs)) {
return List.of();
}
List<String> names = entityFQNs.stream().distinct().map(FullyQualifiedName::buildHash).toList();
return findByNames(getTableName(), getNameHashColumn(), names, getCondition(include)).stream()
.map(pair -> jsonToEntity(pair.json, pair.nameColumnHash))
.toList();
}
default T jsonToEntity(String json, Object identity) {
Class<T> clz = getEntityClass();
T entity = json != null ? JsonUtils.readValue(json, clz) : null;
@ -549,4 +595,22 @@ public interface EntityDAO<T extends EntityInterface> {
throw EntityNotFoundException.byMessage(entityNotFound(entityType, id));
}
}
record EntityNameColumnHashJsonPair(String nameColumnHash, String json) {}
class EntityNameColumnHashJsonPairMapper implements RowMapper<EntityNameColumnHashJsonPair> {
@Override
public EntityNameColumnHashJsonPair map(ResultSet r, StatementContext ctx) throws SQLException {
return new EntityNameColumnHashJsonPair(r.getString(1), r.getString(2));
}
}
record EntityIdJsonPair(UUID id, String json) {}
class EntityIdJsonPairMapper implements RowMapper<EntityIdJsonPair> {
@Override
public EntityIdJsonPair map(ResultSet r, StatementContext ctx) throws SQLException {
return new EntityIdJsonPair(UUID.fromString(r.getString(1)), r.getString(2));
}
}
}

View File

@ -120,6 +120,7 @@ import javax.ws.rs.core.UriInfo;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.jdbi.v3.sqlobject.transaction.Transaction;
@ -132,6 +133,7 @@ import org.openmetadata.schema.api.VoteRequest.VoteType;
import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.api.teams.CreateTeam;
import org.openmetadata.schema.configuration.AssetCertificationSettings;
import org.openmetadata.schema.entity.classification.Tag;
import org.openmetadata.schema.entity.data.Table;
import org.openmetadata.schema.entity.feed.Suggestion;
import org.openmetadata.schema.entity.teams.Team;
@ -257,6 +259,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
@Getter protected final boolean supportsStyle;
@Getter protected final boolean supportsLifeCycle;
@Getter protected final boolean supportsCertification;
@Getter protected final boolean supportsChildren;
protected final boolean supportsFollower;
protected final boolean supportsExtension;
protected final boolean supportsVotes;
@ -373,6 +376,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
this.patchFields.addField(allowedFields, FIELD_CERTIFICATION);
this.putFields.addField(allowedFields, FIELD_CERTIFICATION);
}
this.supportsChildren = allowedFields.contains(FIELD_CHILDREN);
Map<String, Pair<Boolean, BiConsumer<List<T>, Fields>>> fieldSupportMap = new HashMap<>();
@ -381,6 +385,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
fieldSupportMap.put(FIELD_DOMAIN, Pair.of(supportsDomain, this::fetchAndSetDomain));
fieldSupportMap.put(FIELD_REVIEWERS, Pair.of(supportsReviewers, this::fetchAndSetReviewers));
fieldSupportMap.put(FIELD_EXTENSION, Pair.of(supportsExtension, this::fetchAndSetExtension));
fieldSupportMap.put(FIELD_CHILDREN, Pair.of(supportsChildren, this::fetchAndSetChildren));
for (Entry<String, Pair<Boolean, BiConsumer<List<T>, Fields>>> entry :
fieldSupportMap.entrySet()) {
@ -390,8 +395,6 @@ public abstract class EntityRepository<T extends EntityInterface> {
if (supportsField) {
this.fieldFetchers.put(fieldName, fetcher);
this.patchFields.addField(allowedFields, fieldName);
this.putFields.addField(allowedFields, fieldName);
}
}
@ -474,7 +477,11 @@ public abstract class EntityRepository<T extends EntityInterface> {
}
}
@SuppressWarnings("unused")
/**
* The default behavior is to execute one by one. For batch execution, override this method in the subclass.
*
* @see GlossaryTermRepository#setInheritedFields(List, Fields) for an example implementation
*/
protected void setInheritedFields(List<T> entities, Fields fields) {
for (T entity : entities) {
setInheritedFields(entity, fields);
@ -633,6 +640,13 @@ public abstract class EntityRepository<T extends EntityInterface> {
return withHref(uriInfo, entityClone);
}
public final List<T> get(UriInfo uriInfo, List<UUID> ids, Fields fields, Include include) {
List<T> entities = find(ids, include);
setFieldsInBulk(fields, entities);
entities.forEach(entity -> withHref(uriInfo, entity));
return entities;
}
/**
* getReference is used for getting the entity references from the entity in the cache.
*/
@ -641,6 +655,11 @@ public abstract class EntityRepository<T extends EntityInterface> {
return find(id, include).getEntityReference();
}
public final List<EntityReference> getReferences(List<UUID> id, Include include)
throws EntityNotFoundException {
return find(id, include).stream().map(EntityInterface::getEntityReference).toList();
}
/**
* Find method is used for getting an entity only with core fields stored as JSON without any relational fields set
*/
@ -665,6 +684,10 @@ public abstract class EntityRepository<T extends EntityInterface> {
}
}
public final List<T> find(List<UUID> ids, Include include) {
return dao.findEntitiesByIds(ids, include);
}
public T getByName(UriInfo uriInfo, String fqn, Fields fields) {
return getByName(uriInfo, fqn, fields, NON_DELETED, false);
}
@ -695,6 +718,14 @@ public abstract class EntityRepository<T extends EntityInterface> {
return findByName(fqn, include).getEntityReference();
}
public final List<T> getByNames(
UriInfo uriInfo, List<String> entityFQNs, Fields fields, Include include) {
List<T> entities = findByNames(entityFQNs, include);
setFieldsInBulk(fields, entities);
entities.forEach(entity -> withHref(uriInfo, entity));
return entities;
}
public final T findByNameOrNull(String fqn, Include include) {
try {
return findByName(fqn, include);
@ -728,16 +759,19 @@ public abstract class EntityRepository<T extends EntityInterface> {
}
}
public List<T> findByNames(List<String> entityFQNs, Include include) {
return dao.findEntityByNames(entityFQNs, include);
}
public final List<T> listAll(Fields fields, ListFilter filter) {
// forward scrolling, if after == null then first page is being asked
List<String> jsons = dao.listAfter(filter, Integer.MAX_VALUE, "", "");
List<T> entities = new ArrayList<>();
for (String json : jsons) {
T entity = setFieldsInternal(JsonUtils.readValue(json, entityClass), fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
T entity = JsonUtils.readValue(json, entityClass);
entities.add(entity);
}
setFieldsInBulk(fields, entities);
return entities;
}
@ -745,17 +779,32 @@ public abstract class EntityRepository<T extends EntityInterface> {
ListFilter filter = new ListFilter(NON_DELETED);
List<String> jsons = listAllByParentFqn(parentFqn, filter);
List<T> entities = new ArrayList<>();
setFieldsInBulk(jsons, fields, entities);
return entities;
}
public void setFieldsInBulk(List<String> jsons, Fields fields, List<T> entities) {
for (String json : jsons) {
T entity = JsonUtils.readValue(json, entityClass);
entities.add(entity);
}
// TODO: Ensure consistent behavior with setFieldsInBulk when all repositories implement it
fetchAndSetFields(entities, fields);
setInheritedFields(entities, fields);
for (T entity : entities) {
clearFieldsInternal(entity, fields);
}
return entities;
}
/**
* Executes {@link #setFields} on a list of entities. By default, this method processes
* each entity individually. To enable batch processing, override this method in a subclass.
* <p>
* For efficient bulk processing, ensure all fields used in {@link #setFields}
* have corresponding batch processing methods, such as {@code fetchAndSetXXX}. For instance,
* if handling a domain field, implement {@link #fetchAndSetDomain}.
* <p>
* Example implementation can be found in {@link GlossaryTermRepository#setFieldsInBulk}.
*/
public void setFieldsInBulk(Fields fields, List<T> entities) {
for (T entity : entities) {
setFieldsInternal(entity, fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
}
@ -788,11 +837,11 @@ public abstract class EntityRepository<T extends EntityInterface> {
List<String> jsons = dao.listAfter(filter, limitParam + 1, afterName, afterId);
for (String json : jsons) {
T entity = setFieldsInternal(JsonUtils.readValue(json, entityClass), fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
entities.add(withHref(uriInfo, entity));
T entity = JsonUtils.readValue(json, entityClass);
entities.add(entity);
}
setFieldsInBulk(fields, entities);
entities.forEach(entity -> withHref(uriInfo, entity));
String beforeCursor;
String afterCursor = null;
@ -834,11 +883,12 @@ public abstract class EntityRepository<T extends EntityInterface> {
List<T> entities = new ArrayList<>();
for (String json : jsons) {
T entity = setFieldsInternal(JsonUtils.readValue(json, entityClass), fields);
setInheritedFields(entity, fields);
clearFieldsInternal(entity, fields);
entities.add(withHref(uriInfo, entity));
T entity = JsonUtils.readValue(json, entityClass);
entities.add(entity);
}
setFieldsInBulk(fields, entities);
entities.forEach(entity -> withHref(uriInfo, entity));
int total = dao.listCount(filter);
String beforeCursor = null;
@ -3943,13 +3993,8 @@ public abstract class EntityRepository<T extends EntityInterface> {
}
}
private void fetchAndSetFields(List<T> entities, Fields fields) {
for (String field : fields) {
BiConsumer<List<T>, Fields> fetcher = fieldFetchers.get(field);
if (fetcher != null) {
fetcher.accept(entities, fields);
}
}
protected void fetchAndSetFields(List<T> entities, Fields fields) {
fieldFetchers.values().forEach(fetcher -> fetcher.accept(entities, fields));
}
private void fetchAndSetOwners(List<T> entities, Fields fields) {
@ -4008,6 +4053,18 @@ public abstract class EntityRepository<T extends EntityInterface> {
}
}
protected void fetchAndSetChildren(List<T> entities, Fields fields) {
if (!fields.contains(FIELD_CHILDREN) || entities == null || entities.isEmpty()) {
return;
}
Map<UUID, List<EntityReference>> childrenMap = batchFetchChildren(entities);
for (T entity : entities) {
entity.setChildren(childrenMap.get(entity.getId()));
}
}
private void fetchAndSetReviewers(List<T> entities, Fields fields) {
if (!fields.contains(FIELD_REVIEWERS) || !supportsReviewers || entities.isEmpty()) {
return;
@ -4051,6 +4108,15 @@ public abstract class EntityRepository<T extends EntityInterface> {
List<CollectionDAO.TagUsageDAO.TagLabelWithFQNHash> tags =
daoCollection.tagUsageDAO().getTagsInternalBatch(entityFQNs);
List<String> tagFQNs =
tags.stream()
.distinct()
.map(CollectionDAO.TagUsageDAO.TagLabelWithFQNHash::getTagFQN)
.toList();
Map<String, TagLabel> tagFQNLabelMap =
EntityUtil.toTagLabels(TagLabelUtil.getTags(tagFQNs).toArray(new Tag[0])).stream()
.collect(Collectors.toMap(TagLabel::getTagFQN, Function.identity()));
// Map from targetFQNHash to targetFQN
Map<String, String> fqnHashToFqnMap =
entityFQNs.stream().collect(Collectors.toMap(FullyQualifiedName::buildHash, fqn -> fqn));
@ -4062,7 +4128,8 @@ public abstract class EntityRepository<T extends EntityInterface> {
String targetFQN = fqnHashToFqnMap.get(targetFQNHash);
if (targetFQN != null) {
tagsMap.computeIfAbsent(targetFQN, k -> new ArrayList<>()).add(tagWithHash.toTagLabel());
TagLabel tagLabel = tagFQNLabelMap.get(tagWithHash.getTagFQN());
tagsMap.computeIfAbsent(targetFQN, k -> new ArrayList<>()).add(tagLabel);
}
}
@ -4152,6 +4219,36 @@ public abstract class EntityRepository<T extends EntityInterface> {
return result;
}
private Map<UUID, List<EntityReference>> batchFetchChildren(List<T> entities) {
List<CollectionDAO.EntityRelationshipObject> records =
daoCollection
.relationshipDAO()
.findToBatch(
entityListToStrings(entities), Relationship.CONTAINS.ordinal(), entityType);
Map<UUID, List<EntityReference>> childrenMap = new HashMap<>();
if (CollectionUtils.isEmpty(records)) {
return childrenMap;
}
Map<String, EntityReference> idReferenceMap =
Entity.getEntityReferencesByIds(
records.get(0).getToEntity(),
records.stream().map(e -> UUID.fromString(e.getToId())).distinct().toList(),
ALL)
.stream()
.collect(Collectors.toMap(e -> e.getId().toString(), Function.identity()));
for (CollectionDAO.EntityRelationshipObject rec : records) {
UUID entityId = UUID.fromString(rec.getFromId());
EntityReference childrenRef = idReferenceMap.get(rec.getToId());
childrenMap.computeIfAbsent(entityId, k -> new ArrayList<>()).add(childrenRef);
}
return childrenMap;
}
private List<String> entityListToStrings(List<T> entities) {
return entities.stream().map(EntityInterface::getId).map(UUID::toString).toList();
}

View File

@ -41,6 +41,7 @@ import static org.openmetadata.service.util.EntityUtil.termReferenceMatch;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -57,6 +58,7 @@ import java.util.stream.Collectors;
import javax.json.JsonPatch;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.common.utils.CommonUtil;
@ -121,8 +123,10 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
UPDATE_FIELDS);
supportsSearch = true;
renameAllowed = true;
fieldFetchers.put("relatedTerms", this::fetchAndSetRelatedTerms);
fieldFetchers.put("parent", this::fetchAndSetParentOrGlossary);
fieldFetchers.put("relatedTerms", this::fetchAndSetRelatedTerms);
fieldFetchers.put("usageCount", this::fetchAndSetUsageCount);
fieldFetchers.put("childrenCount", this::fetchAndSetChildrenCount);
}
@Override
@ -136,6 +140,15 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
fields.contains("childrenCount") ? getChildrenCount(entity) : entity.getChildrenCount());
}
@Override
public void setFieldsInBulk(Fields fields, List<GlossaryTerm> entities) {
fetchAndSetFields(entities, fields);
setInheritedFields(entities, fields);
for (GlossaryTerm entity : entities) {
clearFieldsInternal(entity, fields);
}
}
@Override
public void clearFields(GlossaryTerm entity, Fields fields) {
entity.setRelatedTerms(fields.contains("relatedTerms") ? entity.getRelatedTerms() : null);
@ -151,6 +164,25 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
inheritReviewers(glossaryTerm, fields, parent);
}
@Override
public void setInheritedFields(List<GlossaryTerm> glossaryTerms, Fields fields) {
List<? extends EntityInterface> parents =
getParentEntities(glossaryTerms, "owners,domain,reviewers");
Map<UUID, EntityInterface> parentMap =
parents.stream().collect(Collectors.toMap(EntityInterface::getId, e -> e));
for (GlossaryTerm glossaryTerm : glossaryTerms) {
EntityInterface parent = null;
if (glossaryTerm.getParent() != null) {
parent = parentMap.get(glossaryTerm.getParent().getId());
} else if (glossaryTerm.getGlossary() != null) {
parent = parentMap.get(glossaryTerm.getGlossary().getId());
}
inheritOwners(glossaryTerm, fields, parent);
inheritDomain(glossaryTerm, fields, parent);
inheritReviewers(glossaryTerm, fields, parent);
}
}
private Integer getUsageCount(GlossaryTerm term) {
return daoCollection
.tagUsageDAO()
@ -218,6 +250,24 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
.withReviewers(reviewers);
}
@Override
public void storeEntities(List<GlossaryTerm> entities) {
List<GlossaryTerm> entitiesToStore = new ArrayList<>();
Gson gson = new Gson();
for (GlossaryTerm entity : entities) {
EntityReference glossary = entity.getGlossary();
EntityReference parentTerm = entity.getParent();
List<EntityReference> reviewers = entity.getReviewers();
String jsonCopy = gson.toJson(entity.withGlossary(null).withParent(null).withReviewers(null));
entitiesToStore.add(gson.fromJson(jsonCopy, GlossaryTerm.class));
// restore the relationships
entity.withGlossary(glossary).withParent(parentTerm).withReviewers(reviewers);
}
storeMany(entitiesToStore);
}
@Override
public void storeRelationships(GlossaryTerm entity) {
addGlossaryRelationship(entity);
@ -625,6 +675,24 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
: Entity.getEntity(entity.getGlossary(), fields, Include.ALL);
}
public List<EntityInterface> getParentEntities(List<GlossaryTerm> entities, String fields) {
List<EntityInterface> result = new ArrayList<>();
if (CollectionUtils.isEmpty(entities)) {
return result;
}
List<EntityReference> parents =
entities.stream().map(GlossaryTerm::getParent).filter(Objects::nonNull).distinct().toList();
result.addAll(Entity.getEntities(parents, fields, Include.ALL));
List<EntityReference> glossaries =
entities.stream()
.map(GlossaryTerm::getGlossary)
.filter(Objects::nonNull)
.distinct()
.toList();
result.addAll(Entity.getEntities(glossaries, fields, Include.ALL));
return result;
}
private void addGlossaryRelationship(GlossaryTerm term) {
Relationship relationship = term.getParent() != null ? Relationship.HAS : Relationship.CONTAINS;
addRelationship(
@ -841,11 +909,12 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
}
private void fetchAndSetParentOrGlossary(List<GlossaryTerm> terms, Fields fields) {
if (terms == null || terms.isEmpty() || (!fields.contains("parent"))) {
if (terms == null || terms.isEmpty()) {
return;
}
List<String> entityIds = terms.stream().map(GlossaryTerm::getId).map(UUID::toString).toList();
List<String> entityIds =
terms.stream().map(GlossaryTerm::getId).map(UUID::toString).distinct().toList();
List<CollectionDAO.EntityRelationshipObject> parentRecords =
daoCollection
@ -919,6 +988,39 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
}
}
private void fetchAndSetUsageCount(List<GlossaryTerm> entities, Fields fields) {
if (!fields.contains("usageCount") || entities.isEmpty()) {
return;
}
// TODO: modify to use a single db query
for (GlossaryTerm entity : entities) {
entity.withUsageCount(getUsageCount(entity));
}
}
private void fetchAndSetChildrenCount(List<GlossaryTerm> entities, Fields fields) {
if (!fields.contains("childrenCount") || entities.isEmpty()) {
return;
}
List<String> termIds =
entities.stream().map(GlossaryTerm::getId).map(UUID::toString).distinct().toList();
Map<UUID, Integer> termIdCountMap =
daoCollection
.relationshipDAO()
.countFindTo(termIds, GLOSSARY_TERM, Relationship.CONTAINS.ordinal(), GLOSSARY_TERM)
.stream()
.collect(
Collectors.toMap(
CollectionDAO.EntityRelationshipCount::getId,
CollectionDAO.EntityRelationshipCount::getCount));
for (GlossaryTerm entity : entities) {
entity.setChildrenCount(
termIdCountMap.getOrDefault(entity.getId(), entity.getChildrenCount()));
}
}
/** Handles entity updated from PUT and POST operation. */
public class GlossaryTermUpdater extends EntityUpdater {
public GlossaryTermUpdater(GlossaryTerm original, GlossaryTerm updated, Operation operation) {

View File

@ -416,6 +416,36 @@ public class GlossaryTermResource extends EntityResource<GlossaryTerm, GlossaryT
return create(uriInfo, securityContext, term);
}
@POST
@Path("/createMany")
@Operation(
operationId = "createManyGlossaryTerm",
summary = "Create multiple glossary terms at once",
description = "Create multiple new glossary terms.",
responses = {
@ApiResponse(
responseCode = "200",
description = "The glossary term",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = GlossaryTerm.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createMany(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid List<CreateGlossaryTerm> creates) {
List<GlossaryTerm> terms =
creates.stream()
.map(
create ->
mapper.createToEntity(create, securityContext.getUserPrincipal().getName()))
.toList();
List<GlossaryTerm> result = repository.createMany(uriInfo, terms);
return Response.ok(result).build();
}
@PATCH
@Path("/{id}")
@Operation(

View File

@ -52,6 +52,10 @@ public class TagLabelUtil {
return Entity.getEntityByName(Entity.TAG, tagFqn, "", NON_DELETED);
}
public static List<Tag> getTags(List<String> tagFQNs) {
return Entity.getEntityByNames(Entity.TAG, tagFQNs, "", NON_DELETED);
}
public static Glossary getGlossary(String glossaryName) {
return Entity.getEntityByName(Entity.GLOSSARY, glossaryName, "", NON_DELETED);
}

View File

@ -20,6 +20,7 @@ import static java.util.Collections.emptyList;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static javax.ws.rs.core.Response.Status.OK;
import static org.junit.jupiter.api.Assertions.*;
import static org.openmetadata.common.utils.CommonUtil.listOf;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
@ -56,6 +57,8 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException;
@ -1057,6 +1060,21 @@ public class GlossaryTermResourceTest extends EntityResourceTest<GlossaryTerm, C
}
}
@Test
void test_performance_listEntities() throws IOException {
Glossary glossary = createGlossary("词汇表三", null, null);
List<Map<String, Object>> result =
createTerms(glossary, IntStream.range(0, 500).mapToObj(i -> "term" + i).toList());
Map<String, String> queryParams = new HashMap<>();
queryParams.put("fields", "children,relatedTerms,reviewers,tags");
queryParams.put("limit", "10000");
queryParams.put("directChildrenOf", "词汇表三");
ResultList<GlossaryTerm> list =
assertTimeout(Duration.ofSeconds(3), () -> listEntities(queryParams, ADMIN_AUTH_HEADERS));
assertEquals(result.size(), list.getData().size());
}
public GlossaryTerm createTerm(Glossary glossary, GlossaryTerm parent, String termName)
throws IOException {
return createTerm(glossary, parent, termName, glossary.getReviewers());
@ -1086,6 +1104,25 @@ public class GlossaryTermResourceTest extends EntityResourceTest<GlossaryTerm, C
return createAndCheckEntity(createGlossaryTerm, createdBy);
}
private List<Map<String, Object>> createTerms(Glossary glossary, List<String> termNames)
throws HttpResponseException {
String pathUrl = "/createMany/";
String glossaryFqn = getFqn(glossary);
WebTarget target = getCollection().path(pathUrl);
List<CreateGlossaryTerm> createGlossaryTerms =
termNames.stream()
.map(
name ->
createRequest(name, "d", "", null)
.withRelatedTerms(null)
.withSynonyms(List.of("performance1", "performance2"))
.withStyle(new Style().withColor("#FF5733").withIconURL("https://img"))
.withGlossary(glossaryFqn))
.toList();
return TestUtils.post(
target, createGlossaryTerms, List.class, OK.getStatusCode(), ADMIN_AUTH_HEADERS);
}
public void assertContains(List<GlossaryTerm> expectedTerms, List<GlossaryTerm> actualTerms)
throws HttpResponseException {
assertEquals(expectedTerms.size(), actualTerms.size());
@ -1341,6 +1378,7 @@ public class GlossaryTermResourceTest extends EntityResourceTest<GlossaryTerm, C
// List children glossary terms with term1 as the parent and getting immediate children only
Map<String, String> queryParams = new HashMap<>();
queryParams.put("directChildrenOf", term1.getFullyQualifiedName());
queryParams.put("fields", "childrenCount,children");
List<GlossaryTerm> children = listEntities(queryParams, ADMIN_AUTH_HEADERS).getData();
assertEquals(term1.getChildren().size(), children.size());
@ -1348,10 +1386,21 @@ public class GlossaryTermResourceTest extends EntityResourceTest<GlossaryTerm, C
for (GlossaryTerm responseChild : children) {
assertTrue(
responseChild.getFullyQualifiedName().startsWith(responseChild.getFullyQualifiedName()));
if (responseChild.getChildren() == null) {
assertNull(responseChild.getChildrenCount());
} else {
assertEquals(responseChild.getChildren().size(), responseChild.getChildrenCount());
}
}
GlossaryTerm response = getEntity(term1.getId(), "childrenCount", ADMIN_AUTH_HEADERS);
assertEquals(term1.getChildren().size(), response.getChildrenCount());
queryParams = new HashMap<>();
queryParams.put("directChildrenOf", glossary1.getFullyQualifiedName());
queryParams.put("fields", "childrenCount");
children = listEntities(queryParams, ADMIN_AUTH_HEADERS).getData();
assertEquals(term1.getChildren().size(), children.get(0).getChildrenCount());
}
public Glossary createGlossary(