Last active
June 30, 2020 04:10
-
-
Save samchon/7c88fe51c96b657766f0027b2fa8c9a7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { BaseEntity, ObjectType } from "typeorm"; | |
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; | |
import { HashMap } from "tstl/container/HashMap"; | |
import { Pair } from "tstl/utility/Pair"; | |
import { Singleton } from "tstl/thread/Singleton"; | |
import { DomainError } from "tstl/exception/DomainError"; | |
import { SpecialFields } from "../../typings/SpecialFields"; | |
/** | |
* 추상 ORM 클래스 | |
* | |
* @type Entity 파생 타입 | |
* @author Jeongho Nam - https://github.com/samchon | |
*/ | |
export abstract class Model<Entity extends Model<Entity>> | |
extends BaseEntity | |
{ | |
/** | |
* 주 식별자 ID. | |
*/ | |
public abstract get id(): number; | |
/* ----------------------------------------------------------- | |
CONSTRUCTORS | |
----------------------------------------------------------- */ | |
/** | |
* @inheritDoc | |
*/ | |
public async reload(): Promise<void> | |
{ | |
// DO RELOAD | |
await super.reload(); | |
// CLEAR DICTIONARIES | |
for (let dict of [this.belongs_to_, this.has_one_, this.has_many_, this.has_many_to_many_]) | |
dict.clear(); | |
} | |
/** | |
* 현재의 레코드를 update 한다. | |
* | |
* TypeORM 은 특정 테이블의 주 식별자 {@link id} 필드가 `AUTO_INCREMENT` 가 아닌 경우, 각 | |
* 인스턴스가 새로 생성된 것인지, 아니면 기존의 것을 불러온 것인지 구분하지 못한다. 따라서 테이블의 | |
* 주 식별자 {@link id} 필드가 `AUTO_INCREMENT` 가 아닌데 {@link save}() 메서드를 호출하는 경우, | |
* TypeORM 은 `UPDATE` 가 아닌 `INSERT` 쿼리를 실행해버리고 만다. | |
* | |
* 따라서 테이블의 주 식별자가 {@link id} 필드가 `AUTO_INCREMENT` 가 아니라면, 각 레코드를 신규 | |
* 생성할 때는 {@link save}() 메서드를, 기존의 레코드를 수정할 때는 {@link update}() 메서드를 | |
* 구분해서 호출해줘야 한다. | |
*/ | |
public async update(): Promise<void> | |
{ | |
let columnList = (this.constructor as typeof BaseEntity).getRepository().metadata.columns; | |
let props: any = {}; | |
for (let column of columnList) | |
{ | |
let key: string = column.propertyName; | |
props[key] = (this as any)[key]; | |
} | |
await (this.constructor as typeof BaseEntity).update(this.id, props); | |
} | |
/** | |
* Get 테이블명 | |
* | |
* @return 테이블 이름 | |
*/ | |
public getTable(): string | |
{ | |
return Model.getTable(this.constructor); | |
} | |
public static async bulkInsert<Entity extends Model<Entity>> | |
(this: ObjectType<Entity>, records: QueryDeepPartialEntity<Entity>[]): Promise<void> | |
{ | |
await (this as typeof BaseEntity) | |
.createQueryBuilder() | |
.insert() | |
.values(records) | |
.updateEntity(false) | |
.execute(); | |
} | |
/* ----------------------------------------------------------- | |
RELATIONSHIP ACCESSORS | |
----------------------------------------------------------- */ | |
/** | |
* 현재의 테이블이 특정 테이블에 외래키로 종속되는 관계를 표현 | |
* | |
* @param target 대상 ORM 클래스 | |
* @param field 참조 필드 | |
* @return 종속 테이블의 레코드 객체 | |
*/ | |
protected async belongsTo<Target extends BaseEntity, Field extends NullableNumberFields<Entity>> | |
( | |
target: ObjectType<Target>, | |
field: Field | |
): Promise<number|null extends Entity[Field] ? Target|null : Target> | |
{ | |
let id: number | null = (this as any)[field]; | |
let ret: BaseEntity | null = null; | |
if (id !== null) | |
ret = await this._Memoized_get(this.belongs_to_, | |
async (eloquent, options) => _Nullify(await eloquent.getRepository().findOne(options)), | |
target, | |
"id", | |
id | |
); | |
return <any>ret; | |
} | |
/** | |
* 1: 1 소유 관계를 표현 | |
* | |
* @param target 현 테이블에 종속되는 대상 테이블 | |
* @param field 현 테이블에 종속되는 테이블의 외래키 | |
* @param ensure 종속 레코드가 반드시 존재하는가, false 일 경우 리턴값이 nullable 이 됨. | |
* | |
* @return 소유 객체 | |
*/ | |
protected async hasOne<Target extends BaseEntity, Ensure extends true|false> | |
( | |
target: ObjectType<Target>, | |
field: NumberFields<Target>, | |
ensure: Ensure = false as Ensure | |
): Promise<EnsureType<Target, Ensure>> | |
{ | |
let ret: BaseEntity | null = await this._Memoized_get(this.has_one_, | |
async (eloquent, options) => _Nullify(await eloquent.getRepository().findOne(options)), | |
target, | |
field as string, | |
this.id | |
); | |
return this._Ensure("getOne", ret, target, <string>field, this.id, ensure) as EnsureType<Target, Ensure>; | |
} | |
/** | |
* 1: N 소유 관계를 표현 | |
* | |
* @param target 현 테이블에 종속되는 대상 테이블 | |
* @param field 현 테이블에 종속되는 테이블의 외래키 | |
* | |
* @return 소유 객체 리스트 | |
*/ | |
protected hasMany<Target extends BaseEntity> | |
(target: ObjectType<Target>, field: NullableNumberFields<Target>): Promise<Target[]> | |
{ | |
return this._Memoized_get | |
( | |
this.has_many_, | |
(eloquent, options) => eloquent.getRepository().find(options), | |
target, | |
field as string, | |
this.id | |
) as Promise<Target[]>; | |
} | |
/** | |
* 1: M: N 소유 관계를 표현 | |
* | |
* @param target 목적하는 최종 테이블, 관계상 N 에 해당함 | |
* @param route 경유 관계에 있는 테이블, 관계상 M 에 해당함 | |
* @param targetField N 테이블의 M 에 대한 외래키 | |
* @param myField M 테이블의 현 테이블에 대한 외래키 | |
* | |
* @return 소유 관계에 있는 N 테이블의 레코드 리스트 | |
*/ | |
protected async hasManyToMany<Target extends Model<Target>, Route extends BaseEntity> | |
( | |
target: ObjectType<Target>, | |
route: ObjectType<Route>, | |
targetField: NumberFields<Route>, | |
myField: NumberFields<Route>, | |
): Promise<Target[]> | |
{ | |
let key: Pair<Tuple, Tuple> = new Pair(new Pair(target, targetField) as Tuple, new Pair(route, myField) as Tuple); | |
let it = this.has_many_to_many_.find(key); | |
if (it.equals(this.has_many_to_many_.end()) === false) | |
return await it.second.get() as Target[]; | |
let targetEloquent: typeof BaseEntity = target as typeof BaseEntity; | |
let routeEloquent: typeof BaseEntity = route as typeof BaseEntity; | |
let ret: Singleton<Target[]> = new Singleton(async () => | |
{ | |
return await targetEloquent.createQueryBuilder("T") | |
.innerJoin(routeEloquent, "R", `T.id = R.${targetField}`) | |
.andWhere(`R.${myField} = :id`, { id: this.id }) | |
.getMany() as Target[]; | |
}); | |
this.has_many_to_many_.emplace(key, ret); | |
return await ret.get(); | |
} | |
/** | |
* M: N 관계를 표현 | |
* | |
* @param target 목적 테이블 | |
* @param route 경유 테이블 | |
* @param targetField 경유 테이블의 목적 테이블에 대한 외래키 | |
* @param routeField 경유 테이블의 현 테이블에 대한 외래키 | |
* @return M: N 레코드 리스트 | |
*/ | |
protected async hasManyThrough<Target extends Model<Target>, Route extends BaseEntity> | |
( | |
target: ObjectType<Target>, | |
route: ObjectType<Route>, | |
targetField: NumberFields<Target>, | |
routeField: NumberFields<Route> | |
): Promise<Target[]> | |
{ | |
let key: Pair<Tuple, Tuple> = new Pair(new Pair(target, targetField) as Tuple, new Pair(route, routeField) as Tuple); | |
let it = this.has_many_through_.find(key); | |
if (it.equals(this.has_many_through_.end()) === false) | |
return await it.second.get() as Target[]; | |
let targetEloquent: typeof BaseEntity = target as typeof BaseEntity; | |
let routeEloquent: typeof BaseEntity = route as typeof BaseEntity; | |
let ret: Singleton<Target[]> = new Singleton(async () => | |
{ | |
return await targetEloquent.createQueryBuilder("T") | |
.innerJoin(routeEloquent, "R", `T.${targetField} = R.id`) | |
.andWhere(`R.${routeField} = :id`, { id: this.id }) | |
.getMany() as Target[]; | |
}); | |
this.has_many_through_.emplace(key, ret); | |
return await ret.get(); | |
} | |
/* ----------------------------------------------------------- | |
HIDDEN FEATURES | |
----------------------------------------------------------- */ | |
private belongs_to_: HashMap<Tuple, Singleton<BaseEntity>> = new HashMap(); | |
private has_one_: HashMap<Tuple, Singleton<BaseEntity | null>> = new HashMap(); | |
private has_many_: HashMap<Tuple, Singleton<BaseEntity[]>> = new HashMap(); | |
private has_many_to_many_: HashMap<Pair<Tuple, Tuple>, Singleton<BaseEntity[]>> = new HashMap(); | |
private has_many_through_: HashMap<Pair<Tuple, Tuple>, Singleton<BaseEntity[]>> = new HashMap(); | |
private async _Memoized_get<Ret> | |
( | |
dict: HashMap<Tuple, Singleton<Ret>>, | |
closure: (eloquent: typeof BaseEntity, condition: WhereCondition) => Promise<Ret>, | |
target: ObjectType<BaseEntity>, | |
field: string, | |
value: number | |
): Promise<Ret> | |
{ | |
let key: Tuple = new Pair(target, field); | |
let it: HashMap.Iterator<Tuple, Singleton<Ret>> = dict.find(key); | |
if (it.equals(dict.end()) === false) | |
return await it.second.get(); | |
let eloquent: typeof BaseEntity = target as typeof BaseEntity; | |
let ret: Singleton<Ret> = new Singleton(async () => | |
{ | |
return await closure(eloquent, { | |
where: { | |
[field]: value | |
} | |
}); | |
}); | |
dict.emplace(key, ret); | |
return ret.get(); | |
} | |
private _Ensure<T extends BaseEntity, Ensure extends true|false> | |
(method: string, ret: T | null, target: ObjectType<BaseEntity>, field: string, id: number, ensure: Ensure): EnsureType<T, Ensure> | |
{ | |
if (ret === null) | |
if (ensure === true) | |
throw new DomainError(`Error on ${this.constructor.name}.${method}(): unable to find the matched record from "${target.name}" where ${field} = ${id}.`); | |
else | |
return null as EnsureType<T, Ensure>; | |
return ret as T as EnsureType<T, Ensure>; | |
} | |
} | |
export namespace Model | |
{ | |
export function getTable(model: ObjectType<BaseEntity>): string | |
{ | |
return (model as typeof BaseEntity).getRepository().metadata.tableName; | |
} | |
} | |
interface WhereCondition | |
{ | |
where: { | |
[field: string]: number; | |
} | |
} | |
type NumberFields<T extends object> = SpecialFields<T, number>; | |
type NullableNumberFields<T extends object> = SpecialFields<T, number | null>; | |
type EnsureType<T, Ensure extends true|false> = Ensure extends true ? T : T | null; | |
type Tuple = Pair<ObjectType<BaseEntity>, string>; | |
function _Nullify<T>(obj: T | undefined): T | null | |
{ | |
return (obj === undefined) ? null : obj; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment