Optimize EntityMetadata caching in GlobalWorkspaceDataSource (#16146)

## Context
EntityMetadata was being rebuilt from scratch on every
findMetadata()/getMetadata() call (~20 times per request). This involved
running EntitySchemaTransformer.transform() and
EntityMetadataBuilder.build() repeatedly, causing unnecessary CPU
overhead.

## Implementation
Cache entityMetadatas in ORMWorkspaceContext: Build EntityMetadata once
during workspace context initialization instead of on every metadata
lookup
Remove redundant entitySchemas caching: Since flatMetadata is already
cached, the additional Redis cache for entitySchemaOptions was
unnecessary overhead
Remove WorkspaceEntitiesStorage: Replaced with direct lookup from
FlatObjectMetadataMap
Simplify getObjectMetadataFromEntityTarget: Now only accepts string
targets, using flat metadata maps directly

Also:
Removed unused injections in some services
This commit is contained in:
Weiko
2025-11-28 10:09:52 +01:00
committed by GitHub
parent fd9ea2f5ee
commit 9620a4b0ba
11 changed files with 45 additions and 148 deletions
@@ -6,7 +6,6 @@ import {
} from '@nestjs/common';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { GlobalWorkspaceDataSource } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@@ -19,7 +18,6 @@ export class GlobalWorkspaceDataSourceService
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly entitySchemaFactory: EntitySchemaFactory,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
) {}
@@ -4,7 +4,6 @@ import {
DataSource,
type DataSourceOptions,
type EntityMetadata,
type EntitySchema,
type EntityTarget,
type ObjectLiteral,
type QueryRunner,
@@ -12,9 +11,7 @@ import {
type SelectQueryBuilder,
} from 'typeorm';
import { EntityManagerFactory } from 'typeorm/entity-manager/EntityManagerFactory';
import { EntitySchemaTransformer } from 'typeorm/entity-schema/EntitySchemaTransformer';
import { EntityMetadataNotFoundError } from 'typeorm/error/EntityMetadataNotFoundError';
import { EntityMetadataBuilder } from 'typeorm/metadata-builder/EntityMetadataBuilder';
import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
@@ -26,7 +23,7 @@ import {
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/workspace-context.storage';
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
@@ -83,11 +80,9 @@ export class GlobalWorkspaceDataSource extends DataSource {
target: EntityTarget<ObjectLiteral>,
): EntityMetadata | undefined {
const context = getWorkspaceContext();
const { entitySchemas } = context;
const { entityMetadatas } = context;
const entityMetadata = this.buildMetadatasFromSchemas(entitySchemas);
return entityMetadata.find((metadata) => metadata.target === target);
return entityMetadatas.find((metadata) => metadata.target === target);
}
override getMetadata(target: EntityTarget<ObjectLiteral>): EntityMetadata {
@@ -264,20 +259,4 @@ export class GlobalWorkspaceDataSource extends DataSource {
return super.query(query, parameters, queryRunner);
}
private buildMetadatasFromSchemas(
entitySchemas: EntitySchema[],
): EntityMetadata[] {
const transformer = new EntitySchemaTransformer();
const metadataArgsStorage = transformer.transform(entitySchemas);
const entityMetadataBuilder = new EntityMetadataBuilder(
this,
metadataArgsStorage,
);
const entityMetadatas = entityMetadataBuilder.build();
return entityMetadatas;
}
}
@@ -2,6 +2,8 @@ import { Injectable, type Type } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { EntitySchema, ObjectLiteral } from 'typeorm';
import { EntitySchemaTransformer } from 'typeorm/entity-schema/EntitySchemaTransformer';
import { EntityMetadataBuilder } from 'typeorm/metadata-builder/EntityMetadataBuilder';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
@@ -16,9 +18,9 @@ import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-sche
import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import {
type WorkspaceContext,
type ORMWorkspaceContext,
withWorkspaceContext,
} from 'src/engine/twenty-orm/storage/workspace-context.storage';
} from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
@@ -85,7 +87,7 @@ export class GlobalWorkspaceOrmManager {
private async loadWorkspaceContext(
authContext: WorkspaceAuthContext,
): Promise<WorkspaceContext> {
): Promise<ORMWorkspaceContext> {
const workspaceId = authContext.workspace.id;
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } =
@@ -103,15 +105,6 @@ export class GlobalWorkspaceOrmManager {
const { idByNameSingular: objectIdByNameSingular } =
buildObjectIdByNameMaps(flatObjectMetadataMaps);
const metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (!isDefined(metadataVersion)) {
throw new Error(
`Metadata version not found for workspace ${workspaceId}`,
);
}
const { data: featureFlagsMap } =
await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMapAndVersion(
{ workspaceId },
@@ -122,65 +115,52 @@ export class GlobalWorkspaceOrmManager {
workspaceId,
});
const entitySchemas = await this.buildEntitySchemas(
const entitySchemas = this.buildEntitySchemas(
workspaceId,
metadataVersion,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const entityMetadatas = this.buildEntityMetadatas(entitySchemas);
return {
authContext,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatIndexMaps,
objectIdByNameSingular,
metadataVersion,
featureFlagsMap,
permissionsPerRoleId,
entitySchemas,
entityMetadatas,
};
}
private async buildEntitySchemas(
private buildEntitySchemas(
workspaceId: string,
dataSourceMetadataVersion: number,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) {
const cachedEntitySchemaOptions =
await this.workspaceCacheStorageService.getORMEntitySchema(
workspaceId,
dataSourceMetadataVersion,
return Object.values(flatObjectMetadataMaps.byId)
.filter(isDefined)
.map((flatObjectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
),
);
}
let cachedEntitySchemas: EntitySchema[];
private buildEntityMetadatas(entitySchemas: EntitySchema[]) {
const transformer = new EntitySchemaTransformer();
const metadataArgsStorage = transformer.transform(entitySchemas);
if (cachedEntitySchemaOptions) {
cachedEntitySchemas = cachedEntitySchemaOptions.map(
(option) => new EntitySchema(option),
);
} else {
const entitySchemas = Object.values(flatObjectMetadataMaps.byId)
.filter(isDefined)
.map((flatObjectMetadata) =>
this.entitySchemaFactory.create(
workspaceId,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
),
);
const entityMetadataBuilder = new EntityMetadataBuilder(
this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource(),
metadataArgsStorage,
);
await this.workspaceCacheStorageService.setORMEntitySchema(
workspaceId,
dataSourceMetadataVersion,
entitySchemas.map((entitySchema) => entitySchema.options),
);
cachedEntitySchemas = entitySchemas;
}
return cachedEntitySchemas;
return entityMetadataBuilder.build();
}
}