Turso 는 SQLite 의 오픈소스 포크인
libSQL
를 기반으로 만든 Edge(분산형) 데이터베이스 서비스(DBasS)로 매우 빠르고 저렴한 것이 특징이다. Drizzle ORM 과 함께 사용하는 방법에 대해 공부한다.
0. 개요
- Bun 1.0.3 + Elysia (TS)
- Turso (SQLite)
- Drizzle ORM
화면 캡쳐
1. Turso
Turso 는 SQLite 를 Edge(분산형) 서비스 형태로 제공하는 데이터베이스이다.
- 가장 빠른 : CDN 처럼 SQLite 복제본이 가까운 클라우드에서 제공된다.
- 가장 쉬운 : SQLite 의 가볍고 단순한 사용법을 장점으로 삼았다.
- 가장 저렴 : 단일 파일 구조라서 용량 단위로 비용이 청구된다.
- Starter 플랜(무료)은 데이터베이스 500개, 최대 9GB, Locations 3개
Turso 의 edge 서비스 예시
참고문서
- Turso - Blog
- Drizzle - Turso(SQLite)
- 깃허브 - NTAK666/try-bun
- BETH Stack : Bun + Elysia + Turso + HTMX
- 깃허브 - justinfriebel/sveltekit-turso-drizzle
CLI 설치 및 로그인
- 한번 로그인 해 두면 환경변수로 API 토큰 설정 안해도 되더라.
- 참고 : Running remotely
1
2
3
4
5
6
7
8
9
10
11
12
13
# macOS
brew install tursodatabase/tap/turso
# Linux or WSL
curl -sSfL https://get.tur.so/install.sh | bash
# 웹브라우저 로그인 : account 생성
turso auth signup
# 인증 토큰 조회
turso auth token
# "token..."
export TURSO_API_TOKEN=[YOUR-TOKEN-STRING]
데이터베이스 생성 및 관리
- Turso 대시보드
- DB 관리를 위해 생성(create)과 삭제(destroy)를 기억해 두고
- 평상시에는 shell 로 테이블과 데이터를 관리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# default 위치에 db 생성
turso db create my-db
# Turso SQL shell
turso db shell my-db
turso db show my-db
# Name: my-db
# URL: libsql://[DB-NAME]-[ORG-NAME].turso.io
# Locations: sin (일단은 primary 하나 밖에 없음)
turso db tokens create my-db
# "token..."
turso db list
turso db destroy my-db
# Are you sure? [y/n]
group 및 location 관리
- primary location 은 변경할 방법이 없는 듯 하다. (CLI 에서 가장 가까운 지역)
- 설정으로 꼭 포함할 지역을 추가(add) 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
turso db locations
# nrt Tokyo, Japan
# hkg Hong Kong, Hong Kong
# sin Singapore, Singapore [default]
# ...
turso group locations add default nrt
# Group default replicated to nrt in 6 seconds.
turso group locationslist default
# nrt, sin (primary)
turso db create --location nrt my-db
# primary 지역(sin)에 하나 생기고, nrt 에 복사본 생성
# 복사본 추가 생성
# turso db replicate my-db hkg
Turso 로컬 개발환경 설정
데몬 프로그램으로 sqld 가 필요하다.
1
2
3
4
5
6
7
8
9
10
11
12
# macOS 설치
$ brew tap libsql/sqld
$ brew install sqld
# 또는 Docker
$ docker run -p 8080:8080 -d ghcr.io/libsql/sqld:latest
$ turso dev
sqld listening on port 8080.
Use the following URL to configure your libSQL client SDK for local development:
http://127.0.0.1:8080
또는 로컬 파일로 생성할 수 있다.
1
$ turso dev --db-file book.sqlite
2. ElaysiaJS 로 REST API 만들기
앞서 작성했던 bun-puppeteer-tutorial 코드를 조금 더 다듬어 보았다.
- ElaysiaJS : ExpressJS 유사 프레임워크
- Turso(libSQL)
- Drizzle ORM
use
로 파일 분리하기
db, html, puppeteer 등 기능별로 파일을 분리하고, index.ts
에서 통합한다.
- [src]
- index.ts
- server-db.ts
- server-html.ts
- server-puppeteer.ts
- [html]
- index.html
- script.js
- [lib]
- db.ts
- error.ts
- scraper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Elysia, t, NotFoundError } from 'elysia';
import { handleError } from '$lib/error';
import { Browser } from 'puppeteer';
import { app as dbApp } from './server-db';
import { app as htmlApp } from './server-html';
import { app as puppeteerApp } from './server-puppeteer';
const app = new Elysia()
.use(dbApp)
.use(htmlApp)
.use(puppeteerApp)
.onError((err) => handleError(err))
.onStart(async ({ browser }) => {
console.log('💫 Elysia start!');
if (browser && browser instanceof Browser) {
console.log('Browser version :', await browser.version());
}
})
.onStop(async ({ browser, db }) => {
if (browser && browser instanceof Browser) {
await browser.close();
console.log('Browser is closed!');
}
console.log('💤 Elysia stop!');
});
typebox 이용한 params, query 타입 검증 및 변환
숫자의 경우 transform
단계가 필요한데, elysia 에서 상용구 t.Numeric
으로 지원한다.
- elysia 의
t
는 typebox를 다시 export 한 이름(alias)이다.- typebox 는 zod 라이브러리와 유사하다. Typescript 를 검증과 변환 등을 수행
- 쿼리 파리미터는 query 로, 경로 파라미터는 params 로 접근한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { t } from 'elysia';
const app = new Elysia()
.get(
'/query',
({ query: { id } }) => {
console.log(`query params: id=${id}`, typeof id);
return {
type: 'query',
params: [id],
};
},
{
query: t.Object({
id: t.Numeric(), // parseInt(query.id) 대신에 형변환 처리
}),
}
);
복잡한 model 정의를 분리하는 방법
- POST 메소드를 위한 body 모델을 정의
- 필수 요소로 name, author 가 필요하다
- response 를 위해 필수 및 생략가능(Optional) 속성으로 구성
- success 는 꼭 필요
- operator, data, affectedId 등은 생략 가능 (Nullable 과는 다르다)
- model 은 use 키워드로 적용되고, 타입 검증시 alias 로 접근한다.
- 입력으로 book 모델 이용
- 출력으로 result 모델 이용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const bookModel = new Elysia().model({
book: t.Object({
name: t.String(),
author: t.String(),
}),
result: t.Object({
success: t.Boolean(),
operator: t.Optional(
// === operator?: string
t.Union([t.Literal('create'), t.Literal('update'), t.Literal('delete')])
),
data: t.Optional(t.Object({})),
affectedId: t.Optional(t.Number()),
}),
});
export const app = new Elysia()
.use(bookModel)
.decorate('db', new BooksDatabase())
.get('/books', ({ db }) => db.getBooks())
.post(
'/books',
async ({ db, body }) => {
try {
const result = await db.addBook(body);
return { success: true, data: result.shift() };
} catch (e) {
console.error('create:', e);
return { success: false, operator: 'create' };
}
},
{
body: 'book',
response: 'result',
}
)
3. drizzle orm 으로 turso 다루기
참고문서
- Drizzle ORM - SQLite 컬럼 타입 : schema 정의시 사용
- Turso 예제 - 깃허브 api-mug-store-api
- 인덱스 생성, 외부키 연결, createdAt(날짜 기본값) 등 스키마 예시가 좋다.
- 추가 : Turso 예제 - 이커머스 상점(Remix, Drizzle, Turso)
books 테이블 스키마
Turso 는 libSQL
기반이기 때문에 sqlite-core
를 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
// https://orm.drizzle.team/docs/column-types/sqlite
export const books = sqliteTable('books', {
id: integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
author: text('author').notNull(),
});
/*
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
author TEXT
)
*/
Drizzle ORM 로 CRUD 처리하기
Books 를 다루기 위한 메소드들을 묶어 BooksDatabase
클래스로 정의했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { drizzle, LibSQLDatabase } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import { sql, eq } from 'drizzle-orm';
import * as schema from '../../drizzle/schema';
export type NewBook = typeof schema.books.$inferInsert;
export class BooksDatabase {
private db: LibSQLDatabase;
constructor() {
if (process.env.TURSO_DB_URL === undefined) {
throw new Error('TURSO_DB_URL is not defined');
}
if (process.env.TURSO_DB_AUTH_TOKEN === undefined) {
throw new Error('TURSO_DB_AUTH_TOKEN is not defined');
}
const client = createClient({
url: process.env.TURSO_DB_URL,
authToken: process.env.TURSO_DB_AUTH_TOKEN,
});
this.db = drizzle(client);
this.init()
.then(() => console.log('Database initialized'))
.catch(console.error);
}
async init() {
return this.db.run(sql`
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
author TEXT
)
`);
}
async getBooks() {
return await this.db.select().from(schema.books);
}
async addBook(book: NewBook) {
return await this.db
.insert(schema.books)
.values({
name: book.name,
author: book.author,
})
.returning();
}
async updateBook(id: number, book: NewBook) {
return await this.db
.update(schema.books)
.set({
name: book.name,
author: book.author,
})
.where(eq(schema.books.id, id))
.returning({ affectedId: schema.books.id });
}
async deleteBook(id: number) {
return await this.db
.delete(schema.books)
.where(eq(schema.books.id, id))
.returning({ affectedId: schema.books.id });
}
}
Insert 를 위한 타입 추정 상용구 : $inferInsert
id
컬럼은 생략 가능한 형태로 타입을 생성한다.- select 를 위한 타입 추정에는
$inferSelect
를 사용 (id 포함)
- select 를 위한 타입 추정에는
1
2
3
4
5
6
7
8
9
export type NewBook = typeof schema.books.$inferInsert;
/*
export interface NewBook {
id?: number;
name: string;
author: string;
}
*/
insert/update/delete 이후 결과 반환 : returning
- Drizzle 에서 postgresql, sqlite 에 대해 지원한다. (mysql 제외)
- returning 의 반환 형태로 컬럼 스키마를 지정할 수 있다.
affectedId
컬럼으로id
값을 반환하도록 작성- 이후 elysia 에서 response 모델로
affectedId
속성을 작성
1
2
3
4
return await this.db
.delete(schema.books)
.where(eq(schema.books.id, id))
.returning({ affectedId: schema.books.id });
9. Review
- 중간에
Table is locked
오류가 있었는데, 재로그인 하니깐 해결되었다.- 개발중이라 오류로 중단되는 경우가 많았는데 그 탓에 close 처리가 안되어 발생
- Turso 가 빠르다는건 모르겠다. 글로벌로 제공해야 edge 효과를 얻을듯.
- 다만 무료 버전이라도 마음껏 DB 를 생성하는건 좋다.
- 그래서 로컬 개발환경은 아직 사용해보지 못했다.
- Elysia 에 더 익숙해지는 기회가 되었다. 다루기 편해지니깐 좋아졌다.
- Docker 작업은 다음에 하자.
참고
끝! 읽어주셔서 감사합니다.