惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
WordPress大学
WordPress大学
B
Blog RSS Feed
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
C
Check Point Blog
P
Proofpoint News Feed
H
Help Net Security
月光博客
月光博客
博客园_首页
Stack Overflow Blog
Stack Overflow Blog
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
Recent Announcements
Recent Announcements
人人都是产品经理
人人都是产品经理
U
Unit 42
美团技术团队
I
InfoQ
A
About on SuperTechFans

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Why I Don't Use an ORM in My Rust Apps — Just SQL and rus...
hiyoyo · 2026-06-12 · via DEV Community
Cover image for Why I Don't Use an ORM in My Rust Apps — Just SQL and rusqlite

hiyoyo

All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

Every Rust project I start, someone suggests Diesel or SeaORM. I've tried both. I keep coming back to raw rusqlite. Here's why.


The ORM appeal

ORMs promise type safety, migrations, and less SQL. On paper, compelling. In practice, for a solo dev building desktop apps, the tradeoffs don't work out.


What ORMs cost in Rust

Compile times. Diesel adds significant compile time through its macro-heavy approach. On an 8-year-old MacBook Air, this matters. Every saved minute of compile time is real productivity.

Learning curve. The ORM API is another thing to learn. rusqlite maps directly to SQL — if you know SQL, you know rusqlite.

Abstraction leaks. Complex queries eventually break ORM abstractions. You end up mixing ORM and raw SQL anyway. Better to just use raw SQL.


What raw rusqlite looks like

fn get_sync_records(
    conn: &Connection,
    device_id: &str,
    limit: usize,
) -> Result<Vec<SyncRecord>, AppError> {
    let mut stmt = conn.prepare(
        "SELECT id, file_path, last_synced, file_hash
         FROM sync_records
         WHERE device_id = ?1
         ORDER BY last_synced DESC
         LIMIT ?2"
    )?;

    let records = stmt.query_map(params![device_id, limit as i64], |row| {
        Ok(SyncRecord {
            id: row.get(0)?,
            file_path: row.get(1)?,
            last_synced: row.get(2)?,
            file_hash: row.get(3)?,
        })
    })?
    .collect::<Result<Vec<_>, _>>()?;

    Ok(records)
}

Explicit, readable, no magic. The SQL is right there.


Migrations without a framework

For 3-4 schema versions across an app's lifetime, a simple version table works:

fn run_migrations(conn: &Connection) -> Result<(), AppError> {
    let version: i64 = conn.query_row(
        "SELECT version FROM schema_version",
        [],
        |r| r.get(0)
    ).unwrap_or(0);

    if version < 1 {
        conn.execute_batch("
            CREATE TABLE IF NOT EXISTS sync_records (...);
            UPDATE schema_version SET version = 1;
        ")?;
    }

    if version < 2 {
        conn.execute_batch("
            ALTER TABLE sync_records ADD COLUMN direction TEXT;
            UPDATE schema_version SET version = 2;
        ")?;
    }

    Ok(())
}

Not glamorous. Works perfectly for the scale of a solo desktop app.


When I'd use an ORM

Complex relational data with many joins, a team that prefers ORM conventions, or a project where compile time isn't a concern.

For a solo dev building Tauri desktop apps with simple data models: raw rusqlite is the right call.


TL;DR: For solo Tauri desktop apps, skip Diesel/SeaORM. Raw rusqlite means faster compile times, no learning curve beyond SQL itself, and no abstraction leaks on complex queries. A simple version table handles migrations at this scale.


If this was useful, a ❤️ helps more than you'd think — thanks!

HiyokoAutoSync | X → @hiyoyok