Implementation:Datahub project Datahub EntityClient Get Mutable
| Field | Value |
|---|---|
| Implementation Name | EntityClient Get and Mutable |
| Type | API Doc |
| Status | Active |
| Last Updated | 2026-02-10 |
| Repository | Datahub_project_Datahub |
| Source Files | EntityClient.java (Lines 460-533), Entity.java (Lines 385-437)
|
Overview
The get() method on EntityClient fetches an entity from the DataHub server as a read-only object, and the mutable() method on Entity creates a writable copy for modifications. Together, these methods implement the read-modify-write pattern for existing entities.
Source Reference: EntityClient.get()
File: metadata-integration/java/datahub-client/src/main/java/datahub/client/v2/operations/EntityClient.java (Lines 460-533)
@Nonnull
public <T extends Entity> T get(@Nonnull String urn, @Nonnull Class<T> entityClass)
throws IOException, ExecutionException, InterruptedException {
// Create URN object
com.linkedin.common.urn.Urn urnObj = com.linkedin.common.urn.Urn.createFromString(urn);
// Create entity instance to get default aspects
T entity = createEntityInstance(urnObj, entityClass);
// Get default aspects and fetch with them
List<Class<? extends RecordTemplate>> defaultAspects = entity.getDefaultAspects();
return get(urn, entityClass, defaultAspects);
}
@Nonnull
public <T extends Entity> T get(
@Nonnull String urn,
@Nonnull Class<T> entityClass,
@Nonnull List<Class<? extends RecordTemplate>> aspects)
throws IOException, ExecutionException, InterruptedException {
com.linkedin.common.urn.Urn urnObj = com.linkedin.common.urn.Urn.createFromString(urn);
// Build aspect names for single batch fetch
List<String> aspectNames = new ArrayList<>();
Map<String, Class<? extends RecordTemplate>> aspectClassMap = new HashMap<>();
for (Class<? extends RecordTemplate> aspectClass : aspects) {
String aspectName = getAspectName(aspectClass);
aspectNames.add(aspectName);
aspectClassMap.put(aspectName, aspectClass);
}
// Fetch all aspects in a single API call
Map<String, RecordTemplate> aspectCache = fetchAspects(urnObj, aspectNames, aspectClassMap);
// Create entity with loaded aspects (read-only by default)
T entityWithAspects = createEntityInstance(urnObj, entityClass, aspectCache);
entityWithAspects.bindToClient(this, config.getMode());
return entityWithAspects;
}
Source Reference: Entity.mutable()
File: metadata-integration/java/datahub-client/src/main/java/datahub/client/v2/entity/Entity.java (Lines 385-437)
The base Entity class provides a default mutable() implementation using reflection, but entity subclasses override it with dedicated copy constructors:
Dataset.mutable() (Line 182)
@Override
@Nonnull
public Dataset mutable() {
if (!readOnly) {
return this; // Already mutable, return self (idempotent)
}
return new Dataset(this); // Copy constructor
}
Dataset Copy Constructor (Lines 137-148)
protected Dataset(@Nonnull Dataset other) {
super(
other.urn, // Shared: URN
other.cache, // Shared: aspect cache
other.client, // Shared: client reference
other.mode, // Shared: operation mode
new HashMap<>(), // Fresh: pending patches
new ArrayList<>(), // Fresh: pending MCPs
new HashMap<>(), // Fresh: patch builders
false, // Not dirty
false); // Not read-only (mutable)
}
Entity Base mutable() (Lines 385-437)
@Nonnull
@SuppressWarnings("unchecked")
public <T extends Entity> T mutable() {
if (!readOnly) {
return (T) this; // Already mutable
}
// Reflection-based copy with shared cache but independent mutation tracking
// Sets readOnly=false, dirty=false on the copy
...
}
getAspectLazy()
File: metadata-integration/java/datahub-client/src/main/java/datahub/client/v2/entity/Entity.java (Lines 491-528)
@Nullable
public <T extends RecordTemplate> T getAspectLazy(@Nonnull Class<T> aspectClass) {
String aspectName = getAspectName(aspectClass);
// Try cache first
T cached = cache.get(aspectName, aspectClass, ReadMode.ALLOW_DIRTY);
if (cached != null) { return cached; }
// Skip lazy-load if entity has pending mutations
if (dirty && client != null) { return null; }
// Fetch from server if client is bound
if (client != null) {
AspectWithMetadata<T> aspectWithMetadata = client.getAspect(urn, aspectClass);
T aspect = aspectWithMetadata.getAspect();
if (aspect != null) {
cache.put(aspectName, aspect, AspectSource.SERVER, false);
}
return aspect;
}
return null;
}
Method Signatures
EntityClient.get() (default aspects)
public <T extends Entity> T get(@Nonnull String urn, @Nonnull Class<T> entityClass)
throws IOException, ExecutionException, InterruptedException
EntityClient.get() (specified aspects)
public <T extends Entity> T get(
@Nonnull String urn,
@Nonnull Class<T> entityClass,
@Nonnull List<Class<? extends RecordTemplate>> aspects)
throws IOException, ExecutionException, InterruptedException
Entity.mutable()
public <T extends Entity> T mutable()
Accessed via:
// Fetch read-only entity
Dataset dataset = client.entities().get(urnString, Dataset.class);
// Create mutable copy
Dataset mutable = dataset.mutable();
I/O Contract
get()
Input:
urn-- String URN of the entity to fetch (e.g.,"urn:li:dataset:(urn:li:dataPlatform:snowflake,my_table,PROD)")entityClass-- The entity class (e.g.,Dataset.class)- (Optional)
aspects-- List of specific aspect classes to fetch
Output: A read-only entity instance with:
- Pre-loaded aspects from the server (in the WriteTrackingAspectCache)
readOnly = true(mutation methods throwReadOnlyEntityException)- Bound to the client for lazy loading of additional aspects
Exceptions:
IOException-- if the entity does not exist or network error occursExecutionException-- if the server returns an error responseInterruptedException-- if the thread is interrupted while waiting
mutable()
Input: A read-only entity instance (from get())
Output: A mutable entity instance with:
- Shared aspect cache (reads see the same pre-loaded data as the original)
- Independent mutation tracking (fresh pending patches, MCPs, and patch builders)
readOnly = false,dirty = false
If called on an already mutable entity, returns the same instance (idempotent).
getAspectLazy()
Input: An aspect class (e.g., GlobalTags.class)
Output: The aspect instance, or null if not available. Loading behavior:
- Returns cached aspect if present (even if dirty)
- Skips server fetch if entity has pending mutations (dirty flag)
- Fetches from server and caches if client is bound and aspect is not cached
- Returns null if no client is bound and aspect is not cached
Usage Examples
Read-Modify-Write Pattern
// Phase 1: Read (immutable)
Dataset dataset = client.entities().get(
"urn:li:dataset:(urn:li:dataPlatform:snowflake,my_table,PROD)",
Dataset.class);
dataset.isReadOnly(); // true
dataset.getDescription(); // Works fine - reads from cache
// Phase 2: Mutable copy
Dataset mutable = dataset.mutable();
mutable.isReadOnly(); // false
mutable.isMutable(); // true
// Phase 3: Modify and persist
mutable.addTag("verified");
mutable.setDescription("Updated description");
mutable.addOwner("urn:li:corpuser:johndoe", OwnershipType.DATA_OWNER);
client.entities().upsert(mutable);
Fetch with Specific Aspects
Dataset dataset = client.entities().get(
urnString,
Dataset.class,
List.of(DatasetProperties.class, GlobalTags.class));
Lazy Loading
Dataset dataset = client.entities().get(urnString, Dataset.class);
// SchemaMetadata is not in default aspects, but will be lazy-loaded on access
SchemaMetadata schema = dataset.getSchema();
// Triggers: client.getAspect(urn, SchemaMetadata.class) under the hood
When mutable() is called, the following table shows which state is shared between the original and the mutable copy:
| State | Shared? | Reason |
|---|---|---|
| URN | Shared | Identity is immutable |
Aspect cache (WriteTrackingAspectCache) |
Shared | Reads see the same server data |
| Client reference | Shared | Both use the same server connection |
| Operation mode | Shared | Both use the same mode (SDK/INGESTION) |
Pending patches (pendingPatches) |
Independent (fresh empty) | Mutations are independent |
Pending MCPs (pendingMCPs) |
Independent (fresh empty) | Mutations are independent |
Patch builders (patchBuilders) |
Independent (fresh empty) | Mutations are independent |
| Dirty flag | Independent (false) |
Copy starts clean |
| Read-only flag | Independent (false) |
Copy is mutable |
Related
- Implements: Datahub_project_Datahub_Entity_Read_Modify
- Depends on: Datahub_project_Datahub_DataHubClientV2_Builder
- Related Implementation: Datahub_project_Datahub_EntityClient_Upsert
- Related Implementation: Datahub_project_Datahub_Entity_Metadata_Mutations
- Environment: Environment:Datahub_project_Datahub_Java_17_Backend_Environment