Skip to content

Instantly share code, notes, and snippets.

@samchon
Last active June 30, 2020 04:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samchon/7c88fe51c96b657766f0027b2fa8c9a7 to your computer and use it in GitHub Desktop.
Save samchon/7c88fe51c96b657766f0027b2fa8c9a7 to your computer and use it in GitHub Desktop.
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