GN⁺: Rust-Query - Rust Type 시스템을 사용한 안전한 RDB쿼리
(blog.lucasholten.com)- "Rust에서 안전하게 데이터를 영구 저장하고, 복잡한 쿼리를 쉽게 작성하며, SQL을 한 줄도 작성하지 않아도 된다면 어떨까?"
- Rust-query는 이를 실현하기 위해 개발된 라이브러리임
Rust와 데이터베이스
- Rust의 기존 데이터베이스 라이브러리는 컴파일 시점 보장이 부족하거나 사용이 번거롭고 SQL처럼 직관적이지 않음
- 데이터베이스는 충돌-방지 소프트웨어를 구축하고, 아토믹 트랜잭션을 지원하는 데 중요한 역할을 함
- SQL은 데이터베이스와 상호작용하기 위한 표준 프로토콜이지만, 컴퓨터가 생성하는 것이 적합하며 사람이 직접 작성하기에는 비효율적임
Rust-query 소개
- rust-query는 Rust의 타입 시스템과 깊이 통합된 데이터베이스 쿼리 라이브러리임
- Rust에서 네이티브처럼 데이터베이스 작업을 수행할 수 있도록 설계됨
주요 기능 및 설계 결정
-
명시적 테이블 별칭: 테이블 조인 후 해당 테이블을 나타내는 더미 객체 제공 (
let user = User::join(rows);
) -
Null 안전성: 쿼리의 선택적 값은 Rust의
Option
타입으로 처리 -
직관적인 집계 함수:
GROUP BY
없이 행 단위의 직관적인 집계 지원 -
타입 안전 외래 키 탐색: 외래 키를 기반으로 암시적 조인을 쉽게 수행 (
track.album().artist().name()
) -
타입 안전 고유 조회: 특정 고유 제약 조건을 가진 행 조회 (
Option<Rating>
반환) - 다중 버전 스키마: 선언적 방식으로 모든 스키마 버전 차이를 확인 가능
- 타입 안전 마이그레이션: 임의의 Rust 코드를 사용해 행 처리 가능
- 타입 안전 고유 충돌 처리: 고유 제약 조건 충돌 시 특정 에러 타입 반환
- 트랜잭션 수명에 묶인 행 참조: 행 참조는 행이 존재할 때만 유효
- 캡슐화된 타입 행 ID: 행 번호는 API 외부로 노출되지 않음
쿼리 및 데이터 삽입
스키마 정의
#[schema]
enum Schema {
User {
name: String,
},
Story {
author: User,
title: String,
content: String,
},
#[unique(user, story)]
Rating {
user: User,
story: Story,
stars: i64,
},
}
use v0::*;
- Rust의
enum
구문을 사용하여 스키마 정의 - 외래 키 제약 조건은 다른 테이블 이름을 열 타입으로 지정하여 생성
-
#[unique]
속성을 사용해 고유 제약 조건 추가 -
#[schema]
매크로는 정의를 분석하여v0
모듈 생성
데이터 삽입
fn insert_data(txn: &mut TransactionMut<Schema>) {
let alice = txn.insert(User { name: "alice" });
let bob = txn.insert(User { name: "bob" });
let dream = txn.insert(Story {
author: alice,
title: "My crazy dream",
content: "A dinosaur and a bird...",
});
let rating = txn.try_insert(Rating {
user: bob,
story: dream,
stars: 5,
}).expect("no rating for this user and story exists yet");
}
- 삽입 작업은 새로 삽입된 행의 참조를 반환
- 고유 제약 조건이 있는 테이블 삽입 시
try_insert
사용 필요 -
try_insert
는 충돌 시 특정 에러 타입 반환
데이터 쿼리
fn query_data(txn: &Transaction<Schema>) {
let results = txn.query(|rows| {
let story = Story::join(rows);
let avg_rating = aggregate(|rows| {
let rating = Rating::join(rows);
rows.filter_on(rating.story(), &story);
rows.avg(rating.stars().as_float())
});
rows.into_vec((story.title(), avg_rating))
});
for (title, avg_rating) in results {
println!("story '{title}' has avg rating {avg_rating:?}");
}
}
-
rows
는 쿼리에서 현재 행 집합을 나타냄 -
aggregate
를 사용하여 집계 연산 수행 - 결과는 튜플이나 구조체의 벡터로 수집 가능
스키마 진화와 마이그레이션
- 새로운 스키마 버전을 생성할 때는
#[version]
속성을 사용함
새로운 스키마 버전 추가
#[schema]
#[version(0..=1)]
enum Schema {
User {
name: String,
#[version(1..)]
email: String,
},
// ... 나머지 스키마 ...
}
use v1::*;
데이터 마이그레이션
- 마이그레이션은 이전 및 새로운 스키마에 대해 타입 검사됨
- 행 데이터를 임의의 Rust 코드로 처리 가능 (
map_dummy
사용)
let m = m.migrate(v1::update::Schema {
user: Box::new(|old_user| {
Alter::new(v1::update::UserMigration {
email: old_user
.name()
.map_dummy(|name| format!("{name}@example.com")),
})
}),
});
마무리
-
rust-query는 Rust에서 관계형 데이터베이스와 상호작용하는 새로운 접근 방식을 제시함:
- 컴파일 시점 검사
- Rust와 조합 가능한 쿼리
- 타입 검사를 통한 스키마 진화 지원
- 현재 SQLite를 유일한 백엔드로 사용하며, 실험적 응용 프로그램 개발에 적합
- GitHub 이슈를 통해 피드백 환영
| 컴퓨터가 생성하는 것이 적합하며 사람이 직접 작성하기에는 비효율적임
개발자가 100명 이상 투입되는 한국에만 있는 '차세대'를 해보는 입장에서는.
매우 흥미롭군요.
사실 투입되는 대부분의 개발자들은 SQL전문가? 들이거든요.
Hacker News 의견
-
애플리케이션 정의 스키마에 대한 우려는 잘못된 시스템에 의해 검증된다는 점임. 데이터베이스가 스키마의 권위자이며, 다른 모든 애플리케이션 레이어는 이를 기반으로 가정함. Rust의 SQLx는 데이터베이스 타입에 기반한 구조체를 생성하여 컴파일 타임에 검증하지만, 프로덕션 데이터베이스와 동일한 타입을 보장하지 않음. 로컬 Postgres v15에서 쿼리를 설계하고 프로덕션에서 Postgres v12를 실행할 때 런타임 오류가 발생할 수 있음. 애플리케이션 정의 스키마는 잘못된 안전감을 제공하고 엔지니어에게 추가 작업을 부과함.
-
SQL은 완벽하지 않지만 몇 가지 장점이 있음. 대부분의 사람들이 기본적인 SQL을 알고 있으며, PostgreSQL 같은 데이터베이스의 문서는 SQL로 작성됨. 외부 도구도 SQL을 사용하며, 쿼리 변경 시 비싼 컴파일 단계가 필요하지 않음. SQLx는 매개변수를 타입 체크하고 데이터베이스 자체가 쿼리를 검증하도록 하여 컴파일 시간을 증가시키는 타입 시스템 문제를 피함. 새로운 데이터베이스에서는 더 나은 쿼리 언어가 승리할 수 있지만, 기존 SQL 데이터베이스에서는 SQLx가 더 나은 선택임.
-
SQL은 컴퓨터가 작성해야 한다는 의견에 대해 반대하는 의견이 있음. SQL은 고수준 언어로, 파이썬이나 러스트보다 더 높은 수준임. SQL은 읽기 쉽고 사용하기 쉽게 설계되었으며, 컴파일 시 여러 절차로 변환됨. SQL은 웹 개발의 병목 지점에 있으며, 상태 변형이 발생하는 곳임. SQL은 높은 수준의 언어이기 때문에 최적화가 어려움. SQL은 기술 부채로, 더 적절한 API를 개발하는 것보다 SQL을 사용하는 것이 10배 더 효율적임.
-
Rust의 typesafe-db-access에 대한 탐색이 기쁘다는 의견이 있음. 기존 라이브러리는 컴파일 타임 보장을 제공하지 않으며, SQL처럼 장황하거나 어색함. diesel은 컴파일 타임 보장을 제공함. ORM 대 비ORM 논쟁에서 typesafe 쿼리 빌더를 선호하며, diesel은 이 범주에 속함. Rust-query는 전체 ORM 쪽으로 기울어질 것으로 보임.
-
스키마와 데이터 타입을 연결하는 접근 방식이 흥미롭다는 의견이 있음. 예제에서 Schema 열거형이 없다는 점이 직관적이지 않음. 매크로 안에 정의되면 더 명확할 것임.
-
라이브러리 API에서 실제 행 번호가 노출되지 않는다는 점이 혼란스러움. 웹 서버에서는 데이터에 행 ID를 전달하여 프론트엔드에서 다른 요청으로 데이터를 참조하고 수정할 수 있어야 함.
-
SQL은 컴퓨터가 작성해야 한다는 의견에 부분적으로 동의하지만, SQL은 코드 생성기가 작성하기에 가장 편리한 언어가 아님. 단순한 계획 최적화가 쿼리의 레이아웃을 완전히 변경할 수 있음. Google의 SQL pipe 제안은 약간 개선되었지만, 새로운 쿼리 언어의 문제를 여전히 가지고 있음.
-
SeaQuery를 사용해왔지만, 고급 쿼리를 생성하려면 문서가 충분하지 않다는 의견이 있음. 강력한 타입의 쿼리가 개발 과정을 느리게 할 수 있어, 기존의 준비된 문과 값 바인딩으로 돌아가는 것을 고려 중임.
-
개별 행 수준의 조작을 통한 마이그레이션은 실행 속도가 매우 느릴 수 있음. 예를 들어, 10억 개의 행이 있는 테이블에서 일반적인 업데이트 문이 한 시간까지 걸릴 수 있음. 행별 업데이트는 더 많은 시간이 걸릴 것임.