Remove unnecessary db query to determine whether a model exists in db
This commit is contained in:
parent
f07704c6dc
commit
25f890e082
@ -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[];
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user