This is an automated email from the ASF dual-hosted git repository.

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new e85863b7bf [#7177] feat(core): Implement cache integration with 
relational (#7354)
e85863b7bf is described below

commit e85863b7bf72922941fa2d5d88b0c22d546dc4fc
Author: Lord of Abyss <[email protected]>
AuthorDate: Sun Jun 15 01:25:41 2025 +0800

    [#7177] feat(core): Implement cache integration with relational (#7354)
    
    ### What changes were proposed in this pull request?
    
    Change:
    - Updated EntityCache interface: `get` no longer delegates to underlying
    store
    - Enhanced RelationalEntityStore to support entity caching
    - Revised multiple test cases to reflect caching behavior and validate
    correctness
    
    Tasks:
    - [X] Implement the methods related to the `SupportsEntityStoreCache`
    and `SupportsRelationEntityCache` interface in `CaffeineEntityCache`.
    - [X] Implement cache integration with relational store and add basic
    unit tests.
    - [ ] Add unit tests for both index and cache.
    - [ ] Use JCStress to perform multi-threaded testing on
    `CaffeineEntityCache` and the `CacheIndex`.
    
    ### Why are the changes needed?
    
    Fix: #7177
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    local test.
---
 .../hadoop/TestHadoopCatalogOperations.java        |   7 +
 .../catalog/kafka/TestKafkaCatalogOperations.java  |   7 +
 .../catalog/model/TestModelCatalogOperations.java  |   7 +
 .../apache/gravitino/cache/BaseEntityCache.java    |  67 +--
 .../gravitino/cache/CaffeineEntityCache.java       | 161 ++-----
 .../org/apache/gravitino/cache/NoOpsCache.java     | 156 +++++++
 .../gravitino/cache/SupportsEntityStoreCache.java  |  25 +-
 .../cache/SupportsRelationEntityCache.java         |  19 -
 .../storage/relational/RelationalEntityStore.java  |  49 +-
 .../authorization/TestAccessControlManager.java    |   7 +
 .../gravitino/authorization/TestOwnerManager.java  |   7 +
 .../gravitino/cache/TestCaffeineEntityCache.java   | 396 ++++++++++++++++
 .../gravitino/storage/TestEntityStorage.java       |   8 +
 .../org/apache/gravitino/tag/TestTagManager.java   |   9 +-
 .../java/org/apache/gravitino/utils/TestUtil.java  | 502 +++++++++++++++++++++
 15 files changed, 1203 insertions(+), 224 deletions(-)

diff --git 
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
 
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
index 2b47097985..4b1b936632 100644
--- 
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
+++ 
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/TestHadoopCatalogOperations.java
@@ -62,6 +62,7 @@ import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.GravitinoEnv;
@@ -219,6 +220,12 @@ public class TestHadoopCatalogOperations {
     when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
     when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 1000L);
+    // Fix cache config for test
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
 
     store = EntityStoreFactory.createEntityStore(config);
     store.initialize(config);
diff --git 
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
 
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
index d09a33a634..e820b8dd4d 100644
--- 
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
+++ 
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java
@@ -51,6 +51,7 @@ import java.time.Instant;
 import java.util.Map;
 import org.apache.commons.io.FileUtils;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.NameIdentifier;
@@ -150,6 +151,12 @@ public class TestKafkaCatalogOperations extends 
KafkaClusterEmbedded {
     when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
     when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 1000L);
+    // Fix cache config for test
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
 
     // Mock
     MetalakeMetaService metalakeMetaService = 
MetalakeMetaService.getInstance();
diff --git 
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
 
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
index 1d23622bfa..709999fd81 100644
--- 
a/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
+++ 
b/catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java
@@ -49,6 +49,7 @@ import java.util.stream.Collectors;
 import org.apache.commons.io.FileUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.NameIdentifier;
@@ -119,6 +120,12 @@ public class TestModelCatalogOperations {
     when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
     when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 1000L);
+    // Fix cache config for test
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
 
     store = EntityStoreFactory.createEntityStore(config);
     store.initialize(config);
diff --git a/core/src/main/java/org/apache/gravitino/cache/BaseEntityCache.java 
b/core/src/main/java/org/apache/gravitino/cache/BaseEntityCache.java
index 37ceab2448..b61cc93454 100644
--- a/core/src/main/java/org/apache/gravitino/cache/BaseEntityCache.java
+++ b/core/src/main/java/org/apache/gravitino/cache/BaseEntityCache.java
@@ -20,28 +20,11 @@
 package org.apache.gravitino.cache;
 
 import com.google.common.base.Preconditions;
-import java.util.Collections;
-import java.util.EnumMap;
 import java.util.List;
-import java.util.Map;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Entity;
-import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
-import org.apache.gravitino.meta.BaseMetalake;
-import org.apache.gravitino.meta.CatalogEntity;
-import org.apache.gravitino.meta.ColumnEntity;
-import org.apache.gravitino.meta.FilesetEntity;
-import org.apache.gravitino.meta.ModelEntity;
-import org.apache.gravitino.meta.ModelVersionEntity;
-import org.apache.gravitino.meta.RoleEntity;
-import org.apache.gravitino.meta.SchemaEntity;
-import org.apache.gravitino.meta.TableEntity;
-import org.apache.gravitino.meta.TagEntity;
-import org.apache.gravitino.meta.TopicEntity;
-import org.apache.gravitino.meta.UserEntity;
-import org.apache.gravitino.storage.relational.RelationalEntityStore;
 
 /**
  * An abstract class that provides a basic implementation for the {@link 
EntityCache} interface.
@@ -49,57 +32,17 @@ import 
org.apache.gravitino.storage.relational.RelationalEntityStore;
  * other entity cache implementations.
  */
 public abstract class BaseEntityCache implements EntityCache {
-  private static final Map<Entity.EntityType, Class<?>> ENTITY_CLASS_MAP;
-  // The entity store used by the cache, initialized through the constructor.
-  protected final RelationalEntityStore entityStore;
   protected final Config cacheConfig;
 
-  static {
-    Map<Entity.EntityType, Class<?>> map = new 
EnumMap<>(Entity.EntityType.class);
-    map.put(Entity.EntityType.METALAKE, BaseMetalake.class);
-    map.put(Entity.EntityType.CATALOG, CatalogEntity.class);
-    map.put(Entity.EntityType.SCHEMA, SchemaEntity.class);
-    map.put(Entity.EntityType.TABLE, TableEntity.class);
-    map.put(Entity.EntityType.FILESET, FilesetEntity.class);
-    map.put(Entity.EntityType.MODEL, ModelEntity.class);
-    map.put(Entity.EntityType.TOPIC, TopicEntity.class);
-    map.put(Entity.EntityType.TAG, TagEntity.class);
-    map.put(Entity.EntityType.MODEL_VERSION, ModelVersionEntity.class);
-    map.put(Entity.EntityType.COLUMN, ColumnEntity.class);
-    map.put(Entity.EntityType.USER, UserEntity.class);
-    map.put(Entity.EntityType.GROUP, Entity.class);
-    map.put(Entity.EntityType.ROLE, RoleEntity.class);
-    ENTITY_CLASS_MAP = Collections.unmodifiableMap(map);
-  }
-
   /**
    * Constructs a new {@link BaseEntityCache} instance.
    *
-   * @param entityStore The entity store to be used by the cache.
+   * @param config The cache configuration
    */
-  public BaseEntityCache(Config config, EntityStore entityStore) {
+  public BaseEntityCache(Config config) {
     Preconditions.checkArgument(config != null, "Config must not be null");
-    Preconditions.checkArgument(entityStore != null, "EntityStore must not be 
null");
 
     this.cacheConfig = config;
-    this.entityStore = (RelationalEntityStore) entityStore;
-  }
-
-  /**
-   * Returns the class of the entity based on its type.
-   *
-   * @param type The entity type
-   * @return The class of the entity
-   * @throws IllegalArgumentException if the entity type is not supported
-   */
-  @SuppressWarnings("unchecked")
-  public static <E extends Entity & HasIdentifier> Class<E> 
getEntityClass(Entity.EntityType type) {
-    Preconditions.checkArgument(type != null, "EntityType must not be null");
-
-    Class<?> clazz = ENTITY_CLASS_MAP.get(type);
-    Preconditions.checkArgument(clazz != null, "Unsupported EntityType: " + 
type);
-
-    return (Class<E>) clazz;
   }
 
   /**
@@ -108,7 +51,7 @@ public abstract class BaseEntityCache implements EntityCache 
{
    * @param entity The {@link Entity} instance.
    * @return The {@link NameIdentifier} of the entity
    */
-  protected static NameIdentifier getIdentFromEntity(Entity entity) {
+  public static NameIdentifier getIdentFromEntity(Entity entity) {
     validateEntityHasIdentifier(entity);
     HasIdentifier hasIdentifier = (HasIdentifier) entity;
 
@@ -133,7 +76,7 @@ public abstract class BaseEntityCache implements EntityCache 
{
    * @param <E> The type of the entities in the new list.
    */
   @SuppressWarnings("unchecked")
-  protected static <E extends Entity & HasIdentifier> List<E> 
convertEntity(List<Entity> entities) {
+  public static <E extends Entity & HasIdentifier> List<E> 
convertEntities(List<Entity> entities) {
     entities.forEach(BaseEntityCache::validateEntityHasIdentifier);
 
     return (List<E>) (List<? extends Entity>) entities;
@@ -147,7 +90,7 @@ public abstract class BaseEntityCache implements EntityCache 
{
    * @param <E> The type of the new entity.
    */
   @SuppressWarnings("unchecked")
-  protected static <E extends Entity & HasIdentifier> E convertEntity(Entity 
entity) {
+  public static <E extends Entity & HasIdentifier> E convertEntity(Entity 
entity) {
     validateEntityHasIdentifier(entity);
 
     return (E) entity;
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java 
b/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
index c002bf64fe..317beff0d1 100644
--- a/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
+++ b/core/src/main/java/org/apache/gravitino/cache/CaffeineEntityCache.java
@@ -23,7 +23,6 @@ import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.RemovalCause;
 import com.github.benmanes.caffeine.cache.stats.CacheStats;
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree;
@@ -32,6 +31,7 @@ import 
com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFa
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.Executors;
@@ -44,14 +44,14 @@ import java.util.stream.Collectors;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.Entity;
-import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.ModelVersionEntity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-/** This class implements the {@link EntityCache} using Caffeine. */
+/** This class implements the {@link org.apache.gravitino.cache.EntityCache} 
using Caffeine */
 public class CaffeineEntityCache extends BaseEntityCache {
   private static final int CACHE_CLEANUP_CORE_THREADS = 1;
   private static final int CACHE_CLEANUP_MAX_THREADS = 1;
@@ -59,7 +59,6 @@ public class CaffeineEntityCache extends BaseEntityCache {
   private static final int CACHE_MONITOR_PERIOD_MINUTES = 5;
   private static final int CACHE_MONITOR_INITIAL_DELAY_MINUTES = 0;
   private static final Logger LOG = 
LoggerFactory.getLogger(CaffeineEntityCache.class.getName());
-  private static volatile CaffeineEntityCache INSTANCE;
   private final ReentrantLock opLock = new ReentrantLock();
 
   /** Cache part */
@@ -70,38 +69,13 @@ public class CaffeineEntityCache extends BaseEntityCache {
 
   private ScheduledExecutorService scheduler;
 
-  @VisibleForTesting
-  static void resetForTest() {
-    INSTANCE = null;
-  }
-
-  /**
-   * Returns the instance of {@link CaffeineEntityCache} based on the cache 
configuration and entity
-   * store.
-   *
-   * @param cacheConfig The cache configuration
-   * @param entityStore The entity store to load entities from the database
-   * @return The instance of {@link CaffeineEntityCache}
-   */
-  public static CaffeineEntityCache getInstance(Config cacheConfig, 
EntityStore entityStore) {
-    if (INSTANCE == null) {
-      synchronized (CaffeineEntityCache.class) {
-        if (INSTANCE == null) {
-          INSTANCE = new CaffeineEntityCache(cacheConfig, entityStore);
-        }
-      }
-    }
-    return INSTANCE;
-  }
-
   /**
    * Constructs a new {@link CaffeineEntityCache}.
    *
    * @param cacheConfig the cache configuration
-   * @param entityStore The entity store to load entities from the database
    */
-  private CaffeineEntityCache(Config cacheConfig, EntityStore entityStore) {
-    super(cacheConfig, entityStore);
+  public CaffeineEntityCache(Config cacheConfig) {
+    super(cacheConfig);
     this.cacheIndex = new ConcurrentRadixTree<>(new 
DefaultCharArrayNodeFactory());
 
     ThreadPoolExecutor cleanupExec = buildCleanupExecutor();
@@ -134,58 +108,6 @@ public class CaffeineEntityCache extends BaseEntityCache {
     }
   }
 
-  /** {@inheritDoc} */
-  @Override
-  public <E extends Entity & HasIdentifier> List<E> getOrLoad(
-      NameIdentifier ident, Entity.EntityType type, 
SupportsRelationOperations.Type relType)
-      throws IOException {
-    checkArguments(ident, type, relType);
-
-    // Executes a thread-safe operation: attempts to load the entity from the 
cache,
-    // falls back to the underlying store if not present, and updates the 
cache.
-    // If an error occurs during loading, the exception will be propagated.
-    return withLockAndThrow(
-        () -> {
-          EntityCacheKey entityCacheKey = EntityCacheKey.of(ident, type, 
relType);
-          List<Entity> entitiesFromCache = 
cacheData.getIfPresent(entityCacheKey);
-
-          if (entitiesFromCache != null) {
-            return convertEntity(entitiesFromCache);
-          }
-
-          List<E> entities = entityStore.listEntitiesByRelation(relType, 
ident, type);
-          syncEntitiesToCache(
-              entityCacheKey, entities.stream().map(e -> (Entity) 
e).collect(Collectors.toList()));
-
-          return entities;
-        });
-  }
-
-  /** {@inheritDoc} */
-  @Override
-  public <E extends Entity & HasIdentifier> E getOrLoad(
-      NameIdentifier ident, Entity.EntityType type) throws IOException {
-    checkArguments(ident, type);
-
-    // Executes a thread-safe operation: attempts to load the entity from the 
cache,
-    // falls back to the underlying store if not present, and updates the 
cache.
-    // If an error occurs during loading, the exception will be propagated.
-    return withLockAndThrow(
-        () -> {
-          EntityCacheKey entityCacheKey = EntityCacheKey.of(ident, type);
-          List<Entity> entitiesFromCache = 
cacheData.getIfPresent(entityCacheKey);
-
-          if (entitiesFromCache != null) {
-            return convertEntity(entitiesFromCache.get(0));
-          }
-
-          E entityFromStore = entityStore.get(ident, type, 
getEntityClass(type));
-          syncEntitiesToCache(entityCacheKey, 
Lists.newArrayList(entityFromStore));
-
-          return entityFromStore;
-        });
-  }
-
   /** {@inheritDoc} */
   @Override
   public <E extends Entity & HasIdentifier> Optional<List<E>> getIfPresent(
@@ -194,12 +116,9 @@ public class CaffeineEntityCache extends BaseEntityCache {
       Entity.EntityType identType) {
     checkArguments(nameIdentifier, identType, relType);
 
-    return withLock(
-        () -> {
-          List<Entity> entitiesFromCache =
-              cacheData.getIfPresent(EntityCacheKey.of(nameIdentifier, 
identType, relType));
-          return 
Optional.ofNullable(entitiesFromCache).map(BaseEntityCache::convertEntity);
-        });
+    List<Entity> entitiesFromCache =
+        cacheData.getIfPresent(EntityCacheKey.of(nameIdentifier, identType, 
relType));
+    return 
Optional.ofNullable(entitiesFromCache).map(BaseEntityCache::convertEntities);
   }
 
   /** {@inheritDoc} */
@@ -208,14 +127,11 @@ public class CaffeineEntityCache extends BaseEntityCache {
       NameIdentifier ident, Entity.EntityType type) {
     checkArguments(ident, type);
 
-    return withLock(
-        () -> {
-          List<Entity> entitiesFromCache = 
cacheData.getIfPresent(EntityCacheKey.of(ident, type));
+    List<Entity> entitiesFromCache = 
cacheData.getIfPresent(EntityCacheKey.of(ident, type));
 
-          return Optional.ofNullable(entitiesFromCache)
-              .filter(l -> !l.isEmpty())
-              .map(entities -> convertEntity(entities.get(0)));
-        });
+    return Optional.ofNullable(entitiesFromCache)
+        .filter(l -> !l.isEmpty())
+        .map(entities -> convertEntity(entities.get(0)));
   }
 
   /** {@inheritDoc} */
@@ -224,15 +140,7 @@ public class CaffeineEntityCache extends BaseEntityCache {
       NameIdentifier ident, Entity.EntityType type, 
SupportsRelationOperations.Type relType) {
     checkArguments(ident, type, relType);
 
-    return withLock(
-        () -> {
-          boolean existed = contains(ident, type, relType);
-          if (existed) {
-            invalidateEntities(ident);
-          }
-
-          return existed;
-        });
+    return withLock(() -> invalidateEntities(ident));
   }
 
   /** {@inheritDoc} */
@@ -240,15 +148,7 @@ public class CaffeineEntityCache extends BaseEntityCache {
   public boolean invalidate(NameIdentifier ident, Entity.EntityType type) {
     checkArguments(ident, type);
 
-    return withLock(
-        () -> {
-          boolean existed = contains(ident, type);
-          if (existed) {
-            invalidateEntities(ident);
-          }
-
-          return existed;
-        });
+    return withLock(() -> invalidateEntities(ident));
   }
 
   /** {@inheritDoc} */
@@ -256,16 +156,14 @@ public class CaffeineEntityCache extends BaseEntityCache {
   public boolean contains(
       NameIdentifier ident, Entity.EntityType type, 
SupportsRelationOperations.Type relType) {
     checkArguments(ident, type, relType);
-
-    return withLock(() -> cacheData.getIfPresent(EntityCacheKey.of(ident, 
type, relType)) != null);
+    return cacheData.getIfPresent(EntityCacheKey.of(ident, type, relType)) != 
null;
   }
 
   /** {@inheritDoc} */
   @Override
   public boolean contains(NameIdentifier ident, Entity.EntityType type) {
     checkArguments(ident, type);
-
-    return withLock(() -> cacheData.getIfPresent(EntityCacheKey.of(ident, 
type)) != null);
+    return cacheData.getIfPresent(EntityCacheKey.of(ident, type)) != null;
   }
 
   /** {@inheritDoc} */
@@ -293,6 +191,9 @@ public class CaffeineEntityCache extends BaseEntityCache {
       List<E> entities) {
     checkArguments(ident, type, relType);
     Preconditions.checkArgument(entities != null, "Entities cannot be null");
+    if (entities.isEmpty()) {
+      return;
+    }
 
     syncEntitiesToCache(
         EntityCacheKey.of(ident, type, relType),
@@ -306,12 +207,26 @@ public class CaffeineEntityCache extends BaseEntityCache {
 
     withLock(
         () -> {
+          invalidateOnKeyChange(entity);
           NameIdentifier identifier = getIdentFromEntity(entity);
           EntityCacheKey entityCacheKey = EntityCacheKey.of(identifier, 
entity.type());
+
           syncEntitiesToCache(entityCacheKey, Lists.newArrayList(entity));
         });
   }
 
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Entity & HasIdentifier> void invalidateOnKeyChange(E 
entity) {
+    // Invalidate the cache if inserting the entity may affect related cache 
keys.
+    // For example, inserting a model version changes the latest version of 
the model,
+    // so the corresponding model cache entry should be invalidated.
+    if (Objects.requireNonNull(entity.type()) == 
Entity.EntityType.MODEL_VERSION) {
+      NameIdentifier modelIdent = ((ModelVersionEntity) 
entity).modelIdentifier();
+      invalidate(modelIdent, Entity.EntityType.MODEL);
+    }
+  }
+
   /** {@inheritDoc} */
   @Override
   public <E extends Exception> void withCacheLock(ThrowingRunnable<E> action) 
throws E {
@@ -402,12 +317,14 @@ public class CaffeineEntityCache extends BaseEntityCache {
    *
    * @param identifier The identifier of the entity to invalidate
    */
-  private void invalidateEntities(NameIdentifier identifier) {
-    Iterable<EntityCacheKey> entityKeysToRemove =
-        cacheIndex.getValuesForKeysStartingWith(identifier.toString());
+  private boolean invalidateEntities(NameIdentifier identifier) {
+    List<EntityCacheKey> entityKeysToRemove =
+        
Lists.newArrayList(cacheIndex.getValuesForKeysStartingWith(identifier.toString()));
 
     cacheData.invalidateAll(entityKeysToRemove);
     entityKeysToRemove.forEach(key -> cacheIndex.remove(key.toString()));
+
+    return !entityKeysToRemove.isEmpty();
   }
 
   /**
@@ -433,8 +350,8 @@ public class CaffeineEntityCache extends BaseEntityCache {
    * Runs the given action with the lock and returns the result.
    *
    * @param action The action to run with the lock
-   * @return The result of the action
    * @param <T> The type of the result
+   * @return The result of the action
    */
   private <T> T withLock(Supplier<T> action) {
     try {
diff --git a/core/src/main/java/org/apache/gravitino/cache/NoOpsCache.java 
b/core/src/main/java/org/apache/gravitino/cache/NoOpsCache.java
new file mode 100644
index 0000000000..56cdb93511
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/cache/NoOpsCache.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.cache;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.locks.ReentrantLock;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.SupportsRelationOperations;
+
+/** A cache implementation that does not cache anything. */
+public class NoOpsCache extends BaseEntityCache {
+  private final ReentrantLock opLock = new ReentrantLock();
+  /**
+   * Constructs a new {@link BaseEntityCache} instance.
+   *
+   * @param config The cache configuration
+   */
+  public NoOpsCache(Config config) {
+    super(config);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  protected void invalidateExpiredItem(EntityCacheKey key) {
+    // do nothing
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void clear() {
+    // do nothing
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public long size() {
+    return 0;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Exception> void withCacheLock(ThrowingRunnable<E> action) 
throws E {
+    try {
+      opLock.lockInterruptibly();
+      try {
+        action.run();
+      } finally {
+        opLock.unlock();
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new IllegalStateException("Thread was interrupted while waiting 
for lock", e);
+    }
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <T, E extends Exception> T withCacheLock(ThrowingSupplier<T, E> 
action) throws E {
+    try {
+      opLock.lockInterruptibly();
+      try {
+        return action.get();
+      } finally {
+        opLock.unlock();
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new IllegalStateException("Thread was interrupted while waiting 
for lock", e);
+    }
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Entity & HasIdentifier> Optional<E> getIfPresent(
+      NameIdentifier ident, Entity.EntityType type) {
+    return Optional.empty();
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean invalidate(NameIdentifier ident, Entity.EntityType type) {
+    return false;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean contains(NameIdentifier ident, Entity.EntityType type) {
+    return false;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Entity & HasIdentifier> void put(E entity) {
+    // do nothing
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Entity & HasIdentifier> void invalidateOnKeyChange(E 
entity) {
+    // do nothing
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Entity & HasIdentifier> Optional<List<E>> getIfPresent(
+      SupportsRelationOperations.Type relType,
+      NameIdentifier nameIdentifier,
+      Entity.EntityType identType) {
+    return Optional.empty();
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean invalidate(
+      NameIdentifier ident, Entity.EntityType type, 
SupportsRelationOperations.Type relType) {
+    return false;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean contains(
+      NameIdentifier ident, Entity.EntityType type, 
SupportsRelationOperations.Type relType) {
+    return false;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public <E extends Entity & HasIdentifier> void put(
+      NameIdentifier ident,
+      Entity.EntityType type,
+      SupportsRelationOperations.Type relType,
+      List<E> entities) {
+    // do nothing
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/SupportsEntityStoreCache.java 
b/core/src/main/java/org/apache/gravitino/cache/SupportsEntityStoreCache.java
index 177c0ecc77..ed42084fb0 100644
--- 
a/core/src/main/java/org/apache/gravitino/cache/SupportsEntityStoreCache.java
+++ 
b/core/src/main/java/org/apache/gravitino/cache/SupportsEntityStoreCache.java
@@ -19,7 +19,6 @@
 
 package org.apache.gravitino.cache;
 
-import java.io.IOException;
 import java.util.Optional;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.HasIdentifier;
@@ -30,19 +29,6 @@ import org.apache.gravitino.NameIdentifier;
  * loading, storing, and invalidating individual metadata entities.
  */
 public interface SupportsEntityStoreCache {
-  /**
-   * Retrieves an entity by its name identifier and type. If the entity is not 
present in the cache,
-   * it will be loaded from the backing EntityStore.
-   *
-   * @param ident The name identifier of the entity
-   * @param type The type of the entity
-   * @return The cached or newly loaded Entity instance
-   * @param <E> The class of the entity
-   * @throws IOException if the operation fails
-   */
-  <E extends Entity & HasIdentifier> E getOrLoad(NameIdentifier ident, 
Entity.EntityType type)
-      throws IOException;
-
   /**
    * Retrieves an entity from the cache if it exists. Will not attempt to load 
from the store if
    * missing.
@@ -80,4 +66,15 @@ public interface SupportsEntityStoreCache {
    * @param <E> The class of the entity
    */
   <E extends Entity & HasIdentifier> void put(E entity);
+
+  /**
+   * Invalidates related cache entries when inserting the given entity, if 
necessary.
+   *
+   * <p>For example, inserting a {@code ModelVersion} may require invalidating 
the corresponding
+   * {@code Model} entry in the cache to maintain consistency.
+   *
+   * @param entity The entity being inserted
+   * @param <E> The type of the entity
+   */
+  <E extends Entity & HasIdentifier> void invalidateOnKeyChange(E entity);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/SupportsRelationEntityCache.java
 
b/core/src/main/java/org/apache/gravitino/cache/SupportsRelationEntityCache.java
index 42db55ca1f..98619db006 100644
--- 
a/core/src/main/java/org/apache/gravitino/cache/SupportsRelationEntityCache.java
+++ 
b/core/src/main/java/org/apache/gravitino/cache/SupportsRelationEntityCache.java
@@ -19,11 +19,9 @@
 
 package org.apache.gravitino.cache;
 
-import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
 import org.apache.gravitino.Entity;
-import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.SupportsRelationOperations;
@@ -33,23 +31,6 @@ import org.apache.gravitino.SupportsRelationOperations;
  * relationships.
  */
 public interface SupportsRelationEntityCache {
-  /**
-   * Retrieves a list of entities related to the specified entity under the 
given relation type. If
-   * the related entities are not present in the cache, they will be loaded 
from the underlying
-   * {@link EntityStore}.
-   *
-   * @param ident The name identifier of the entity to find related entities 
for
-   * @param type The type of the entity to find related entities for
-   * @param relType The relation type to find related entities for
-   * @return A list of related entities, or an empty list if none are found
-   * @param <E> The class of the related entities
-   * @throws IOException if an I/O error occurs while loading related entities 
from the underlying
-   *     {@link EntityStore}
-   */
-  <E extends Entity & HasIdentifier> List<E> getOrLoad(
-      NameIdentifier ident, Entity.EntityType type, 
SupportsRelationOperations.Type relType)
-      throws IOException;
-
   /**
    * Retrieves a list of related entities from the cache, if present.
    *
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index a6d650c547..2bc2075f70 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -23,6 +23,7 @@ import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Function;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
@@ -34,6 +35,9 @@ import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.cache.CaffeineEntityCache;
+import org.apache.gravitino.cache.EntityCache;
+import org.apache.gravitino.cache.NoOpsCache;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.meta.TagEntity;
 import org.apache.gravitino.tag.SupportsTagOperations;
@@ -54,12 +58,18 @@ public class RelationalEntityStore
           Configs.DEFAULT_ENTITY_RELATIONAL_STORE, 
JDBCBackend.class.getCanonicalName());
   private RelationalBackend backend;
   private RelationalGarbageCollector garbageCollector;
+  private EntityCache cache;
 
   @Override
   public void initialize(Config config) throws RuntimeException {
     this.backend = createRelationalEntityBackend(config);
     this.garbageCollector = new RelationalGarbageCollector(backend, config);
     this.garbageCollector.start();
+    // TODO USE SPI to load the cache
+    this.cache =
+        config.get(Configs.CACHE_ENABLED)
+            ? new CaffeineEntityCache(config)
+            : new NoOpsCache(config);
   }
 
   private static RelationalBackend createRelationalEntityBackend(Config 
config) {
@@ -95,19 +105,22 @@ public class RelationalEntityStore
 
   @Override
   public boolean exists(NameIdentifier ident, Entity.EntityType entityType) 
throws IOException {
-    return backend.exists(ident, entityType);
+    boolean existsInCache = cache.contains(ident, entityType);
+    return existsInCache || backend.exists(ident, entityType);
   }
 
   @Override
   public <E extends Entity & HasIdentifier> void put(E e, boolean overwritten)
       throws IOException, EntityAlreadyExistsException {
     backend.insert(e, overwritten);
+    cache.put(e);
   }
 
   @Override
   public <E extends Entity & HasIdentifier> E update(
       NameIdentifier ident, Class<E> type, Entity.EntityType entityType, 
Function<E, E> updater)
       throws IOException, NoSuchEntityException, EntityAlreadyExistsException {
+    cache.invalidate(ident, entityType);
     return backend.update(ident, entityType, updater);
   }
 
@@ -115,15 +128,26 @@ public class RelationalEntityStore
   public <E extends Entity & HasIdentifier> E get(
       NameIdentifier ident, Entity.EntityType entityType, Class<E> e)
       throws NoSuchEntityException, IOException {
-    return backend.get(ident, entityType);
+    return cache.withCacheLock(
+        () -> {
+          Optional<E> entityFromCache = cache.getIfPresent(ident, entityType);
+          if (entityFromCache.isPresent()) {
+            return entityFromCache.get();
+          }
+
+          E entity = backend.get(ident, entityType);
+          cache.put(entity);
+          return entity;
+        });
   }
 
   @Override
   public boolean delete(NameIdentifier ident, Entity.EntityType entityType, 
boolean cascade)
       throws IOException {
     try {
+      cache.invalidate(ident, entityType);
       return backend.delete(ident, entityType, cascade);
-    } catch (NoSuchEntityException nse) {
+    } catch (NoSuchEntityException e) {
       return false;
     }
   }
@@ -135,6 +159,7 @@ public class RelationalEntityStore
 
   @Override
   public void close() throws IOException {
+    cache.clear();
     garbageCollector.close();
     backend.close();
   }
@@ -184,7 +209,20 @@ public class RelationalEntityStore
   public <E extends Entity & HasIdentifier> List<E> listEntitiesByRelation(
       Type relType, NameIdentifier nameIdentifier, Entity.EntityType 
identType, boolean allFields)
       throws IOException {
-    return backend.listEntitiesByRelation(relType, nameIdentifier, identType, 
allFields);
+    return cache.withCacheLock(
+        () -> {
+          Optional<List<E>> entities = cache.getIfPresent(relType, 
nameIdentifier, identType);
+          if (entities.isPresent()) {
+            return entities.get();
+          }
+
+          List<E> backendEntities =
+              backend.listEntitiesByRelation(relType, nameIdentifier, 
identType, allFields);
+
+          cache.put(nameIdentifier, identType, relType, backendEntities);
+
+          return backendEntities;
+        });
   }
 
   @Override
@@ -196,6 +234,7 @@ public class RelationalEntityStore
       Entity.EntityType dstType,
       boolean override)
       throws IOException {
-    backend.insertRelation(relType, srcIdentifier, srcType, dstIdentifier, 
dstType, true);
+    cache.invalidate(srcIdentifier, srcType, relType);
+    backend.insertRelation(relType, srcIdentifier, srcType, dstIdentifier, 
dstType, override);
   }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
index d62a641438..e93deab223 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -52,6 +52,7 @@ import java.util.UUID;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.GravitinoEnv;
@@ -130,6 +131,12 @@ public class TestAccessControlManager {
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
     Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
     
Mockito.when(config.get(CATALOG_CACHE_EVICTION_INTERVAL_MS)).thenReturn(1000L);
+    // Fix cache for testing.
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
 
     Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
     Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
diff --git 
a/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java 
b/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java
index 98b83a77b3..95dd6fcd21 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java
@@ -44,6 +44,7 @@ import java.util.UUID;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.GravitinoEnv;
@@ -103,6 +104,12 @@ public class TestOwnerManager {
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
     Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
     
Mockito.when(config.get(CATALOG_CACHE_EVICTION_INTERVAL_MS)).thenReturn(1000L);
+    // Fix the cache config for testing
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
 
     Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
     Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
diff --git 
a/core/src/test/java/org/apache/gravitino/cache/TestCaffeineEntityCache.java 
b/core/src/test/java/org/apache/gravitino/cache/TestCaffeineEntityCache.java
new file mode 100644
index 0000000000..57a60f1f4b
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/cache/TestCaffeineEntityCache.java
@@ -0,0 +1,396 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.cache;
+
+import static org.mockito.Mockito.spy;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.meta.BaseMetalake;
+import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.meta.FilesetEntity;
+import org.apache.gravitino.meta.GroupEntity;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.ModelVersionEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.meta.SchemaEntity;
+import org.apache.gravitino.meta.TableEntity;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.meta.TopicEntity;
+import org.apache.gravitino.meta.UserEntity;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.TestUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestCaffeineEntityCache {
+  private CaffeineEntityCache real;
+  private CaffeineEntityCache cache;
+
+  private NameIdentifier ident1;
+  private NameIdentifier ident2;
+  private NameIdentifier ident3;
+  private NameIdentifier ident4;
+  private NameIdentifier ident5;
+  private NameIdentifier ident6;
+  private NameIdentifier ident7;
+  private NameIdentifier ident8;
+  private NameIdentifier ident9;
+  private NameIdentifier ident10;
+  private NameIdentifier ident11;
+  private NameIdentifier ident12;
+  private NameIdentifier ident13;
+
+  // Test Entities
+  private SchemaEntity entity1;
+  private SchemaEntity entity2;
+  private TableEntity entity3;
+  private TableEntity entity4;
+  private TableEntity entity5;
+  private CatalogEntity entity6;
+  private BaseMetalake entity7;
+  private UserEntity entity8;
+  private UserEntity entity9;
+  private GroupEntity entity10;
+  private GroupEntity entity11;
+  private RoleEntity entity12;
+  private RoleEntity entity13;
+
+  @BeforeAll
+  void init() {
+    initTestNameIdentifier();
+    initTestEntities();
+  }
+
+  @Test
+  void testPutAllTypeInCache() {
+
+    initCache();
+
+    BaseMetalake testMetalake = TestUtil.getTestMetalake();
+    CatalogEntity testCatalogEntity = TestUtil.getTestCatalogEntity();
+    SchemaEntity testSchemaEntity = TestUtil.getTestSchemaEntity();
+    TableEntity testTableEntity = TestUtil.getTestTableEntity();
+    ModelEntity testModelEntity = TestUtil.getTestModelEntity();
+    FilesetEntity testFileSetEntity = TestUtil.getTestFileSetEntity();
+    TopicEntity testTopicEntity = TestUtil.getTestTopicEntity();
+    TagEntity testTagEntity = TestUtil.getTestTagEntity();
+    UserEntity testUserEntity = TestUtil.getTestUserEntity();
+    GroupEntity testGroupEntity = TestUtil.getTestGroupEntity();
+    RoleEntity testRoleEntity = TestUtil.getTestRoleEntity();
+    ModelVersionEntity testModelVersionEntity = 
TestUtil.getTestModelVersionEntity();
+
+    cache.put(testMetalake);
+    cache.put(testCatalogEntity);
+    cache.put(testSchemaEntity);
+    cache.put(testTableEntity);
+    cache.put(testModelEntity);
+    cache.put(testFileSetEntity);
+    cache.put(testTopicEntity);
+    cache.put(testTagEntity);
+    cache.put(testUserEntity);
+    cache.put(testGroupEntity);
+    cache.put(testRoleEntity);
+    cache.put(testModelVersionEntity);
+
+    cache.put(
+        testRoleEntity.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_GROUP_REL,
+        ImmutableList.of(testGroupEntity));
+    cache.put(
+        testRoleEntity.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_USER_REL,
+        ImmutableList.of(testUserEntity));
+
+    Assertions.assertEquals(14, cache.size());
+    Assertions.assertTrue(
+        cache.getIfPresent(testMetalake.nameIdentifier(), 
Entity.EntityType.METALAKE).isPresent());
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(testCatalogEntity.nameIdentifier(), 
Entity.EntityType.CATALOG)
+            .isPresent());
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(testSchemaEntity.nameIdentifier(), 
Entity.EntityType.SCHEMA)
+            .isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testTableEntity.nameIdentifier(), 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testModelEntity.nameIdentifier(), 
Entity.EntityType.MODEL).isPresent());
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(testFileSetEntity.nameIdentifier(), 
Entity.EntityType.FILESET)
+            .isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testTopicEntity.nameIdentifier(), 
Entity.EntityType.TOPIC).isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testTagEntity.nameIdentifier(), 
Entity.EntityType.TAG).isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testUserEntity.nameIdentifier(), 
Entity.EntityType.USER).isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testGroupEntity.nameIdentifier(), 
Entity.EntityType.GROUP).isPresent());
+    Assertions.assertTrue(
+        cache.getIfPresent(testRoleEntity.nameIdentifier(), 
Entity.EntityType.ROLE).isPresent());
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(testModelVersionEntity.nameIdentifier(), 
Entity.EntityType.MODEL_VERSION)
+            .isPresent());
+
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(
+                SupportsRelationOperations.Type.ROLE_GROUP_REL,
+                testRoleEntity.nameIdentifier(),
+                Entity.EntityType.ROLE)
+            .isPresent());
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(
+                SupportsRelationOperations.Type.ROLE_USER_REL,
+                testRoleEntity.nameIdentifier(),
+                Entity.EntityType.ROLE)
+            .isPresent());
+  }
+
+  @Test
+  void testGetIfPresent() {
+
+    initCache();
+
+    cache.put(entity1);
+    cache.put(entity2);
+    cache.put(entity3);
+    cache.put(
+        entity12.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_USER_REL,
+        ImmutableList.of(entity8, entity9));
+
+    Assertions.assertTrue(cache.getIfPresent(ident1, 
Entity.EntityType.SCHEMA).isPresent());
+    Assertions.assertTrue(cache.getIfPresent(ident2, 
Entity.EntityType.SCHEMA).isPresent());
+    Assertions.assertTrue(cache.getIfPresent(ident3, 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertTrue(
+        cache
+            .getIfPresent(
+                SupportsRelationOperations.Type.ROLE_USER_REL, ident12, 
Entity.EntityType.ROLE)
+            .isPresent());
+    Assertions.assertEquals(cache.size(), 4);
+
+    Assertions.assertFalse(cache.getIfPresent(ident4, 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident5, 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident6, 
Entity.EntityType.CATALOG).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident7, 
Entity.EntityType.METALAKE).isPresent());
+  }
+
+  @Test
+  void testContains() {
+
+    initCache();
+
+    cache.put(entity1);
+    cache.put(entity2);
+    cache.put(entity3);
+
+    Assertions.assertTrue(cache.contains(ident1, Entity.EntityType.SCHEMA));
+    Assertions.assertTrue(cache.contains(ident2, Entity.EntityType.SCHEMA));
+    Assertions.assertTrue(cache.contains(ident3, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident4, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident5, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident6, Entity.EntityType.CATALOG));
+    Assertions.assertFalse(cache.contains(ident7, Entity.EntityType.METALAKE));
+  }
+
+  @Test
+  void testSize() {
+
+    initCache();
+
+    cache.put(entity1);
+    cache.put(entity2);
+    cache.put(entity3);
+
+    Assertions.assertEquals(3, cache.size());
+  }
+
+  @Test
+  void testClear() {
+
+    initCache();
+
+    cache.put(entity1);
+    cache.put(entity2);
+    cache.put(entity3);
+    cache.put(entity4);
+    cache.put(entity5);
+    cache.put(entity6);
+    cache.put(entity7);
+    cache.put(
+        entity12.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_USER_REL,
+        ImmutableList.of(entity8, entity9));
+    cache.put(
+        entity13.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_GROUP_REL,
+        ImmutableList.of(entity10, entity11));
+
+    Assertions.assertEquals(9, cache.size());
+
+    cache.clear();
+
+    Assertions.assertEquals(0, cache.size());
+    Assertions.assertFalse(cache.getIfPresent(ident1, 
Entity.EntityType.SCHEMA).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident2, 
Entity.EntityType.SCHEMA).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident3, 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident4, 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident5, 
Entity.EntityType.TABLE).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident6, 
Entity.EntityType.CATALOG).isPresent());
+    Assertions.assertFalse(cache.getIfPresent(ident7, 
Entity.EntityType.METALAKE).isPresent());
+    Assertions.assertFalse(
+        cache
+            .getIfPresent(
+                SupportsRelationOperations.Type.ROLE_USER_REL, ident12, 
Entity.EntityType.ROLE)
+            .isPresent());
+    Assertions.assertFalse(
+        cache
+            .getIfPresent(
+                SupportsRelationOperations.Type.ROLE_GROUP_REL, ident13, 
Entity.EntityType.ROLE)
+            .isPresent());
+  }
+
+  @Test
+  void testInvalidateMetalake() {
+
+    initCache();
+
+    cache.put(entity1);
+    cache.put(entity2);
+    cache.put(entity3);
+    cache.put(entity4);
+    cache.put(entity5);
+    cache.put(entity6);
+    cache.put(entity7);
+    cache.put(entity8);
+    cache.put(entity9);
+    cache.put(entity10);
+    cache.put(entity11);
+
+    cache.put(
+        entity12.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_USER_REL,
+        ImmutableList.of(entity8, entity9));
+    cache.put(
+        entity13.nameIdentifier(),
+        Entity.EntityType.ROLE,
+        SupportsRelationOperations.Type.ROLE_GROUP_REL,
+        ImmutableList.of(entity10, entity11));
+
+    Assertions.assertEquals(13, cache.size());
+
+    cache.invalidate(ident7, Entity.EntityType.METALAKE);
+
+    Assertions.assertEquals(4, cache.size());
+    Assertions.assertTrue(cache.contains(ident10, Entity.EntityType.GROUP));
+    Assertions.assertTrue(cache.contains(ident11, Entity.EntityType.GROUP));
+    Assertions.assertTrue(
+        cache.contains(
+            ident13, Entity.EntityType.ROLE, 
SupportsRelationOperations.Type.ROLE_GROUP_REL));
+    Assertions.assertTrue(cache.getIfPresent(ident2, 
Entity.EntityType.SCHEMA).isPresent());
+
+    Assertions.assertFalse(cache.contains(ident1, Entity.EntityType.SCHEMA));
+    Assertions.assertFalse(cache.contains(ident3, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident4, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident5, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident6, Entity.EntityType.TABLE));
+    Assertions.assertFalse(cache.contains(ident7, Entity.EntityType.TABLE));
+  }
+
+  private void initCache() {
+    initCache(new Config() {});
+  }
+
+  // TODO Add other tests for cache
+
+  private void initCache(Config config) {
+    real = new CaffeineEntityCache(config);
+    cache = spy(real);
+  }
+
+  private void initTestNameIdentifier() {
+    ident1 = NameIdentifier.of("metalake1", "catalog1", "schema1");
+    ident2 = NameIdentifier.of("metalake2", "catalog2", "schema2");
+    ident3 = NameIdentifier.of("metalake1", "catalog1", "schema1", "table1");
+    ident4 = NameIdentifier.of("metalake1", "catalog2", "schema1", "table2");
+    ident5 = NameIdentifier.of("metalake1", "catalog1", "schema2", "table3");
+    ident6 = NameIdentifier.of("metalake1", "catalog1");
+    ident7 = NameIdentifier.of("metalake1");
+
+    ident8 = NameIdentifierUtil.ofUser("metalake1", "user1");
+    ident9 = NameIdentifierUtil.ofUser("metalake1", "user2");
+
+    ident10 = NameIdentifierUtil.ofGroup("metalake2", "group1");
+    ident11 = NameIdentifierUtil.ofGroup("metalake2", "group2");
+
+    ident12 = NameIdentifierUtil.ofRole("metalake1", "role1");
+    ident13 = NameIdentifierUtil.ofRole("metalake2", "role2");
+
+    // TODO remove next PR
+    System.out.println(ident8 + " " + ident9);
+  }
+
+  private void initTestEntities() {
+    entity1 =
+        TestUtil.getTestSchemaEntity(
+            1L, "schema1", Namespace.of("metalake1", "catalog1"), 
"test_schema1");
+    entity2 =
+        TestUtil.getTestSchemaEntity(
+            2L, "schema2", Namespace.of("metalake2", "catalog2"), 
"test_schema2");
+    entity3 =
+        TestUtil.getTestTableEntity(3L, "table1", Namespace.of("metalake1", 
"catalog1", "schema1"));
+    entity4 =
+        TestUtil.getTestTableEntity(4L, "table2", Namespace.of("metalake1", 
"catalog2", "schema1"));
+    entity5 =
+        TestUtil.getTestTableEntity(5L, "table3", Namespace.of("metalake1", 
"catalog1", "schema2"));
+    entity6 =
+        TestUtil.getTestCatalogEntity(
+            6L, "catalog1", Namespace.of("metalake1"), "hive", "test_catalog");
+    entity7 = TestUtil.getTestMetalake(7L, "metalake1", "test_metalake1");
+
+    entity8 = TestUtil.getTestUserEntity(8L, "user1", "metalake1", 
ImmutableList.of(12L));
+    entity9 = TestUtil.getTestUserEntity(9L, "user2", "metalake1", 
ImmutableList.of(12L));
+
+    entity10 = TestUtil.getTestGroupEntity(10L, "group1", "metalake2", 
ImmutableList.of("role2"));
+    entity11 = TestUtil.getTestGroupEntity(11L, "group2", "metalake2", 
ImmutableList.of("role2"));
+
+    entity12 = TestUtil.getTestRoleEntity(12L, "role1", "metalake1");
+    entity13 = TestUtil.getTestRoleEntity(13L, "role2", "metalake2");
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java 
b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java
index 00a7358222..686c61a323 100644
--- a/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java
+++ b/core/src/test/java/org/apache/gravitino/storage/TestEntityStorage.java
@@ -55,6 +55,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.EntityAlreadyExistsException;
@@ -139,6 +140,13 @@ public class TestEntityStorage {
     
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
     Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
+    // Fix cache config for test
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
+
     BaseIT baseIT = new BaseIT();
 
     try {
diff --git a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java 
b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
index 5cbffb3f97..3506e1755e 100644
--- a/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
+++ b/core/src/test/java/org/apache/gravitino/tag/TestTagManager.java
@@ -18,7 +18,6 @@
  */
 package org.apache.gravitino.tag;
 
-import static org.apache.gravitino.Configs.CATALOG_CACHE_EVICTION_INTERVAL_MS;
 import static org.apache.gravitino.Configs.DEFAULT_ENTITY_RELATIONAL_STORE;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
 import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS;
@@ -52,6 +51,7 @@ import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.EntityStoreFactory;
@@ -132,7 +132,12 @@ public class TestTagManager {
     
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
     Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
     Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
-    
Mockito.when(config.get(CATALOG_CACHE_EVICTION_INTERVAL_MS)).thenReturn(1000L);
+    // Fix cache config for test
+    Mockito.when(config.get(Configs.CACHE_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_MAX_ENTRIES)).thenReturn(10_000);
+    
Mockito.when(config.get(Configs.CACHE_EXPIRATION_TIME)).thenReturn(3_600_000L);
+    Mockito.when(config.get(Configs.CACHE_WEIGHER_ENABLED)).thenReturn(true);
+    Mockito.when(config.get(Configs.CACHE_STATS_ENABLED)).thenReturn(false);
 
     Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
     Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
diff --git a/core/src/test/java/org/apache/gravitino/utils/TestUtil.java 
b/core/src/test/java/org/apache/gravitino/utils/TestUtil.java
new file mode 100644
index 0000000000..4c6beb2a99
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/utils/TestUtil.java
@@ -0,0 +1,502 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.utils;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.BaseMetalake;
+import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.meta.ColumnEntity;
+import org.apache.gravitino.meta.FilesetEntity;
+import org.apache.gravitino.meta.GroupEntity;
+import org.apache.gravitino.meta.ModelEntity;
+import org.apache.gravitino.meta.ModelVersionEntity;
+import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.meta.SchemaEntity;
+import org.apache.gravitino.meta.SchemaVersion;
+import org.apache.gravitino.meta.TableEntity;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.meta.TopicEntity;
+import org.apache.gravitino.meta.UserEntity;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.storage.RandomIdGenerator;
+
+/**
+ * Utility class providing factory methods for creating mock and test 
instances of various metadata
+ * entities used in unit tests.
+ *
+ * <p>This class includes shortcut methods for constructing entities such as 
{@link BaseMetalake},
+ * {@link CatalogEntity}, {@link SchemaEntity}, {@link TableEntity}, {@link 
ModelEntity}, {@link
+ * FilesetEntity}, {@link TopicEntity}, {@link TagEntity}, {@link UserEntity}, 
{@link GroupEntity},
+ * {@link RoleEntity}, and {@link ModelVersionEntity}. It also includes 
methods to create mock
+ * {@link ColumnEntity} and {@link SecurableObject} instances for use in 
mocking behavior-dependent
+ * components.
+ *
+ * <p>These utilities are intended for testing purposes only and rely on 
in-memory mock objects or
+ * randomly generated IDs to simulate real entities.
+ *
+ * <p>Note: This class is not thread-safe.
+ */
+public class TestUtil {
+  // Random ID generator used to generate IDs for test entities.
+  private static RandomIdGenerator generator = new RandomIdGenerator();
+
+  /**
+   * Shorthand for creating a test metalake entity with default values.
+   *
+   * @return The test {@link BaseMetalake} entity.
+   */
+  public static BaseMetalake getTestMetalake() {
+    return getTestMetalake(generator.nextId(), "test_metalake", "metalake 
entity test");
+  }
+
+  /**
+   * Get the test metalake entity with the given ID, name, and comment.
+   *
+   * @param id The ID of the metalake entity.
+   * @param name The name of the metalake entity.
+   * @param comment The comment of the metalake entity.
+   * @return The test {@link BaseMetalake} entity.
+   */
+  public static BaseMetalake getTestMetalake(long id, String name, String 
comment) {
+    return BaseMetalake.builder()
+        .withId(id)
+        .withName(name)
+        .withVersion(SchemaVersion.V_0_1)
+        .withAuditInfo(getTestAuditInfo())
+        .withComment(comment)
+        .withProperties(ImmutableMap.of())
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test catalog entity with default values.
+   *
+   * @return The test {@link CatalogEntity} entity.
+   */
+  public static CatalogEntity getTestCatalogEntity() {
+    return getTestCatalogEntity(
+        generator.nextId(), "test_catalog", Namespace.of("m1"), "hive", 
"catalog entity test");
+  }
+
+  /**
+   * Returns a test catalog entity with the given ID, name, namespace, 
provider, and comment.
+   *
+   * @param id The ID of the catalog entity.
+   * @param name The name of the catalog entity.
+   * @param namespace The namespace of the catalog entity.
+   * @param provider The provider of the catalog entity.
+   * @param comment The comment of the catalog entity.
+   * @return The test {@link CatalogEntity} entity.
+   */
+  public static CatalogEntity getTestCatalogEntity(
+      long id, String name, Namespace namespace, String provider, String 
comment) {
+    return CatalogEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(namespace)
+        .withType(Catalog.Type.RELATIONAL)
+        .withProvider(provider)
+        .withAuditInfo(getTestAuditInfo())
+        .withComment(comment)
+        .withProperties(ImmutableMap.of())
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test schema entity with default values.
+   *
+   * @return The test {@link SchemaEntity} entity.
+   */
+  public static SchemaEntity getTestSchemaEntity() {
+    return getTestSchemaEntity(
+        generator.nextId(), "test_schema", Namespace.of("m1", "c1"), "schema 
entity test");
+  }
+
+  /**
+   * Returns a test schema entity with the given ID, name, namespace, and 
comment.
+   *
+   * @param id The ID of the schema entity.
+   * @param name The name of the schema entity.
+   * @param namespace The namespace of the schema entity.
+   * @param comment The comment of the schema entity.
+   * @return The test {@link SchemaEntity} entity.
+   */
+  public static SchemaEntity getTestSchemaEntity(
+      long id, String name, Namespace namespace, String comment) {
+    return SchemaEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(namespace)
+        .withAuditInfo(getTestAuditInfo())
+        .withComment(comment)
+        .withProperties(ImmutableMap.of())
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test model entity with default values.
+   *
+   * @return The test {@link TableEntity} entity.
+   */
+  public static ModelEntity getTestModelEntity() {
+    return getTestModelEntity(generator.nextId(), "test_model", 
Namespace.of("m1", "c1", "s1"));
+  }
+
+  /**
+   * Returns a test model entity with the given ID, name, and namespace.
+   *
+   * @param id The ID of the model entity.
+   * @param name The name of the model entity.
+   * @param namespace The namespace of the model entity.
+   * @return The test {@link ModelEntity} entity.
+   */
+  public static ModelEntity getTestModelEntity(long id, String name, Namespace 
namespace) {
+    return ModelEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(namespace)
+        .withLatestVersion(1)
+        .withAuditInfo(getTestAuditInfo())
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test table entity with default values.
+   *
+   * @return The test {@link TableEntity} entity.
+   */
+  public static TableEntity getTestTableEntity() {
+    return getTestTableEntity(generator.nextId(), "test_table", 
Namespace.of("m1", "c2", "s2"));
+  }
+
+  /**
+   * Returns a test table entity with the given ID, name, and namespace.
+   *
+   * @param id The ID of the table entity.
+   * @param name The name of the table entity.
+   * @param namespace The namespace of the table entity.
+   * @return The test {@link TableEntity} entity.
+   */
+  public static TableEntity getTestTableEntity(long id, String name, Namespace 
namespace) {
+    return TableEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withAuditInfo(getTestAuditInfo())
+        .withNamespace(namespace)
+        .withColumns(ImmutableList.of(getMockColumnEntity()))
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test fileset entity with default values.
+   *
+   * @return The test {@link FilesetEntity} entity.
+   */
+  public static FilesetEntity getTestFileSetEntity() {
+    return getTestFileSetEntity(
+        generator.nextId(),
+        "fileset_test",
+        "file:///tmp/fileset_test",
+        Namespace.of("m1", "c3", "s3"),
+        "fileset entity test",
+        Fileset.Type.EXTERNAL);
+  }
+
+  /**
+   * Returns a test fileset entity with the given ID, name, storage location, 
namespace, and
+   * comment.
+   *
+   * @param id The ID of the fileset entity.
+   * @param name The name of the fileset entity.
+   * @param storageLocation The storage location of the fileset entity.
+   * @param namespace The namespace of the fileset entity.
+   * @param comment The comment of the fileset entity.
+   * @param type The type of the fileset entity.
+   * @return The test {@link FilesetEntity} entity.
+   */
+  public static FilesetEntity getTestFileSetEntity(
+      long id,
+      String name,
+      String storageLocation,
+      Namespace namespace,
+      String comment,
+      Fileset.Type type) {
+    return FilesetEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(namespace)
+        .withStorageLocations(ImmutableMap.of("location1", storageLocation))
+        .withFilesetType(type)
+        .withAuditInfo(getTestAuditInfo())
+        .withComment(comment)
+        .withProperties(ImmutableMap.of())
+        .build();
+  }
+
+  /**
+   * Returns a test topic entity with default values.
+   *
+   * @return The test {@link TopicEntity} entity.
+   */
+  public static TopicEntity getTestTopicEntity() {
+    return getTestTopicEntity(
+        generator.nextId(), "topic_test", Namespace.of("m1", "c4", "s4"), 
"topic entity test");
+  }
+
+  /**
+   * Returns a test topic entity with the given ID, name, namespace, and 
comment.
+   *
+   * @param id The ID of the topic entity.
+   * @param name The name of the topic entity.
+   * @param namespace The namespace of the topic entity.
+   * @param comment The comment of the topic entity.
+   * @return The test {@link TopicEntity} entity.
+   */
+  public static TopicEntity getTestTopicEntity(
+      long id, String name, Namespace namespace, String comment) {
+    return TopicEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(namespace)
+        .withAuditInfo(getTestAuditInfo())
+        .withComment(comment)
+        .withProperties(ImmutableMap.of())
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test tag entity with default values.
+   *
+   * @return The test {@link TagEntity} entity.
+   */
+  public static TagEntity getTestTagEntity() {
+    return getTestTagEntity(generator.nextId(), "tag_test", 
Namespace.of("m1"), "tag entity test");
+  }
+
+  /**
+   * Returns a test tag entity with the given ID, name, namespace, and comment.
+   *
+   * @param id The ID of the tag entity.
+   * @param name The name of the tag entity.
+   * @param namespace The namespace of the tag entity.
+   * @param comment The comment of the tag entity.
+   * @return The test {@link TagEntity} entity.
+   */
+  public static TagEntity getTestTagEntity(
+      long id, String name, Namespace namespace, String comment) {
+    return TagEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(namespace)
+        .withAuditInfo(getTestAuditInfo())
+        .withComment(comment)
+        .withProperties(ImmutableMap.of())
+        .build();
+  }
+
+  /**
+   * Returns a column entity with default values.
+   *
+   * @return The test {@link ColumnEntity} entity.
+   */
+  public static ColumnEntity getMockColumnEntity() {
+    ColumnEntity mockColumn = mock(ColumnEntity.class);
+    when(mockColumn.name()).thenReturn("filed1");
+    when(mockColumn.dataType()).thenReturn(Types.StringType.get());
+    when(mockColumn.nullable()).thenReturn(false);
+    when(mockColumn.auditInfo()).thenReturn(getTestAuditInfo());
+
+    return mockColumn;
+  }
+
+  /**
+   * Returns an audit info entity with default values.
+   *
+   * @return The test {@link AuditInfo} entity.
+   */
+  public static AuditInfo getTestAuditInfo() {
+    return AuditInfo.builder()
+        .withCreator("admin")
+        .withCreateTime(Instant.now())
+        .withLastModifier("admin")
+        .withLastModifiedTime(Instant.now())
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test user entity with default values.
+   *
+   * @return The test {@link UserEntity} entity.
+   */
+  public static UserEntity getTestUserEntity() {
+    return getTestUserEntity(
+        generator.nextId(), "test_user", "test_metalake", 
ImmutableList.of(1L));
+  }
+
+  /**
+   * Returns a test user entity with the given ID, name, and metalake.
+   *
+   * @param id The ID of the user entity.
+   * @param name The name of the user entity.
+   * @param metalake The metalake of the user entity.
+   * @param roles The roles of the user entity.
+   * @return The test {@link UserEntity} entity.
+   */
+  public static UserEntity getTestUserEntity(
+      long id, String name, String metalake, List<Long> roles) {
+    return UserEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(NamespaceUtil.ofUser(metalake))
+        .withAuditInfo(getTestAuditInfo())
+        .withRoleIds(roles)
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test group entity with default values.
+   *
+   * @return The test {@link GroupEntity} entity.
+   */
+  public static GroupEntity getTestGroupEntity() {
+    return getTestGroupEntity(
+        generator.nextId(), "test_group", "test_metalake", 
ImmutableList.of("role1", "role2"));
+  }
+
+  /**
+   * Returns a test group entity with the given ID, name, metalake, and roles.
+   *
+   * @param id The ID of the group entity.
+   * @param name The name of the group entity.
+   * @param metalake The metalake of the group entity.
+   * @param roles The roles of the group entity.
+   * @return The test {@link GroupEntity} entity.
+   */
+  public static GroupEntity getTestGroupEntity(
+      long id, String name, String metalake, List<String> roles) {
+    return GroupEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(NamespaceUtil.ofGroup(metalake))
+        .withAuditInfo(getTestAuditInfo())
+        .withRoleNames(roles)
+        .build();
+  }
+
+  /**
+   * Shorthand for creating a test role entity with default values.
+   *
+   * @return The test {@link RoleEntity} entity.
+   */
+  public static RoleEntity getTestRoleEntity() {
+    return getTestRoleEntity(generator.nextId(), "test_role", "test_metalake");
+  }
+
+  /**
+   * Returns a test role entity with the given ID, name, and metalake.
+   *
+   * @param id The ID of the role entity.
+   * @param name The name of the role entity.
+   * @param metalake The metalake of the role entity.
+   * @return The test {@link RoleEntity} entity.
+   */
+  public static RoleEntity getTestRoleEntity(long id, String name, String 
metalake) {
+    return RoleEntity.builder()
+        .withId(id)
+        .withName(name)
+        .withNamespace(NamespaceUtil.ofRole(metalake))
+        .withAuditInfo(getTestAuditInfo())
+        .withSecurableObjects(ImmutableList.of(getMockSecurableObject()))
+        .build();
+  }
+
+  /**
+   * Returns a mock securable object with default values.
+   *
+   * @return The test {@link SecurableObject} entity.
+   */
+  public static SecurableObject getMockSecurableObject() {
+    SecurableObject mockObject = mock(SecurableObject.class);
+    when(mockObject.privileges())
+        .thenReturn(
+            ImmutableList.of(Privileges.CreateSchema.allow(), 
Privileges.CreateTable.allow()));
+
+    return mockObject;
+  }
+
+  /**
+   * Shorthand for creating a test model version entity with default values.
+   *
+   * @return The test {@link ModelVersionEntity} entity.
+   */
+  public static ModelVersionEntity getTestModelVersionEntity() {
+    return getTestModelVersionEntity(
+        NameIdentifier.of("m1", "c1", "s1", "model-version-test"),
+        1,
+        "uri",
+        ImmutableMap.of("key1", "value1"),
+        "model version entity test",
+        ImmutableList.of("alias1", "alias2"));
+  }
+
+  /**
+   * Returns a test model version entity with the given model identifier, 
version, uri, properties,
+   * comment, and aliases.
+   *
+   * @param modelIdentifier The model identifier of the model version entity.
+   * @param version The version of the model version entity.
+   * @param uri The uri of the model version entity.
+   * @param properties The properties of the model version entity.
+   * @param comment The comment of the model version entity.
+   * @param aliases The aliases of the model version entity.
+   * @return The test {@link ModelVersionEntity} entity.
+   */
+  public static ModelVersionEntity getTestModelVersionEntity(
+      NameIdentifier modelIdentifier,
+      int version,
+      String uri,
+      Map<String, String> properties,
+      String comment,
+      List<String> aliases) {
+    return ModelVersionEntity.builder()
+        .withVersion(version)
+        .withModelIdentifier(modelIdentifier)
+        .withUri(uri)
+        .withProperties(properties)
+        .withComment(comment)
+        .withAuditInfo(getTestAuditInfo())
+        .withAliases(aliases)
+        .build();
+  }
+}

Reply via email to