목록으로

Tauri + sqlite + diesel + r2d2 pool

데스크탑 앱 개발 프레임워크 tauri와 rust ORM diesel을 r2d2 pool과 함께 사용하는 방법을 정리했다. 로컬 db로 사용하기 위해서 sqlite를 사용했지만 다른 db에도 적용 가능할 것이다.

Tauri 프로젝트 생성

yarn create tauri-app 을 사용해 프로젝트를 생성했다. cargo create-tauri-app 으로 생성하면 웹 프레임워크를 선택할 수 없고 vanilla js로만 만들어진다.

주의: yarn create tauri-appsveltesvelte-ts 는 sveltekit이 아니라 그냥 svelte 프로젝트다. 그래서 yarn create svelte@latest 로 sveltekit 프로젝트를 생성하고 tauri를 따로 설치해주었다. 자세한 과정은 tauri 문서에 설명되어 있다.

2022년 10월 5일부터 create-tauri-app이 Sveltekit도 지원한다. 그래서 yarn create tauri-app을 쓰면 된다.

Diesel

diesel은 rust를 위한 ORM이다. sql에 알맞은 타입을 생성해주어 컴파일 타임에 sql 오류를 잡을 수 있다.

diesel이 sql에서 타입을 추론해야 하는 만큼 sql도 직접 작성해야 하고 migration도 해줘야 하고 diesel_cli도 설치해야 해서 과정이 꽤 복잡하다. sqlite dll 문제도 있어서 한참 헤맸다.

diesel 설치

https://crates.io/crates/diesel 의 diesel crate를 cargo.toml 에 추가한다. 이때 features"sqlite""r2d2" 를 명시해주어야 한다. 그리고 diesel이 사용할 DATABASE_URL 환경 변수를 설정해주어야 하므로 dotenvy 라이브러리도 설치해준다.

[dependencies]
# tauri, serde 등 tauri 프로젝트 생성하면 설치되는 라이브러리 생략
diesel = { version = "2.0.0", features = ["sqlite", "r2d2"] }
dotenvy = "0.15.5"

2022년 9월 기준 버전이다.

r2d2 db 커넥션 풀 라이브러리. 요청이 들어올 때마다 db에 새로 연결하는 대신, 한 번만 연결해두고 그 연결을 반복해서 사용할 수 있도록 하는 것.. 정도로 이해하고 있다.

diesel_cli 설치

diesel과는 별개로 sql을 rust 타입으로 바꾸기 위해서 diesel_cli를 설치해야 한다. 그런데 공식문서대로 cargo install diesel 를 실행해봤지만 계속 실패했다. sqlite만 설치하기 위해서 cargo install diesel_cli --no-default-features --features sqlite 도 해봤고 choco install sqlite 도 해봤지만 안 됐다.

Installing diesel_cli on Windows : some assembly required · Issue #487 · diesel-rs/diesel

그러다가 이 이슈를 발견했다. 결론은 cargo install diesel_cli --no-default-features --features "sqlite-bundled" 를 실행하면 된다. sqlite dll을 함께 다운로드하는 옵션이다.

diesel 설정

이제 공식문서대로 진행하면 된다. 다만 주의할 점은 sqlite는 primary key가 null일 수도 있다는 점이다. 이거 때문에 id가 Option 이 돼서 한참 헤맸다. 그러니 id PRIMARY KEY NOT NULL 이라고 써 줘야 한다.

diesel migration run 까지 공식문서를 따른다.

models.rs

tauri와 통신하기 위해 Serialize 를 derive해야 한다.

use diesel::prelude::*;
use serde::Serialize;

#[derive(Queryable, Serialize)]
pub struct Post {
    pub id: i32,
    pub title: String,
}

establish_connection

rd2d pool을 사용하기 위해서 다음과 같이 써야 한다.

// src-tauri/src/lib.rs

use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use diesel::sqlite::SqliteConnection;
use dotenvy::dotenv;
use std::env;

pub mod models;
// src/models.rs를 공식문서대로 만들어준다
pub mod schema;

pub type SqlitePool = Pool<ConnectionManager<SqliteConnection>>;
pub type SqlitePooledConnection = PooledConnection<ConnectionManager<SqliteConnection>>;

pub fn establish_connection() -> SqlitePool {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    let manager = ConnectionManager::<SqliteConnection>::new(&database_url);
    Pool::builder()
        .build(manager)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

show_posts.rs

tauri의 [managed state] 기능을 사용할 수 있도록 Pool을 사용한다.

use crate::models::Post; // src/models.rs
use crate::schema::posts::dsl::*; // src/schema.rs
use crate::SqlitePool; // src/lib.rs의 SqlitePool
use diesel::prelude::*;

#[tauri::command]
pub fn show_posts(state: tauri::State<SqlitePool>) -> Vec<Post> {
    posts
        .load::<Post>(&mut *state.get().unwrap()) // 이 부분이 다르다
        .expect("Error loading posts")
}

main.rs

manage 를 통해 connection을 managed state로 만들어준다.

use app::posts::show_posts::show_posts; src/posts/show_posts.rs
use app::__cmd__show_posts; // src/posts/show_posts.rs에서 tauri::command 함수를 use 했더니 이렇게 됐다.
use app::establish_connection; // src/lib.rs

fn main() {
    let connection = establish_connection();
    tauri::Builder::default()
        .manage(connection) // 여기서 managed state로 만든다
        .invoke_handler(tauri::generate_handler![show_posts])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

이제 rust 부분은 끝났다.

프론트엔드에서 rust 호출

tauri 공식문서대로 따라하면 된다. 예시는 svelte지만 react든 뭐든 거의 비슷할 것이다.

let posts = [];
invoke('show_posts').then((res) => posts = res);
// {#await}을 사용하려면
// $: posts = invoke('show_posts'); 로 사용하면 된다

<ul>
	{each posts as post}
		<li>{post.title}</li>
	{/each}
</ul>

끝!