1년 반의 개발 끝에 안정화된 MikroORM v7을 소개합니다. 핵심 패키지 런타임 의존성 0, knex에서 Kysely로 전환, 네이티브 ESM, Node.js 직접 의존성 제거, 타입 안전 QueryBuilder 등 주요 변경 사항과 새로운 기능을 정리합니다.
1년 반의 활발한 개발 끝에, 마침내 MikroORM v7이 안정화되었음을 기쁜 마음으로 발표합니다. 이번은 지금까지 중 가장 큰 릴리스이며, 부제가 모든 것을 말해줍니다 — Unchained. 우리는 knex로부터 벗어났고, 코어 의존성을 0으로 줄였으며, 네이티브 ESM을 제공하고, Node.js에 대한 강한 결합을 제거했으며, 그 위에 다양한 새 기능을 추가했습니다. 이제 자세히 살펴봅시다!

v7의 온갖 멋진 것들로 가기 전에, 6.x 기능 릴리스에서 추가된 중요한 내용 몇 가지를 언급해 보겠습니다:
defineEntity 헬퍼@Transactional 데코레이터하지만 역사 수업은 이쯤 하고, 미래에 대해 이야기해 봅시다!

v7의 대표 기능 중 하나는 @mikro-orm/core 패키지가 이제 런타임 의존성이 0개라는 점입니다. 맞습니다 — 하나도 없습니다. 우리는 dotenv, esprima, reflect-metadata, dataloader, globby를 비롯한 모든 것을 제거했습니다. 이제 코어 패키지는 완전히 독립적으로 서 있습니다.
이는 번들 크기, 콜드 스타트 시간(서버리스 사용자에게는 특히 희소식!), 전반적인 유지보수성에 큰 영향을 줍니다. 이들 중 일부는 선택적 peer dependency로는 여전히 사용할 수 있습니다 — 예를 들어 dataloader 통합을 원하면 dataloader를 설치하고, reflect 기반 메타데이터 프로바이더를 선호한다면 reflect-metadata를 설치하면 됩니다.
Dotenv 지원은 완전히 제거되었습니다. 자동
.env로딩에 의존하고 있었다면, ORM을 초기화하기 전에 직접dotenv.config()를 호출해야 합니다.

내부 쿼리 빌딩이 완전히 다시 작성되었습니다. MikroORM은 더 이상 SQL 쿼리를 생성하거나 실행하기 위해 knex에 의존하지 않습니다. 대신 Kysely가 쿼리 러너로 사용되며, 실제 쿼리 빌딩은 전적으로 MikroORM이 직접 수행합니다 — 이를 통해 생성되는 SQL에 대한 완전한 제어권을 확보했습니다.
@mikro-orm/knex 패키지는 @mikro-orm/sql로 이름이 변경되었고, 이제 공유 SQL 드라이버 로직은 모두 여기에서 관리됩니다. 원시 쿼리를 위해 기본 Kysely 인스턴스에 접근하는 것도 여전히 가능합니다:
const kysely = orm.em.getKysely();const res = await kysely.selectFrom('author').selectAll().execute();
defineEntity를 사용하면, em.getKysely()가 반환하는 Kysely 인스턴스는 엔티티 메타데이터를 기반으로 자동으로 타입이 지정됩니다 — Kysely 인터페이스를 수동으로 정의하지 않아도 테이블/컬럼 이름에 대한 완전한 자동완성과 타입 체크를 얻을 수 있습니다. 데코레이터를 선호한다면 클래스에 EntityName 심볼을 선언함으로써 동일한 자동 타이핑에 옵트인할 수 있습니다(데코레이터만으로는 defineEntity가 자동으로 제공하는 타입 매핑이 생성되지 않기 때문에 필요합니다):
import { EntityName } from '@mikro-orm/core';@Entity()class Author { [EntityName]?: 'Author'; @PrimaryKey() id!: number; @Property() firstName!: string;}
내장 MikroKyselyPlugin은 onCreate/onUpdate 훅을 처리하고, MikroORM 타입 시스템을 통해 값을 변환하며, 데이터베이스 측 이름 대신 엔티티 이름으로 테이블을, 프로퍼티 이름으로 컬럼을 참조할 수 있게 해줍니다. 이를 가장 잘 활용하는 방법은 tableNamingStrategy와 columnNamingStrategy를 엔티티/프로퍼티 이름을 사용하도록 설정하는 것입니다 — 플러그인이 런타임에 실제 테이블 및 컬럼 이름으로의 변환을 처리합니다:
const kysely = em.getKysely({ tableNamingStrategy: 'entity', columnNamingStrategy: 'property',});// write queries using entity and property names — fully typed!const rows = await kysely .selectFrom('Author') .select('firstName') .execute();
자세한 내용은 Kysely integration guide를 참고하세요.
기존 코드에서 knex 쿼리 빌더나 knex.raw() 표현식을 사용하고 있다면 @mikro-orm/knex-compat을 설치하세요 — ORM raw 프래그먼트로 변환할 수 있는 raw 헬퍼를 제공합니다:
import { raw } from '@mikro-orm/knex-compat';import knex from 'knex';const knexQb = knex({ client: 'pg' }).select('*').from('author');const authors = await em.find(Author, { [raw(knexQb)]: [] });
MikroORM v7은 이제 네이티브 ESM 패키지입니다. 이는 수년간 추진해온 거대한 진전입니다. Node.js에 도입된 require(esm) 지원(그리고 Node 20 LTS에도 백포트됨) 덕분에, CJS 프로젝트도 문제 없이 MikroORM을 사용할 수 있어야 합니다 — 프로젝트를 ESM으로 전환할 필요가 없습니다.
mikro-orm-esm CLI 스크립트는 사라졌습니다. 이제 어디서나 동작하는 단 하나의 mikro-orm CLI만 존재합니다. 여기에 새로운 TypeScript 로더 지원(아래에서 더 설명)까지 결합되면서, CLI 설정이 이전보다 훨씬 덜 고통스러워질 것입니다.
이제 Node.js 22.17+ 및 TypeScript 5.8+가 필요합니다.

코어 런타임(코어 + 드라이버)은 더 이상 node:path, node:fs 같은 Node.js 내장 모듈에 의존하지 않습니다. 덕분에 번들링이 더 쉬워졌고, Deno 및 기타 런타임 지원 가능성을 위한 기준선도 마련되었습니다.
AsyncLocalStorage 같은 일부 기능은 프로젝트가 빌드될 수 있도록 더미 폴백을 갖고 있지만, 적절한 구현이 없으면 올바르게 동작하지 않습니다(예: 동시 flush는 이를 필요로 합니다). 다음 패키지들은 여전히 Node.js 런타임에 의존합니다: @mikro-orm/cli, @mikro-orm/entity-generator, @mikro-orm/reflection.
이제 모든 MikroORM 패키지가 npm과 함께 JSR에도 게시됩니다. JSR은 TypeScript 소스를 직접 배포하므로, Deno 사용자와 JSR 네이티브 툴링은 빌드 단계 없이도 1급 타입 정보를 얻을 수 있습니다. 코어의 Node.js 의존성 0과 네이티브 ESM까지 결합되어, MikroORM v7은 첫날부터 Deno 생태계의 1급 시민입니다.

많은 분들이 요청해온 기능입니다. 이제 QueryBuilder는 제네릭 파라미터를 통해 조인된 alias를 추적하여, b.title이나 a.name 같은 alias 프로퍼티에 대해 완전한 자동완성과 타입 체크를 제공합니다.
const qb = em.createQueryBuilder(Author, 'a') .leftJoin('a.books', 'b') // 'b' now typed as Book .leftJoin('b.publisher', 'p') // 'p' now typed as Publisher .leftJoin('b.tags', 't') // 't' now typed as Tag .select(['a.name', 'b.title', 'p.name', 't.label']) .where({ 'b.title': { $like: '%orm%' } }) .orderBy({ 'p.name': 'asc', 't.label': 'desc' });
alias와 점을 입력하면 IDE가 사용 가능한 모든 프로퍼티를 제안합니다:
qb.where({ 'b.|' }) // suggests: title, price, author, publisher, tags, ...qb.orderBy({ 'p.|' }) // suggests: name, address, books, ...qb.select(['t.|']) // suggests: label, books, ...
그리고 잘못된 프로퍼티나 알 수 없는 alias는 컴파일 타임에 실패합니다:
qb.where({ 'b.invalid': 1 }); // TS error: 'invalid' doesn't exist on Bookqb.orderBy({ 'x.name': 'asc' }); // TS error: 'x' is not a known aliasqb.leftJoin('a.invalid', 'x'); // TS error: 'invalid' is not a relation on Author
QueryBuilder는 select()에서 필드 alias 지정도 지원합니다 — @Formula 프로퍼티까지 포함하며 — as 문법 또는 sql.ref().as()를 통해 가능합니다:
// string 'as' syntaxconst qb = em.createQueryBuilder(Book, 'b') .select(['b.title', 'b.priceTaxed as tax']);// or via sql.ref().as()const qb = em.createQueryBuilder(Book, 'b') .select(['b.title', sql.ref('b.priceTaxed').as('tax')]);
alias는 타입 시스템에서 추적되므로, having()과 orderBy()에서도 타입 체크가 적용됩니다:
const qb = em.createQueryBuilder(FooBar, 'fb') .select(['fb.id', 'random as rnd']) .groupBy('fb.id') .having({ rnd: { $gt: 0 } }) // 'rnd' is type-checked! .orderBy({ rnd: 'desc' });
QueryBuilder에서도 fields 파라미터가 이제 타입 안전해졌습니다. 선택된 필드는 제네릭 파라미터를 통해 추적되므로, getResult()와 getResultList()는 선택된 프로퍼티만 가진 타입으로 좁혀진 엔티티를 반환하고, execute()는 올바르게 타입 지정된 DTO를 반환합니다:
const qb = em.createQueryBuilder(Author, 'a') .select(['a.id', 'a.email']);// getResultList() returns Loaded<Author, never, 'id' | 'email'>[]const entities = await qb.getResultList();// execute() returns Pick<EntityDTO<Author>, 'id' | 'email'>[]const rows = await qb.execute();
이는 조인된 관계에도 확장됩니다 — leftJoinAndSelect로 선택된 필드는 alias별로 추적되므로, 전체 엔티티 그래프가 올바르게 좁혀집니다:
const qb = em.createQueryBuilder(Author, 'a') .select('a.id') .leftJoinAndSelect('a.books', 'b', {}, ['title']) .leftJoinAndSelect('b.publisher', 'p', {}, ['name']);const rows = await qb.execute();// typed as EntityDTO<Loaded<Author, 'books' | 'books.publisher', 'id' | 'books.title' | 'books.publisher.name'>>[]
QueryBuilder는 이제 with() 및 withRecursive() 메서드를 통해 CTE(Common Table Expression)를 지원합니다. CTE는 모든 SQL 드라이버에서 동작하며, 컬럼 리스트 및 PostgreSQL에서의 MATERIALIZED / NOT MATERIALIZED 힌트도 지원합니다.
const sub = em.createQueryBuilder(Author, 'a') .select(['a.id', 'a.name']) .where({ age: { $gte: 40 } });const rows = await em.createQueryBuilder(Author) .with('older_authors', sub) .select('*') .from('older_authors', 'oa') .execute();
여러 CTE를 체이닝할 수 있으며, 재귀 CTE도 지원합니다:
import { raw } from '@mikro-orm/core';const qb = em.createQueryBuilder(Author) .withRecursive('seq', raw('select 1 as n union all select n + 1 from seq where n < ?', [5])) .select('*') .from('seq', 's');const rows = await qb.execute<{ n: number }[]>();// [{ n: 1 }, { n: 2 }, { n: 3 }, { n: 4 }, { n: 5 }]
타입이 지정된 QueryBuilder로부터 CTE를 구성하면, 결과 CTE 테이블은 엔티티 타입을 상속합니다 — 따라서 바깥 쿼리의 select, where 및 기타 메서드는 CTE의 형태에 대해 완전하게 타입 체크됩니다.
새로운 unionWhere 옵션은 복잡한 $or 쿼리에 대한 인덱스 친화적인 대안을 제공합니다. 쿼리 플래너를 무력화할 수 있는 OR 조건 하나로 쿼리를 구성하는 대신, unionWhere는 각 분기마다 별도의 서브쿼리를 만들고 UNION ALL로 결합합니다:
const results = await em.find(Employee, {}, { unionWhere: [ { department: 'engineering' }, { salary: { $gt: 100_000 } }, ],});
이는 서로 다른 분기가 서로 다른 관계를 건드릴 때 특히 유용합니다 — PostgreSQL의 플래너는 조인에 걸친 $or에서 테이블별 인덱스를 종종 사용하지 못하지만, 각 UNION 분기는 독립적으로 플래닝되며 자체 인덱스를 사용할 수 있습니다.
QueryBuilder는 더 많은 제어를 위해 union() 및 unionAll() 메서드를 노출합니다:
const qb1 = em.createQueryBuilder(Employee).select('id').where({ department: 'engineering' });const qb2 = em.createQueryBuilder(Employee).select('id').where({ salary: { $gt: 100_000 } });const results = await em.createQueryBuilder(Employee) .select('*') .where({ id: { $in: qb1.unionAll(qb2) } }) .getResultList();
기본적으로 unionWhere는 UNION ALL을 사용합니다(중복 유지, 정렬 오버헤드 없음). 분기 간 중복이 많다면 unionWhereStrategy: 'union'을 사용해 중복 제거를 하세요.
v6에서는 raw()로 생성된 raw 쿼리 프래그먼트가 생성된 문자열 키로 캐시되었고 각 쿼리 실행 후 캐시가 비워졌습니다. 즉, 프래그먼트를 변수에 저장해 여러 쿼리에서 재사용할 수 없었습니다 — 두 번째 사용 시 캐시 엔트리가 이미 사라져 실패했기 때문입니다.
v7은 이 메커니즘을 심볼과 WeakMap으로 대체하여, raw 프래그먼트가 완전히 재사용 가능해졌고 더 이상 참조되지 않을 때 자동으로 가비지 컬렉션됩니다:
// define once, use many timesconst fullName = raw(concat(first_name, ' ', last_name));const res1 = await em.find(User, {}, { orderBy: { [fullName]: 'asc' } });const res2 = await em.find(User, {}, { orderBy: { [fullName]: 'desc' } });
$size collection operatorv7은 to-many 관계의 크기로 쿼리할 수 있는 $size 연산자를 추가합니다. 1:M과 M:N 컬렉션 모두에서 동작하며, 정확한 일치뿐 아니라 비교 연산자도 지원합니다:
// authors with exactly 3 booksconst authors = await em.find(Author, { books: { $size: 3 },});// authors with at least 2 booksconst prolific = await em.find(Author, { books: { $size: { $gte: 2 } },});// books with between 1 and 3 tagsconst tagged = await em.find(Book, { tags: { $size: { $gt: 0, $lte: 3 } },});
내부적으로 이 연산자는 COUNT(*)를 사용하는 상관 서브쿼리를 생성하므로, 추가 설정 없이도 모든 SQL 드라이버에서 동작합니다.
오랫동안 골칫거리였던 부분 중 하나가 embedded array 요소의 프로퍼티를 쿼리하는 것이었습니다. v6에서는 raw $contains 쿼리로 우회하거나 QueryBuilder로 내려가야 했습니다. v7은 이를 투명하게 만듭니다 — 프로퍼티를 직접 쿼리하기만 하면 MikroORM이 플랫폼별 JSON 배열 반복(PostgreSQL의 jsonb_array_elements, MySQL/MariaDB의 json_table, SQLite의 json_each)을 사용하는 올바른 EXISTS 서브쿼리를 생성합니다:
// find users who have an address in Londonconst users = await em.find(User, { addresses: { city: 'London' },});// multiple conditions match the **same element** (like MongoDB's $elemMatch)const users = await em.find(User, { addresses: { city: 'London', country: 'UK' },});// operators work tooconst users = await em.find(User, { addresses: { number: { $gt: 5 } },});
요소 범위 내에서 논리 연산자 $or, $and, $not도 지원됩니다. $not은 NOT EXISTS 의미론을 사용합니다 — 조건과 일치하는 요소가 하나도 없는 행을 찾습니다. 기존의 배열 수준 연산자 $contains, $contained, $overlap은 변경 없이 계속 동작합니다.
$elemMatch for JSON array propertiesembeddable 메타데이터가 없는 일반 JSON 배열 프로퍼티에 대해, v7은 $elemMatch 연산자를 도입합니다. 동일한 EXISTS 서브쿼리 패턴을 생성하지만, 쿼리 값을 기반으로 타입을 추론합니다 — 숫자/불리언/문자열은 플랫폼별로 자동 캐스팅됩니다:
@Entity()class Event { @Property({ type: 'json', nullable: true }) tags?: { name: string; priority: number }[];}// find events with a high-priority "typescript" tagconst events = await em.find(Event, { tags: { $elemMatch: { name: 'typescript', priority: { $gte: 8 } } },});
MongoDB에서는 $elemMatch가 네이티브로 그대로 전달됩니다. SQL 드라이버에서는 $and와 잘 어울려 요소 수준과 배열 수준 조건을 결합할 수 있습니다:
const events = await em.find(Event, { $and: [ { tags: { $elemMatch: { priority: { $gt: 5 } } } }, { tags: { $contains: [{ name: 'typescript' }] } }, ],});
v7은 em.find(), em.count(), 그리고 QueryBuilder 에 collation 옵션을 추가합니다. SQL 드라이버에서는 collation 이름 문자열을 전달하면, ORDER BY 절의 모든 컬럼에 COLLATE로 적용됩니다:
const users = await em.find(User, {}, { collation: 'utf8mb4_general_ci', orderBy: { name: 'asc' },});// produces: ... ORDER BY nameCOLLATEutf8mb4_general_ci ASC
MongoDB에서는 네이티브 CollationOptions 객체를 전달하면, 필터링과 정렬을 포함한 전체 쿼리에 적용됩니다:
const users = await em.find(User, { name: 'john' }, { collation: { locale: 'en', strength: 2 }, orderBy: { name: QueryOrder.ASC },});
MongoDB에는 새로운 쿼리 옵션도 추가되었습니다: indexHint(문자열 또는 객체), maxTimeMS, allowDiskUse — 모두 FindOptions에서 바로 사용할 수 있습니다.
Streaming은 항상 knex로 raw를 써야 했지만, v7에서 마침내 1급 지원이 추가되었습니다. 이제 em.stream()과 qb.stream()을 사용해 모든 데이터를 메모리에 올리지 않고도 대용량 데이터셋을 처리할 수 있습니다.
두 가지 모드가 지원됩니다 — 행 단위 스트리밍, 그리고 to-many 관계로 인해 발생하는 데카르트 곱을 처리하기 위한 루트 엔티티 그래프의 배치 단위 yield:
// stream row by rowfor await (const author of em.stream(Author, { where: { age: { $gt: 18 } } })) { console.log(author.name);}// or via QueryBuilderconst qb = em.createQueryBuilder(Author).where({ age: { $gt: 18 } });for await (const author of qb.stream()) { console.log(author.name);}
스트리밍은 identity map을 조작하지 않습니다. identity는 단일 루트 엔티티 레코드의 컨텍스트 내에서만 보장됩니다(즉, 해당 레코드의 관계는 유일한 엔티티 인스턴스를 포함하지만, 서로 다른 스트리밍 레코드에 나타난 동일 엔티티는 서로 다른 객체가 됩니다).

v6.5에서 도입된 balanced 로딩 전략이 이제 기본값입니다. 이는 to-one 관계(M:1, 1:1)는 조인하지만, to-many 관계(1:M, M:N)는 별도의 쿼리를 사용합니다. 이렇게 하면 to-one 관계에서는 쿼리 수를 줄이면서도, to-many 컬렉션을 조인할 때 발생하는 데카르트 곱 폭발을 피할 수 있어 두 세계의 장점을 모두 얻습니다.
const author = await em.findOne(Author, 1, { populate: ['books.tags', 'favouriteBook'],});// issues two queries:// 1. select author + favourite book (joined)// 2. select books + tags (separate query, joined together)
이전 기본값(joined)을 선호한다면 ORM 설정에서 loadStrategy: 'joined'를 지정하면 됩니다.
전역 strategy 옵션은 populate된 모든 관계에 적용되지만, 새 populateHints 옵션을 사용하면 개별 관계에 대해 로딩 전략이나 조인 타입을 오버라이드할 수 있습니다. 키는 populate에서 사용하는 점으로 구분된 경로와 동일하며, 자동완성도 지원됩니다:
const author = await em.findOne(Author, 1, { populate: ['books.inspiredBy', 'favouriteBook'], strategy: 'joined', populateHints: { books: { joinType: 'inner join' }, 'books.inspiredBy': { strategy: 'select-in' }, favouriteBook: { joinType: 'left join' }, },});
이렇게 하면 books는 inner join으로 로드하고, inspiredBy는 별도의 select-in 쿼리를 사용하며, favouriteBook은 left join을 강제합니다 — 모두 한 번의 쿼리 호출로 처리됩니다.
defineEntity with custom classes
v7에서 가장 멋진 추가 사항 중 하나는, 어떤 프로퍼티 정의도 중복하지 않고 자동 생성된 defineEntity 클래스를 확장할 수 있는 기능입니다. defineEntity를 사용하면 내부 클래스가 자동으로 생성됩니다. 이제 Schema.class를 통해 이를 확장하고, Schema.setClass()로 커스텀 클래스를 다시 등록할 수 있습니다:
const UserSchema = defineEntity({ name: 'User', properties: { id: p.integer().primary(), firstName: p.string(), lastName: p.string(), },});// extend the auto-generated class to add domain methodsclass User extends UserSchema.class { fullName() { return ${this.firstName} ${this.lastName}; }}// register the custom class — must happen before MikroORM.init()UserSchema.setClass(User);
setClass()를 호출하면 모든 엔티티 인스턴스는 커스텀 User 클래스의 인스턴스가 됩니다. 즉, 스키마에서 한 번만 프로퍼티를 정의하면서(완전한 타입 추론 포함) 클래스에는 도메인 메서드를 추가하는, 두 세계의 장점을 모두 얻을 수 있습니다. 중복도 없고, 수동 인터페이스도 필요 없습니다.
const user = em.create(User, { firstName: 'John', lastName: 'Doe' });console.log(user.fullName()); // "John Doe"console.log(user instanceof User); // true
이제 엔티티 자체에 기본 orderBy를 정의할 수 있습니다. 이 정렬은 엔티티를 직접 쿼리할 때뿐만 아니라 관계로 populate될 때도 자동으로 적용됩니다:
const Comment = defineEntity({ name: 'Comment', orderBy: { createdAt: QueryOrder.DESC, id: QueryOrder.DESC }, properties: { id: p.number().primary(), createdAt: p.datetime(), text: p.string(), post: () => p.manyToOne(Post), },});
적용 가능한 모든 정렬은 명확한 우선순위로 결합됩니다: 런타임 FindOptions.orderBy가 관계 수준 orderBy(@OneToMany, @ManyToMany)보다 우선이며, 이는 엔티티 수준 orderBy보다 우선합니다.
모든 데코레이터는 전용 @mikro-orm/decorators 패키지로 이동했으며, 이제 두 가지 형태로 제공됩니다 — 레거시(TypeScript experimental)와 ES spec:
// legacy decorators (requires experimentalDecorators: true)import { Entity, PrimaryKey, Property } from '@mikro-orm/decorators/legacy';// ES spec decorators (the future!)import { Entity, PrimaryKey, Property } from '@mikro-orm/decorators/es';
즉, 프로젝트에 맞는 데코레이터 스타일을 선택할 수 있습니다. NestJS 또는 experimentalDecorators: true가 필요한 다른 프레임워크를 사용하고 있다면 레거시 import는 그대로 잘 동작합니다. ES 데코레이터는 메타데이터 리플렉션을 지원하지 않으므로, 스칼라 프로퍼티 타입을 옵션에서 명시적으로 제공해야 합니다.
쿼리 시점에 표현식을 평가하는 virtual entities와 달리, view entities는 스키마 제너레이터가 관리하는 실제 데이터베이스 뷰를 생성합니다. 뷰는 PK를 가질 수 있고, FK 없이도 관계 타깃으로 사용할 수 있으며, PostgreSQL에서는 materialized views도 지원됩니다(이는 저장되며 수동으로 refresh해야 합니다).
const AuthorStats = defineEntity({ name: 'AuthorStats', expression: select a.id, a.name, count(b.id) as book_count from author a left join book b on b.author_id = a.id group by a.id, a.name , view: true, properties: { id: p.integer().primary(), name: p.string(), bookCount: p.integer(), },});
PostgreSQL에서의 materialized view:
const AuthorStats = defineEntity({ name: 'AuthorStats', expression: ..., view: { materialized: true }, properties: { id: p.integer().primary(), name: p.string(), bookCount: p.integer(), },});
// refresh the materialized viewawait em.refreshMaterializedView(AuthorStats);

Polymorphic relations은 수년간 가장 많이 요청된 기능 중 하나였고(#706), v7에서 마침내 제공됩니다. 폴리모픽 관계는 하나의 프로퍼티가 여러 서로 다른 타입의 엔티티를 참조할 수 있게 해주며 — 각 타입은 자신의 테이블에 존재합니다. 예를 들어 “좋아요(like)”가 “게시물(post)” 또는 “댓글(comment)” 어느 쪽에도 연결될 수 있는 경우를 떠올리면 됩니다.
const Post = defineEntity({ name: 'Post', properties: { id: p.integer().primary(), title: p.string(), likes: () => p.oneToMany(UserLike).mappedBy('likeable'), },});const Comment = defineEntity({ name: 'Comment', properties: { id: p.integer().primary(), text: p.string(), likes: () => p.oneToMany(UserLike).mappedBy('likeable'), },});const UserLike = defineEntity({ name: 'UserLike', properties: { id: p.integer().primary(), // can point to either Post or Comment likeable: () => p.manyToOne([Post, Comment]), },});
내부적으로 MikroORM은 두 개의 컬럼을 생성합니다 — discriminator(likeable_type)와 FK 값(likeable_id)입니다. discriminator는 할당한 엔티티 타입에 따라 자동으로 관리됩니다:
const like = em.create(UserLike, { likeable: somePost, // sets likeable_type = 'post' automatically});
쿼리는 양쪽 모두에서 populate와 함께 자연스럽게 동작합니다:
// querying with populateconst likes = await em.find(UserLike, {}, { populate: ['likeable'] });// each likeable is the correct entity type (Post or Comment)// inverse side populate — only likes pointing to this post are includedconst post = await em.findOne(Post, 1, { populate: ['likes'] });
폴리모픽 M:N 관계도 공유 피벗 테이블을 통해 지원됩니다 — 여러 엔티티 타입이 같은 피벗 테이블을 공유하며, discriminator 컬럼으로 구분합니다. 커스텀 discriminator 값, 복합 PK, Ref 래퍼, targetKey도 폴리모픽 관계에서 모두 동작합니다.
폴리모픽 관계는 FK가 여러 테이블을 가리킬 수 있기 때문에, 데이터베이스 레벨에서는 외래 키 제약 조건을 생성하지 않습니다.

MikroORM v4는 하나의 계층이 한 테이블을 공유하는 Single Table Inheritance(STI)를 도입했습니다. v7은 대안으로 Table-Per-Type (TPT) 상속을 추가합니다. 여기서는 각 엔티티가 전용 테이블을 가지며, 자식 테이블은 자신의 PK에서 부모 테이블 PK로 향하는 외래 키를 갖고 ON DELETE CASCADE가 적용됩니다.
const Animal = defineEntity({ name: 'Animal', abstract: true, inheritance: 'tpt', properties: { id: p.integer().primary(), name: p.string(), },});const Dog = defineEntity({ name: 'Dog', extends: Animal, properties: { breed: p.string(), },});const Cat = defineEntity({ name: 'Cat', extends: Animal, properties: { color: p.string(), },});
이는 정규화된 3개의 테이블을 생성합니다:
create table animal (id integer primary key, name text not null);create table dog (id integer primary key references animal(id) on delete cascade, breed text not null);create table cat (id integer primary key references animal(id) on delete cascade, color text not null);
특정 자식 타입을 쿼리하면 MikroORM은 자동으로 부모 테이블을 INNER JOIN으로 추가합니다. 추상 베이스 클래스를 쿼리하면, 모든 하위 타입을 LEFT JOIN으로 결합하고 계산된 discriminator를 사용해 올바른 구체 타입을 반환합니다:
// querying a specific type — inner joins the parent tableconst dogs = await em.find(Dog, { name: 'Rex' });// querying the base class — left joins all children, returns concrete typesconst animals = await em.find(Animal, {});animals[0] instanceof Dog; // trueanimals[0].breed; // accessible — it's a Dog instance
업데이트는 변경된 프로퍼티가 있는 테이블만 건드리도록 최적화됩니다. 다단계 계층(손자 extends 자식 extends 루트)도 완전히 지원되며, TPT 베이스 클래스에 대한 관계도 지원됩니다.
TPT와 STI는 같은 계층에서 혼용할 수 없습니다.
역사적으로 to-one 관계는 PK만 타깃으로 할 수 있었지만, SQL 레벨에서는 어떤 unique 컬럼이든 타깃이 될 수 있습니다. v7에서는 targetKey 옵션을 통해 이것이 네이티브로 지원됩니다:
const User = defineEntity({ name: 'User', properties: { id: p.integer().primary(), email: p.string().unique(), },});const Token = defineEntity({ name: 'Token', properties: { id: p.integer().primary(), user: () => p.manyToOne(User).targetKey('email'), },});
이런 관계도 identity map에서 특수 키로 추적됩니다 — 이런 경우에도 identity를 계속 보장합니다.
v7은 모든 SQL 데이터베이스 드라이버에서 고급 인덱스 기능을 네이티브로 지원하여, 인덱스 생성에 대한 세밀한 제어를 제공합니다. 이제 엔티티 정의만으로 컬럼 정렬 순서, NULLS 정렬, 커버링 인덱스, fill factor 등 다양한 옵션을 지정할 수 있습니다:
@Entity()@Index({ properties: ['createdAt', 'name'], columns: [ { name: 'created_at', sort: 'DESC', nulls: 'LAST' }, { name: 'name', sort: 'ASC' }, ], include: ['email'], // covering index (PostgreSQL, MSSQL) fillFactor: 70,})export class Article { // ...}
지원되는 기능의 전체 목록에는 컬럼 정렬 순서(ASC/DESC), NULLS FIRST/NULLS LAST 정렬, 컬럼 프리픽스 길이, 컬럼 collation, 커버링 인덱스(INCLUDE), fill factor, invisible/hidden 인덱스, disabled 인덱스, clustered 인덱스가 포함되며, 각 기능은 해당 기능을 지원하는 데이터베이스 드라이버에서만 동작합니다. 엔티티 제너레이터도 기존 데이터베이스로부터 엔티티를 스캐폴딩할 때 이러한 기능을 감지합니다.
SQLite 드라이버는 플러그형 dialect를 지원하도록 리팩터링되어, ORM 코드를 변경하지 않고도 기반 SQLite 구현을 교체할 수 있습니다. 대표적인 추가 사항은 Node.js 22 내장 node:sqlite 모듈 지원입니다 — 즉, 이제 SQLite를 네이티브 의존성 0개로 실행할 수 있습니다:
import { SqliteDriver, NodeSqliteDialect } from '@mikro-orm/sqlite';const orm = await MikroORM.init({ driver: SqliteDriver, driverOptions: new NodeSqliteDialect(':memory:'),});
기본 dialect는 여전히 better-sqlite3를 사용하므로 기존 프로젝트는 변경 없이 계속 동작합니다. 하지만 최소 설치, Docker 이미지, 네이티브 컴파일이 고통스러운 환경을 목표로 한다면 node:sqlite는 훌륭한 대안입니다.
ATTACH DATABASESQLite는 이제 단일 연결에서 여러 데이터베이스 파일을 다루기 위한 ATTACH DATABASE를 지원합니다. 첨부된 데이터베이스의 테이블은 schema 옵션으로 접근하며, 이는 SQLite의 스키마 프리픽스에 매핑됩니다:
@Entity({ schema: 'users_db' })class UserProfile { @PrimaryKey() id!: number; @Property() username!: string;}
const orm = await MikroORM.init({ driver: SqliteDriver, dbName: 'main.db', schema: 'main', attachDatabases: [ { name: 'users_db', path: './users.db' }, { name: 'logs_db', path: './logs.db' }, ],});
이는 MikroORM의 기존 멀티 스키마 인프라에 매핑되므로, 스키마 제너레이터, 마이그레이션, 엔티티 디스커버리 같은 기능이 첨부된 데이터베이스 전반에 걸쳐 동작합니다.

MikroORM은 시작 시점에 엔티티별로 최적화된 hydration 및 비교 함수를 new Function으로 JIT 컴파일합니다. 성능 면에서는 좋지만, Cloudflare Workers 같은 런타임은 동적 코드 평가를 전면 금지합니다.
v7은 이러한 함수들을 사전에 일반 .js 파일로 생성하는 compile CLI 명령을 추가합니다. 런타임에서는 사전 컴파일된 함수를 직접 사용합니다 — eval도 없고 new Function도 없으며, 엣지 런타임과 완전히 호환됩니다:
npx mikro-orm compile --out ./compiled-functions.js
import compiledFunctions from './compiled-functions.js';await MikroORM.init({ compiledFunctions, // ...});
일치하는 사전 컴파일 함수가 존재하면 그것을 직접 사용합니다. 그렇지 않으면 기존 JIT 경로로 폴백합니다. 즉, 개발(JIT)과 프로덕션(사전 컴파일)에서 설정을 변경하지 않고도 동일한 설정을 사용할 수 있습니다.
@mikro-orm/migrations 패키지는 더 이상 umzug에 의존하지 않습니다. 마이그레이션 오케스트레이션은 이제 완전히 인라인이며, 모든 node: import는 동적 import() 호출 뒤에 숨겨져 있습니다. migrationsList 배열을 제공하면 파일 시스템 접근이 전혀 필요 없어, 번들 환경과 엣지 런타임에서도 마이그레이션이 완전히 호환됩니다.
seeder에도 동일한 접근이 적용됩니다 — 새 seedersList 옵션을 통해 seeders를 명시적으로 등록하여 파일 시스템 디스커버리를 피할 수 있습니다:
import { Migration1 } from './migrations/Migration1.js';import { Migration2 } from './migrations/Migration2.js';import { UserSeeder } from './seeders/UserSeeder.js';await MikroORM.init({ migrations: { migrationsList: [ { name: 'Migration1', class: Migration1 }, { name: 'Migration2', class: Migration2 }, ], }, seeder: { seedersList: [ { name: 'UserSeeder', class: UserSeeder }, ], },});
사전 컴파일 함수와 코어의 Node.js 의존성 0까지 결합하면, 이제 마이그레이션과 시딩까지 포함한 완전한 MikroORM 애플리케이션을 Cloudflare Workers 같은 엣지 런타임에 배포할 수 있습니다.
ts-node 시대는 끝났습니다. v7은 여러 TypeScript 로더를 기본 지원합니다 — swc, tsx, jiti, tsimp 중 하나를 설치하기만 하면 자동으로 동작합니다. 더 이상 mikro-orm-esm 스크립트, 커스텀 로더, shebang 해크를 만지작거릴 필요가 없습니다.
# just install your preferred loadernpm install tsx# and use the CLI as usualnpx mikro-orm schema:update
MIKRO_ORM_CLI_USE_TS_NODE 환경 변수는 MIKRO_ORM_CLI_PREFER_TS로 대체되었습니다.
이전에는 MikroORM 내부의 많은 부분이 엔티티 클래스 이름에 의존했는데, 일부 번들러(Next.js, 당신 얘기입니다)는 클래스 이름을 망가뜨리기도 하고 심지어 유일성도 보장하지 않습니다. 우리는 이를 검증하느라 사용자가 번들러 설정에서 클래스 이름 맹글링을 비활성화하도록 강제하곤 했습니다.
v7에서는 내부적으로 클래스 이름 대신 클래스 참조를 사용하여, 중복되거나 망가진 클래스 이름에 대해 MikroORM이 훨씬 더 견고해졌습니다. 다만 여전히 클래스 이름에 의존하는 곳도 존재합니다(예: single table inheritance discriminator). 그래도 전반적인 상황은 크게 개선되었습니다.
Next.js 프로젝트에서 MikroORM을 설정하는 전체 가이드는 새 Usage with Next.js 문서를 참고하세요.
v7은 내장 슬로우 쿼리 감지를 추가합니다. 밀리초 단위 임계값을 설정하면, 이를 초과하는 모든 쿼리가 debug 설정과 무관하게 warning 레벨로 기록됩니다:
const orm = await MikroORM.init({ slowQueryThreshold: 200, // log queries taking 200ms or more});
슬로우 쿼리는 일반 쿼리 로그와 동일한 포맷(하이라이트, 결과 개수, 리플리카 정보)으로 slow-query 네임스페이스를 통해 기록됩니다. 성공/실패한 쿼리 모두 임계값 검사를 합니다.
slowQueryLoggerFactory를 통해 슬로우 쿼리 로그를 별도의 대상으로 라우팅할 수도 있습니다(파일, 모니터링 서비스 등):
const orm = await MikroORM.init({ slowQueryThreshold: 200, slowQueryLoggerFactory: options => new DefaultLogger({ ...options, writer: msg => fs.appendFileSync('slow-queries.log', msg + '\n'), }),});
자동 flush 모드(쿼리 전에 dirty 엔티티를 검사하고 필요하면 flush하는 모드)는 성능 향상을 위해 재작업되었습니다. 이제 메커니즘은 필요할 때만 get/set 프로퍼티 디스크립터를 사용합니다 — 구체적으로 스칼라 프로퍼티는 엔티티에 대해 em.persist()를 호출할 때만 재정의됩니다. 이는 대량의 엔티티를 다룰 때 성능 오버헤드를 줄여줍니다.
엔티티 제너레이터는 이제 MikroORM을 현대적으로 사용하는 방식에 맞춘 업데이트된 기본값을 제공합니다:
{ entityDefinition: 'defineEntity', // was 'decorators' enumMode: 'dictionary', // was 'ts-enum' bidirectionalRelations: true, // was false identifiedReferences: true, // was false}

MikroORM의 타입 시스템은 강력하지만 역사적으로 TypeScript 컴파일러에 비용이 컸습니다. v7에는 타입 인스턴스화 비용을 전반적으로 낮추기 위한 집중적인 노력이 포함되었습니다. Loaded, AutoPath, InferEntity, EntityData(em.create와 em.assign에서 사용), defineEntity 스키마 같은 핵심 타입들이 모두 재작업되어, 컴파일러가 평가해야 하는 타입 인스턴스화 수를 줄였습니다.
결과는 의미 있게 나타났습니다 — 타입 벤치마크에서 복잡한 em.assign 호출은 최대 40% 더 적은 타입 인스턴스화를 보였고, 프로퍼티가 많은 defineEntity 스키마도 눈에 띄게 더 빠르게 체크됩니다. 대규모 MikroORM 프로젝트에서 IDE 피드백이 느리거나 tsc 시간이 길었던 경험이 있다면, v7은 훨씬 더 경쾌하게 느껴질 것입니다.
v7과 함께 문서도 대대적으로 개편되었습니다. 주요 내용은 다음과 같습니다:
defineEntity를 기본 접근으로 사용하며, 해피 패스에 집중합니다: 제로 설정, 어떤 TS 로더든 가능, 번들러에서 즉시 동작. 더 이상 ts-morph 엣지 케이스에 대한 페이지는 없습니다.defineEntity와 데코레이터를 나란히 보여주므로, 선호하는 스타일을 따라갈 수 있습니다.툴링 측면에서, 코드베이스는 ESLint에서 oxlint로 전환했고 포매팅에는 oxfmt를 채택했습니다 — 둘 다 Oxidation Compiler 프로젝트의 일부입니다. 이제 전체 모노레포를 린트하는 데 몇 분이 아니라 몇 초가 걸립니다.

메이저 버전에는 빠질 수 없는 브레이킹 체인지도 있습니다. 대부분은 단순한 이름 변경이나 정리이지만, 데이터에 조용히 영향을 주거나 빌드를 깨뜨릴 수 있어 특별히 주의할 만한 것들도 몇 가지 있습니다.
forceUtcTimezone enabled by defaultcaution
이는 아마도 반드시 알아야 할 가장 중요한 브레이킹 체인지일 것입니다. forceUtcTimezone 옵션이 이제 모든 SQL 드라이버에서 기본으로 활성화됩니다. PostgreSQL 드라이버는 v6에서도 이미 기본값이 true였으므로, 이는 주로 MySQL, MariaDB, MSSQL 사용자에게 영향을 줍니다. 즉, 타임존이 없는 datetime 컬럼(MySQL/MSSQL의 datetime, PostgreSQL의 timestamp)은 UTC로 값을 저장하고 가져오게 됩니다.
기존 데이터가 로컬 타임존으로 저장되어 있었다면, UTC로 데이터를 마이그레이션하거나 이 옵션을 비활성화하지 않는 한 타임스탬프가 잘못 해석됩니다:
MikroORM.init({ forceUtcTimezone: false, // keep the old behavior});
cascade option이전에는 MikroORM이 ORM 레벨 cascade 옵션으로부터 데이터베이스 레벨 외래 키 규칙(ON DELETE, ON UPDATE)을 추론했습니다. 이 결합은 혼란스러웠습니다 — Cascade.REMOVE가 ORM cascading과 DB cascading이 다른 개념임에도 불구하고, DB 레벨에서도 조용히 deleteRule: 'cascade'를 설정했기 때문입니다.
v7에서는 이들이 완전히 독립되었습니다. 기존 추론에 의존하고 있었다면, 스키마 제너레이터를 처음 실행할 때 스키마 diff가 나타날 것입니다. 개별 관계에 규칙을 명시하거나, 전역 기본값을 설정할 수 있습니다:
MikroORM.init({ schemaGenerator: { defaultDeleteRule: 'cascade', defaultUpdateRule: 'cascade', },});
ReflectMetadataProvider no longer the defaultreflect-metadata를 사용한 데코레이터 타입 추론을 사용한다면, 이제 메타데이터 프로바이더를 명시적으로 설정해야 합니다:
import { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy';MikroORM.init({ metadataProvider: ReflectMetadataProvider,});
defineEntity, ts-morph를 사용하거나 데코레이터 옵션에서 타입을 명시적으로 제공한다면 영향을 받지 않습니다.
em.create() and em.assign() typingem.create()와 em.assign()은 이제 데이터 파라미터에 대해 더 엄격한 타입 체크를 수행합니다. 타입이 지정된 DTO(예: Zod 추론 타입)를 사용한다면, 이전에는 조용히 무시되던 프로퍼티 이름 오타가 이제 컴파일 에러를 발생시킵니다:
type CreateUserDto = { firstName: string; lastNme?: string }; // typo!em.create(User, dto); // TS error in v7 — 'lastNme' doesn't exist on User
이는 좋은 변화입니다! 하지만 이전에는 조용히 통과하던 기존 코드의 오류가 드러날 수 있습니다.
validate and strict always enabled두 옵션은 이제 항상 활성화되며, 자동 수정 메커니즘은 제거되었습니다. 실제로 이는 PostgreSQL에서 sum 같은 집계 함수가 기본적으로 문자열을 반환하는 경우, 결과가 더 이상 조용히 숫자로 캐스팅되지 않는다는 의미입니다 — 타입은 직접 처리해야 합니다.
em.persist()flushMode: 'auto'를 사용한다면, 이제 스칼라 프로퍼티 변경 감지를 위해 명시적인 em.persist() 호출이 필요합니다. 이것이 없으면 스칼라 프로퍼티 변경은 auto flush 체크에서 감지되지 않습니다. 이는 auto flush를 사용하지 않는 프로젝트에서 성능 오버헤드를 줄입니다.
이전에는 환경 변수가 항상 최우선이었습니다 — 오래된 MIKRO_ORM_HOST 환경 변수가 MikroORM.init()에 전달한 명시적 host 옵션을 조용히 덮어쓸 수 있었습니다. v7에서 우선순위는 다음과 같습니다: 명시적 옵션 > 환경 변수 > 설정 파일 > 기본값.
// v6: env var MIKRO_ORM_HOST=db.prod.internal would override the host below// v7: the explicit host option wins, env var is ignoredconst orm = await MikroORM.init({ host: 'localhost', // ...});
설정 파일을 import하여 MikroORM.init(config)로 전달하면, 설정 파일의 모든 값은 명시적 옵션으로 취급되므로 환경 변수가 이를 덮어쓰지 않습니다. v6의 “환경 변수가 항상 이김” 동작을 복원하려면 preferEnvVars 옵션을 사용하세요:
export default defineConfig({ preferEnvVars: true, host: 'localhost', // MIKRO_ORM_HOST env var will override 'localhost'});
@Formula, 인덱스 표현식, 체크 제약 조건, 생성 컬럼에 대한 콜백 시그니처가 변경되었습니다. 파라미터 순서가 바뀌어 이제 columns가 첫 번째, table이 두 번째입니다. 컬럼 값은 따옴표 처리되지 않은 상태로 전달되며, 새로운 quote tagged template 헬퍼가 모든 데이터베이스 플랫폼에서 올바른 식별자 quoting을 처리합니다:
-import { Entity, Formula } from '@mikro-orm/core';+import { Entity, Formula, quote } from '@mikro-orm/core';-@Formula(alias => ${alias}.price * 1.19)+@Formula(cols => quote${cols.price} * 1.19) priceTaxed?: number;
quote 헬퍼는 드라이버별로 올바른 quoting을 보장합니다 — MySQL은 백틱, PostgreSQL은 큰따옴표, MSSQL은 대괄호를 사용합니다. 인덱스 표현식과 체크 제약 조건에서도 동일하게 동작합니다:
-expression: (table, columns, name) => create index ${name} on ${table} (${columns.email})+expression: (columns, table, name) => quotecreate index ${name} on ${table} (${columns.email})-check: columns => ${columns.price} > 0+check: (columns, table) => quote${columns.price} > 0``
하위 호환성을 위해 cols.toString()은 quoted 된 테이블 alias를 반환하므로 단순한 템플릿 리터럴 사용은 여전히 동작합니다 — 하지만 컬럼 이름까지 포함한 완전한 식별자 quoting은 quote 헬퍼만 제공합니다.
persistAndFlush() 및 removeAndFlush() 제거 — 대신 em.persist(entity).flush()를 사용하세요.qb.execute() 또는 qb.getResult()를 사용하세요.em.find(User); em.find('User') 아님).MikroORM.initSync 제거 — 대신 new MikroORM({ ... })를 직접 사용하세요.MikroORM.init()은 이제 명시적 설정이 필요합니다 — 더 이상 CLI 설정을 암묵적으로 로딩하지 않습니다.@mikro-orm/better-sqlite 드라이버 제거 — 대신 @mikro-orm/sqlite를 사용하세요(내부적으로 better-sqlite3 사용).driverOptions 구조 변경 — 옵션이 connection 객체로 감싸지지 않고, 기본 DB 클라이언트에 직접 전달됩니다.embeddables.prefixMode가 relative로 변경되었습니다.em.addFilter() 시그니처가 단일 옵션 객체를 사용하도록 변경되었습니다.@Transactional 데코레이터의 기본 전파가 REQUIRED로 변경되었습니다.SchemaGenerator/Migrator/Seeder 메서드 이름 변경(예: createSchema() → create(), createMigration() → create()).ArrayCollection이 Collection에 병합되었습니다.connect 옵션이 제거되었습니다.@mikro-orm/decorators/legacy 또는 @mikro-orm/decorators/es에서 import해야 합니다 — 드라이버 패키지에서 더 이상 재-export되지 않습니다.#private 필드를 사용합니다 — as any 캐스트나 밑줄 접두 프로퍼티로 내부에 접근하던 코드는 문서화된 public API로 마이그레이션해야 합니다.그리고 정말 정말 더 많은 변경이 있습니다 — 전체 변경 로그는 여기를 참고하세요.
업그레이드 중인가요? v6 → v7 업그레이드 가이드에는 모든 브레이킹 체인지가 전/후 예제와 마이그레이션 단계로 자세히 정리되어 있습니다.

MikroORM은 이제 새 @mikro-orm/oracledb 패키지를 통해 Oracle Database를 지원합니다. 이는 oracledb 드라이버에 의해 구동됩니다. 이로써 지원되는 데이터베이스의 총 수는 8개가 되었습니다.
import { OracleDriver } from '@mikro-orm/oracledb';const orm = await MikroORM.init({ driver: OracleDriver, dbName: 'XEPDB1', host: 'localhost', port: 1521, user: 'orm_test', password: 'secret',});
이 드라이버는 스키마 제너레이터, 쿼리 빌더, 엔티티 매니저, 그리고 모든 표준 ORM 기능을 완전히 지원합니다 — Oracle 특유의 SQL dialect 처리, 예외 변환, 시퀀스 기반 자동 증가까지 포함합니다. 자세한 내용은 Oracle driver documentation을 참고하세요.
로드맵의 다음은 PGlite 드라이버입니다 — Kysely 0.29가 안정 릴리스에 도달하면, 경량의 프로세스 내 PostgreSQL 구현에 대한 지원을 추가할 수 있을 것입니다.
@mikro-orm/nestjs 어댑터도 v7에 맞춰 업데이트되었습니다. 네이티브 ESM, 새 데코레이터 import, 업데이트된 드라이버 패키지 등 새로운 기능을 모두 기본으로 지원합니다.
시작을 돕기 위해 Getting Started 가이드와 NestJS RealWorld 예제 앱 모두에 대해 마이그레이션 PR도 준비했습니다 — 이는 여러분의 NestJS 프로젝트를 v7로 업그레이드할 때 실용적인 참고 자료가 될 수 있습니다.
이번 릴리스는 PR을 제출하고, 이슈를 보고하고, 프리릴리스를 테스트하는 데 도움을 준 많은 기여자들이 없었다면 불가능했을 것입니다. 특히 코드로 기여해주신 모든 분들께 특별한 감사를 드립니다 — 전체 기여자 목록은 changelog에서 확인할 수 있습니다.
LikeMikroORM? ⭐️Star iton GitHub and share this article with your friends. If you want to support the project financially, you can do so viaGitHub Sponsors.