Typeorm Virtual Column 설정 (1)

2022.01.15
7분
댓글

typeorm.png




Typeorm을 사용하며 대부분 마음에 들었는데 한가지 딱 아쉬운 부분이 있다...

Mysql에서는 테이블에 특정 컬럼이 없어도 SELECT시에 새로 만들어 출력 할 수 있다.

현재 Typeorm에 가상 컬럼 기능이 있긴 한데 원하는 목적과 좀 달라서 원하는 데이터를 가공 후 새로운 컬럼을 추가 해 return 받을 수 있게 가상 컬럼에 관해 포스팅 하려 한다.

Typeorm 버전 0.2.37에 가상컬럼 업데이트 될거라는 얘기가 있었지만 현재 0.2.41 버전까지 지원이 안되고 있다....


🎓 가상 컬럼이란?


Mysql 5.7 부터 지원되는 가상 컬럼은 가상의 컬럼을 둬서 수식과 조건문을 사용해 데이터의 가공 결과를 저장하는 것을 말한다. 사용 방법은 PERSISTENT(stored)와 VIRTUAL(generated-only)이라는 두 가지 타입이 존재하고 디폴트는 PERSISTENT 이다

PERSISTENT virtual columns은 실제 데이터가 데이터베이스에 저장되는 특성을 갖게 되며, VIRTUAL virtual columns은 실제 데이터가 데이터베이스에 저장되지 않고 그때 그때 계산돼 보여주는 역할을 한다.

이 포스팅에서 사용하는 타입은 VIRTUAL 이다.


🤔 데이터 조회


예를들어 데이터베이스 각 행에 버스 정류소의 위도, 경도 값이 저장이 돼 있다.

현재 위치 : 37.57418192 127.015664
(롯데캐슬 천지인 정류소)
목표 정류소1 : 37.57198805 127.0117451
(동대문역 2번출구 정류소)
목표 정류소2 : 37.57578617
126.998124 (원남동 사거리 정류소)
목표 정류소3 : 37.58630021 126.9853527
(감사원 정류소)

여기서 현재 위치 좌표 기준 직선거리 2km 이내의 데이터만 조회하려 하며

각 데이터마다 거리가 얼마나 차이 나는지 알 수 있게 distance라는 key를 가진 컬럼을 추가해 조회하려 한다고 가정해 보자.


@entities/stations.ts

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity("stations")
export class Stations {
    @PrimaryGeneratedColumn()
    id: number;

    @Column("varchar")
    station: string;

    @Column("double")
    latitude: number;

    @Column("double")
    longitude: number;
}

Data Mapper 형식으로 Entity를 생성한다.


우선 위도, 경도 값을 double형식으로 갖는 Entity를 설정 해 준다.

그 뒤 getMany()함수로 호출 할 때, getRawMany()함수로 호출 할 때를 나눠 예를 들어 보겠다.

float형식으로 정할 시 좌표가 소숫점 5자리부터 잘리는 현상이 발생하니 double로 설정해주자


getMany() 사용 시

import { getRepository } from "typeorm";
import { Stations } from "@entities/stations";

/**
    lat: 37.57418192,
    lon: 127.015664,
    radius(기준 km): 2
*/
const findAroundLocation = async (lat: number, lon: number, radius: number) => {
    return await getRepository(Stations)
        .createQueryBuilder("stations")
        .addSelect(
                `6371 * acos(cos(radians(${lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians(${lon})) + sin(radians(${lat})) * sin(radians(latitude)))`,
                "distance"
            )
        .having(`distance <= ${radius}`)
        .orderBy("distance", "ASC")
        .getMany();
}

console.log(findAroundLocation());

Return

[
    Stations {
        id: 1,
        station: "동대문역 2번출구 정류소",
        latitude: 37.57198805,
        longitude: 127.0117451,
    },
    Stations {
        id: 2,
        station: "원남동 사거리 정류소",
        latitude: 37.57578617,
        longitude: 126.998124,
    }
]

현재 좌표와 목표 정류소3의 좌표간 거리가 2km가 넘기 때문에 조회되지 않는다.

실 거리가 약 3km (2.991km) 이기 때문이다.


분명 addSelect()함수에 두 좌표간 거리 구하는 공식과 컬럼명을 지정해 줬는데 결과에는 distance가 나오지 않는다.

왜냐하면 DB에서 단일한 결과를 가져오는 getMany()함수의 특성상 쿼리문에서 적용한 데이터는 출력이 안되고 계산에만 사용이 된다.


getRawMany() 사용 시

import { getRepository } from "typeorm";
import { Stations } from "@entities/stations";

/**
    lat: 37.57418192,
    lon: 127.015664,
    radius(기준 km): 2
*/
const aroundStation = async (lat: number, lon: number, radius: number) => {
    return await getRepository(Stations)
        .createQueryBuilder("stations")
        .select()
        .addSelect(
                `6371 * acos(cos(radians(${lat})) * cos(radians(latitude)) * cos(radians(longitude) - radians(${lon})) + sin(radians(${lat})) * sin(radians(latitude)))`,
                "distance"
            )
        .having(`distance <= ${radius}`)
        .orderBy("distance", "ASC")
        .getRawMany();
}

console.log(aroundStation());

Return

[
    Stations {
        stations_id: 1,
        stations_station: "동대문역 2번출구 정류소",
        stations_latitude: 37.57198805,
        stations_longitude: 127.0117451,
        distance: 0.4228400907307967
    },
    Stations {
        stations_id: 2,
        stations_station: "원남동 사거리 정류소",
        stations_latitude: 37.57578617,
        stations_longitude: 126.998124,
        distance: 1.5482886513847316
    }
]

distance는 킬로미터 기준으로 계산한 결과이다.


getRawMany()함수로 조회 시 원시 결과를 얻어오기 때문에 addSelect()함수에서 지정한 distance를 갖고올 수 있다.

이 경우 Mysql 가상 컬럼과 매우 비슷한 결과를 도출할 수 있게 된다.

하지만 객체 key에 stations_ 가 붙어 나오게 된다.

이 경우는 addSelect()함수 호출 이전에 select()함수를 호출해 alias설정을 해주면 손쉽게 해결 가능하다.



이상 Mysql의 가상 컬럼을 비슷하게 구현할 수 있는 Typeorm의 기능을 포스팅 했다.

다음 포스팅에서 왜 Custom Virtual Column을 사용해야 하는지 이유와 함께 포스팅 해야겠다.


📚 참고 자료

Typeorm 공식 레퍼런스



Node.js
Express
TypeScript
Typeorm

프로필 사진
Seongsu Kim
Backend Developer