Latest entity queries to optimize frontend app GQL requests (#232)

* Add latest entity table query pattern

* Add subscriber config to typeorm
This commit is contained in:
nikugogoi 2022-11-16 18:11:27 +05:30 committed by GitHub
parent 408a3927c0
commit a084b4e40c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 140 additions and 38 deletions

View File

@ -44,7 +44,8 @@ export class Database implements DatabaseInterface {
this._config = { this._config = {
...config, ...config,
entities: [path.join(__dirname, 'entity/*')] entities: [path.join(__dirname, 'entity/*')],
subscribers: [path.join(__dirname, 'entity/Subscriber.*')]
}; };
this._baseDatabase = new BaseDatabase(this._config); this._baseDatabase = new BaseDatabase(this._config);

View File

@ -4,9 +4,10 @@
import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm'; import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm';
import { afterEntityInsertOrUpdate } from '@cerc-io/graph-node';
import { FrothyEntity } from './FrothyEntity'; import { FrothyEntity } from './FrothyEntity';
import { ENTITIES } from '../database'; import { ENTITIES } from '../database';
import { afterEntityInsertOrUpdate } from '@cerc-io/graph-node';
@EventSubscriber() @EventSubscriber()
export class EntitySubscriber implements EntitySubscriberInterface { export class EntitySubscriber implements EntitySubscriberInterface {

View File

@ -68,16 +68,23 @@ export class Database {
_conn!: Connection _conn!: Connection
_baseDatabase: BaseDatabase _baseDatabase: BaseDatabase
_entityQueryTypeMap: Map<new() => any, ENTITY_QUERY_TYPE> _entityQueryTypeMap: Map<new() => any, ENTITY_QUERY_TYPE>
_entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
_cachedEntities: CachedEntities = { _cachedEntities: CachedEntities = {
frothyBlocks: new Map(), frothyBlocks: new Map(),
latestPrunedEntities: new Map() latestPrunedEntities: new Map()
} }
constructor (serverConfig: ServerConfig, baseDatabase: BaseDatabase, entityQueryTypeMap: Map<new() => any, ENTITY_QUERY_TYPE> = new Map()) { constructor (
serverConfig: ServerConfig,
baseDatabase: BaseDatabase,
entityQueryTypeMap: Map<new () => any, ENTITY_QUERY_TYPE> = new Map(),
entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
) {
this._serverConfig = serverConfig; this._serverConfig = serverConfig;
this._baseDatabase = baseDatabase; this._baseDatabase = baseDatabase;
this._entityQueryTypeMap = entityQueryTypeMap; this._entityQueryTypeMap = entityQueryTypeMap;
this._entityToLatestEntityMap = entityToLatestEntityMap;
} }
get cachedEntities () { get cachedEntities () {
@ -367,8 +374,21 @@ export class Database {
queryOptions: QueryOptions = {}, queryOptions: QueryOptions = {},
selections: ReadonlyArray<SelectionNode> = [] selections: ReadonlyArray<SelectionNode> = []
): Promise<Entity[]> { ): Promise<Entity[]> {
let entities: Entity[]; let entities: Entity[] = [];
const latestEntity = this._entityToLatestEntityMap.get(entity);
if (latestEntity) {
if (!Object.keys(block).length) {
// Use latest entity tables if block height not passed.
entities = await this.getEntitiesLatest(
queryRunner,
entity,
latestEntity,
where,
queryOptions
);
}
} else {
// Use different suitable query patterns based on entities. // Use different suitable query patterns based on entities.
switch (this._entityQueryTypeMap.get(entity)) { switch (this._entityQueryTypeMap.get(entity)) {
case ENTITY_QUERY_TYPE.SINGULAR: case ENTITY_QUERY_TYPE.SINGULAR:
@ -402,6 +422,7 @@ export class Database {
entities = await this.getEntitiesGroupBy(queryRunner, entity, block, where, queryOptions); entities = await this.getEntitiesGroupBy(queryRunner, entity, block, where, queryOptions);
break; break;
} }
}
if (!entities.length) { if (!entities.length) {
return []; return [];
@ -805,6 +826,42 @@ export class Database {
return entities as Entity[]; return entities as Entity[];
} }
async getEntitiesLatest<Entity> (
queryRunner: QueryRunner,
entity: new () => Entity,
latestEntity: new () => any,
where: Where = {},
queryOptions: QueryOptions = {}
): Promise<Entity[]> {
const repo = queryRunner.manager.getRepository(entity);
const { tableName } = repo.metadata;
let selectQueryBuilder = repo.createQueryBuilder(tableName)
.innerJoin(
latestEntity,
'latest',
`latest.id = ${tableName}.id AND latest.blockHash = ${tableName}.blockHash`
);
selectQueryBuilder = this._baseDatabase.buildQuery(repo, selectQueryBuilder, where, 'latest');
if (queryOptions.orderBy) {
selectQueryBuilder = this._baseDatabase.orderQuery(repo, selectQueryBuilder, queryOptions, '', 'latest');
}
selectQueryBuilder = this._baseDatabase.orderQuery(repo, selectQueryBuilder, { ...queryOptions, orderBy: 'id' }, '', 'latest');
if (queryOptions.skip) {
selectQueryBuilder = selectQueryBuilder.offset(queryOptions.skip);
}
if (queryOptions.limit) {
selectQueryBuilder = selectQueryBuilder.limit(queryOptions.limit);
}
return selectQueryBuilder.getMany();
}
async loadEntitiesRelations<Entity> ( async loadEntitiesRelations<Entity> (
queryRunner: QueryRunner, queryRunner: QueryRunner,
block: BlockHeight, block: BlockHeight,

View File

@ -899,10 +899,18 @@ export const updateEntitiesFromState = async (database: Database, indexer: Index
} }
}; };
export const afterEntityInsertOrUpdate = async<Entity> (frothyEntityType: EntityTarget<Entity>, entities: Set<any>, event: InsertEvent<any> | UpdateEvent<any>): Promise<void> => { export const afterEntityInsertOrUpdate = async<Entity> (
frothyEntityType: EntityTarget<Entity>,
entities: Set<any>,
event: InsertEvent<any> | UpdateEvent<any>,
entityToLatestEntityMap: Map<new () => any, new () => any> = new Map()
): Promise<void> => {
const entity = event.entity; const entity = event.entity;
// TODO: Check and return if entity is being pruned (is_pruned flag update) // Return if the entity is being pruned
if (entity.isPruned) {
return;
}
// Insert the entity details in FrothyEntity table // Insert the entity details in FrothyEntity table
if (entities.has(entity.constructor)) { if (entities.has(entity.constructor)) {
@ -922,5 +930,26 @@ export const afterEntityInsertOrUpdate = async<Entity> (frothyEntityType: Entity
.execute(); .execute();
} }
// TOOD: Update latest entity tables // Get latest entity's type
const entityTarget = entityToLatestEntityMap.get(entity.constructor);
if (!entityTarget) {
return;
}
// Get latest entity's fields to be updated
const latestEntityRepo = event.manager.getRepository(entityTarget);
const latestEntityFields = latestEntityRepo.metadata.columns.map(column => column.propertyName);
const fieldsToUpdate = latestEntityRepo.metadata.columns.map(column => column.databaseName).filter(val => val !== 'id');
// Create a latest entity instance and upsert in the db
const latestEntity = event.manager.create(entityTarget, _.pick(entity, latestEntityFields));
await event.manager.createQueryBuilder()
.insert()
.into(entityTarget)
.values(latestEntity)
.orUpdate(
{ conflict_target: ['id'], overwrite: fieldsToUpdate }
)
.execute();
}; };

View File

@ -788,20 +788,29 @@ export class Database {
return repo.save(entity); return repo.save(entity);
} }
buildQuery<Entity> (repo: Repository<Entity>, selectQueryBuilder: SelectQueryBuilder<Entity>, where: Where = {}): SelectQueryBuilder<Entity> { buildQuery<Entity> (
repo: Repository<Entity>,
selectQueryBuilder: SelectQueryBuilder<Entity>,
where: Where = {},
alias?: string
): SelectQueryBuilder<Entity> {
if (!alias) {
alias = selectQueryBuilder.alias;
}
Object.entries(where).forEach(([field, filters]) => { Object.entries(where).forEach(([field, filters]) => {
filters.forEach((filter, index) => { filters.forEach((filter, index) => {
// Form the where clause. // Form the where clause.
let { not, operator, value } = filter; let { not, operator, value } = filter;
const columnMetadata = repo.metadata.findColumnWithPropertyName(field); const columnMetadata = repo.metadata.findColumnWithPropertyName(field);
assert(columnMetadata); assert(columnMetadata);
let whereClause = `"${selectQueryBuilder.alias}"."${columnMetadata.databaseName}" `; let whereClause = `"${alias}"."${columnMetadata.databaseName}" `;
if (columnMetadata.relationMetadata) { if (columnMetadata.relationMetadata) {
// For relation fields, use the id column. // For relation fields, use the id column.
const idColumn = columnMetadata.relationMetadata.joinColumns.find(column => column.referencedColumn?.propertyName === 'id'); const idColumn = columnMetadata.relationMetadata.joinColumns.find(column => column.referencedColumn?.propertyName === 'id');
assert(idColumn); assert(idColumn);
whereClause = `"${selectQueryBuilder.alias}"."${idColumn.databaseName}" `; whereClause = `"${alias}"."${idColumn.databaseName}" `;
} }
if (not) { if (not) {
@ -853,8 +862,13 @@ export class Database {
repo: Repository<Entity>, repo: Repository<Entity>,
selectQueryBuilder: SelectQueryBuilder<Entity>, selectQueryBuilder: SelectQueryBuilder<Entity>,
orderOptions: { orderBy?: string, orderDirection?: string }, orderOptions: { orderBy?: string, orderDirection?: string },
columnPrefix = '' columnPrefix = '',
alias?: string
): SelectQueryBuilder<Entity> { ): SelectQueryBuilder<Entity> {
if (!alias) {
alias = selectQueryBuilder.alias;
}
const { orderBy, orderDirection } = orderOptions; const { orderBy, orderDirection } = orderOptions;
assert(orderBy); assert(orderBy);
@ -862,7 +876,7 @@ export class Database {
assert(columnMetadata); assert(columnMetadata);
return selectQueryBuilder.addOrderBy( return selectQueryBuilder.addOrderBy(
`"${selectQueryBuilder.alias}"."${columnPrefix}${columnMetadata.databaseName}"`, `"${alias}"."${columnPrefix}${columnMetadata.databaseName}"`,
orderDirection === 'desc' ? 'DESC' : 'ASC' orderDirection === 'desc' ? 'DESC' : 'ASC'
); );
} }