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 {
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> {
@ -47,14 +47,15 @@ export default abstract class Model {
protected readonly _factory: ModelFactory<any>;
private readonly _components: ModelComponent<any>[] = [];
private readonly _validators: { [key: string]: Validator<any> } = {};
private _cached_exists?: boolean = undefined;
private _exists: boolean;
[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.');
this._factory = factory;
this.init();
this._exists = !isNew;
}
protected abstract init(): void;
@ -94,28 +95,29 @@ export default abstract class Model {
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> {
}
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.validate(false, connection);
const exists = await this.exists();
let needs_full_update = false;
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 needs_full_update = connection ?
await this.saveTransaction(connection) :
await MysqlConnectionManager.wrapTransaction(async connection => this.saveTransaction(connection));
const callback = async () => {
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();
@ -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
await this.beforeSave(exists, connection);
if (!exists && this.hasOwnProperty('created_at')) {
await this.beforeSave(connection);
if (!this.exists() && this.hasOwnProperty('created_at')) {
this.created_at = new Date();
}
if (exists && this.hasOwnProperty('updated_at')) {
if (this.exists() && this.hasOwnProperty('updated_at')) {
this.updated_at = new Date();
}
const properties = [];
const values = [];
let needsFullUpdate = false;
if (exists) {
if (this.exists()) {
const data: any = {};
for (const property of this._properties) {
const value = this[property];
if (value !== undefined) {
data[property] = value;
} else {
needs_full_update = true;
if (value === undefined) needsFullUpdate = true;
else data[property] = value;
}
}
let query = this._factory.update(data);
const query = this._factory.update(data);
for (const indexField of this._factory.getPrimaryKeyFields()) {
query = query.where(indexField, this[indexField]);
query.where(indexField, this[indexField]);
}
await query.execute(connection);
} else {
const props_holders = [];
for (const property of this._properties) {
const value = this[property];
if (value !== undefined) {
if (value === undefined) {
needsFullUpdate = true;
} else {
properties.push(property);
props_holders.push('?');
values.push(value);
} else {
needs_full_update = true;
}
}
const fieldNames = properties.map(f => `\`${f}\``).join(', ');
const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection);
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> {
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()) {
query = query.where(indexField, this[indexField]);
query.where(indexField, this[indexField]);
}
await query.execute();
this._cached_exists = false;
this._exists = false;
}
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
@ -195,16 +199,8 @@ export default abstract class Model {
));
}
public async exists(): Promise<boolean> {
if (this._cached_exists === undefined) {
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 exists(): boolean {
return this._exists;
}
public equals(model: this): boolean {
@ -230,5 +226,7 @@ export default abstract class Model {
export interface ModelType<T extends Model> extends Type<T> {
table: string;
new(factory: ModelFactory<any>, isNew: boolean): T;
getPrimaryKeyFields(): string[];
}

View File

@ -28,8 +28,8 @@ export default class ModelFactory<T extends Model> {
this.components.push(modelComponentFactory);
}
public create(data: any): T {
const model = new this.modelType(this, data);
public create(data: any, isNewModel: boolean): T {
const model = new this.modelType(this, isNewModel);
for (const component of this.components) {
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];
}
const model = this.factory.create(modelData);
const model = this.factory.create(modelData, false);
models.push(model);
models.originalData.push(modelData);

View File

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