Remove unnecessary db query to determine whether a model exists in db

This commit is contained in:
Alice Gaudon 2020-09-06 10:43:45 +02:00
parent f07704c6dc
commit 25f890e082
4 changed files with 51 additions and 53 deletions

View File

@ -21,7 +21,7 @@ export default abstract class Model {
} }
public static create<T extends Model>(this: ModelType<T>, data: any): T { public static create<T extends Model>(this: ModelType<T>, data: any): T {
return ModelFactory.get(this).create(data); return ModelFactory.get(this).create(data, true);
} }
public static select<T extends Model>(this: ModelType<T>, ...fields: string[]): ModelQuery<T> { public static select<T extends Model>(this: ModelType<T>, ...fields: string[]): ModelQuery<T> {
@ -47,14 +47,15 @@ export default abstract class Model {
protected readonly _factory: ModelFactory<any>; protected readonly _factory: ModelFactory<any>;
private readonly _components: ModelComponent<any>[] = []; private readonly _components: ModelComponent<any>[] = [];
private readonly _validators: { [key: string]: Validator<any> } = {}; private readonly _validators: { [key: string]: Validator<any> } = {};
private _cached_exists?: boolean = undefined; private _exists: boolean;
[key: string]: any; [key: string]: any;
public constructor(factory: ModelFactory<any>) { public constructor(factory: ModelFactory<any>, isNew: boolean) {
if (!factory || !(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.'); if (!factory || !(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.');
this._factory = factory; this._factory = factory;
this.init(); this.init();
this._exists = !isNew;
} }
protected abstract init(): void; protected abstract init(): void;
@ -94,28 +95,29 @@ export default abstract class Model {
protected async autoFill(): Promise<void> { protected async autoFill(): Promise<void> {
} }
protected async beforeSave(exists: boolean, connection: Connection): Promise<void> { protected async beforeSave(connection: Connection): Promise<void> {
} }
protected async afterSave(): Promise<void> { protected async afterSave(): Promise<void> {
} }
public async save(connection?: Connection, postHook?: (callback: () => Promise<void>) => void): Promise<void> { public async save(connection?: Connection, postHook?: (callback: () => Promise<void>) => void): Promise<void> {
if (connection && !postHook) throw new Error('If connection is provided, postHook must be provided too.');
await this.autoFill(); await this.autoFill();
await this.validate(false, connection); await this.validate(false, connection);
const exists = await this.exists(); const needs_full_update = connection ?
let needs_full_update = false; await this.saveTransaction(connection) :
await MysqlConnectionManager.wrapTransaction(async connection => this.saveTransaction(connection));
if (connection) {
needs_full_update = await this.saveTransaction(connection, exists, needs_full_update);
} else {
needs_full_update = await MysqlConnectionManager.wrapTransaction(async connection => this.saveTransaction(connection, exists, needs_full_update));
}
const callback = async () => { const callback = async () => {
if (needs_full_update) { if (needs_full_update) {
this.updateWithData((await this._factory.select().where('id', this.id!).limit(1).execute()).results[0]); const result = await this._factory.select()
.where('id', this.id!)
.limit(1)
.execute();
this.updateWithData(result.results[0]);
} }
await this.afterSave(); await this.afterSave();
@ -128,65 +130,67 @@ export default abstract class Model {
} }
} }
private async saveTransaction(connection: Connection, exists: boolean, needs_full_update: boolean): Promise<boolean> { private async saveTransaction(connection: Connection): Promise<boolean> {
// Before save // Before save
await this.beforeSave(exists, connection); await this.beforeSave(connection);
if (!exists && this.hasOwnProperty('created_at')) { if (!this.exists() && this.hasOwnProperty('created_at')) {
this.created_at = new Date(); this.created_at = new Date();
} }
if (exists && this.hasOwnProperty('updated_at')) { if (this.exists() && this.hasOwnProperty('updated_at')) {
this.updated_at = new Date(); this.updated_at = new Date();
} }
const properties = []; const properties = [];
const values = []; const values = [];
let needsFullUpdate = false;
if (exists) { if (this.exists()) {
const data: any = {}; const data: any = {};
for (const property of this._properties) { for (const property of this._properties) {
const value = this[property]; const value = this[property];
if (value !== undefined) {
data[property] = value; if (value === undefined) needsFullUpdate = true;
} else { else data[property] = value;
needs_full_update = true;
} }
}
let query = this._factory.update(data); const query = this._factory.update(data);
for (const indexField of this._factory.getPrimaryKeyFields()) { for (const indexField of this._factory.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]); query.where(indexField, this[indexField]);
} }
await query.execute(connection); await query.execute(connection);
} else { } else {
const props_holders = []; const props_holders = [];
for (const property of this._properties) { for (const property of this._properties) {
const value = this[property]; const value = this[property];
if (value !== undefined) {
if (value === undefined) {
needsFullUpdate = true;
} else {
properties.push(property); properties.push(property);
props_holders.push('?'); props_holders.push('?');
values.push(value); values.push(value);
} else {
needs_full_update = true;
} }
} }
const fieldNames = properties.map(f => `\`${f}\``).join(', '); const fieldNames = properties.map(f => `\`${f}\``).join(', ');
const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection); const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection);
if (this.hasOwnProperty('id')) this.id = result.other.insertId; if (this.hasOwnProperty('id')) this.id = result.other.insertId;
this._cached_exists = true; this._exists = true;
} }
return needs_full_update; return needsFullUpdate;
} }
public async delete(): Promise<void> { public async delete(): Promise<void> {
if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.'); if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.');
let query = this._factory.delete(); const query = this._factory.delete();
for (const indexField of this._factory.getPrimaryKeyFields()) { for (const indexField of this._factory.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]); query.where(indexField, this[indexField]);
} }
await query.execute(); await query.execute();
this._cached_exists = false; this._exists = false;
} }
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> { public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
@ -195,16 +199,8 @@ export default abstract class Model {
)); ));
} }
public async exists(): Promise<boolean> { public exists(): boolean {
if (this._cached_exists === undefined) { return this._exists;
const query = this._factory.select('');
for (const indexField of this._factory.getPrimaryKeyFields()) {
query.where(indexField, this[indexField]);
}
this._cached_exists = (await query.limit(1).execute()).results.length > 0;
}
return this._cached_exists;
} }
public equals(model: this): boolean { public equals(model: this): boolean {
@ -230,5 +226,7 @@ export default abstract class Model {
export interface ModelType<T extends Model> extends Type<T> { export interface ModelType<T extends Model> extends Type<T> {
table: string; table: string;
new(factory: ModelFactory<any>, isNew: boolean): T;
getPrimaryKeyFields(): string[]; getPrimaryKeyFields(): string[];
} }

View File

@ -28,8 +28,8 @@ export default class ModelFactory<T extends Model> {
this.components.push(modelComponentFactory); this.components.push(modelComponentFactory);
} }
public create(data: any): T { public create(data: any, isNewModel: boolean): T {
const model = new this.modelType(this, data); const model = new this.modelType(this, isNewModel);
for (const component of this.components) { for (const component of this.components) {
model.addComponent(new component(model)); model.addComponent(new component(model));
} }

View File

@ -240,7 +240,7 @@ export default class ModelQuery<M extends Model> implements WhereFieldConsumer<M
modelData[field.split('.')[1] || field] = result[field]; modelData[field.split('.')[1] || field] = result[field];
} }
const model = this.factory.create(modelData); const model = this.factory.create(modelData, false);
models.push(model); models.push(model);
models.originalData.push(modelData); models.originalData.push(modelData);

View File

@ -47,7 +47,7 @@ describe('Model', () => {
name: 'a_name', name: 'a_name',
date: date, date: date,
non_existing_property: 'dropped_value', non_existing_property: 'dropped_value',
}); }, true);
expect(model.id).toBeUndefined(); expect(model.id).toBeUndefined();
expect(model.name).toBe('a_name'); expect(model.name).toBe('a_name');
@ -67,13 +67,13 @@ describe('Model', () => {
const insertInstance: FakeDummyModel | null = factory.create({ const insertInstance: FakeDummyModel | null = factory.create({
name: 'name1', name: 'name1',
date: date, date: date,
}); }, true);
// Insert // Insert
expect(await insertInstance.exists()).toBeFalsy(); expect(insertInstance.exists()).toBeFalsy();
await insertInstance.save(); await insertInstance.save();
expect(await insertInstance.exists()).toBeTruthy(); expect(insertInstance.exists()).toBeTruthy();
expect(insertInstance.id).toBe(1); // Auto id from insert expect(insertInstance.id).toBe(1); // Auto id from insert
expect(insertInstance.name).toBe('name1'); expect(insertInstance.name).toBe('name1');
@ -90,14 +90,14 @@ describe('Model', () => {
const failingInsertModel = factory.create({ const failingInsertModel = factory.create({
name: 'a', name: 'a',
}); }, true);
await expect(failingInsertModel.save()).rejects.toBeInstanceOf(ValidationBag); await expect(failingInsertModel.save()).rejects.toBeInstanceOf(ValidationBag);
}); });
it('should update properly', async () => { it('should update properly', async () => {
const insertModel = factory.create({ const insertModel = factory.create({
name: 'update', name: 'update',
}); }, true);
await insertModel.save(); await insertModel.save();
const preUpdatedModel = await FakeDummyModel.getById(insertModel.id); const preUpdatedModel = await FakeDummyModel.getById(insertModel.id);
@ -118,7 +118,7 @@ describe('Model', () => {
it('should delete properly', async () => { it('should delete properly', async () => {
const insertModel = factory.create({ const insertModel = factory.create({
name: 'delete', name: 'delete',
}); }, true);
await insertModel.save(); await insertModel.save();
const preDeleteModel = await FakeDummyModel.getById(insertModel.id); const preDeleteModel = await FakeDummyModel.getById(insertModel.id);