Skip to content

Commit 7ce3d3d

Browse files
authored
add blog post for Toasty (#787)
1 parent cf05c71 commit 7ce3d3d

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
---
2+
date: "2024-10-23"
3+
title: "Announcing Toasty, an async ORM for Rust"
4+
description: "October 23, 2024"
5+
---
6+
7+
[Toasty](https://github.com/tokio-rs/toasty) is an asynchronous ORM for the Rust
8+
programming language that prioritizes ease of use. It is currently in the early
9+
stages of development and should be considered a "preview" (not ready for
10+
real-world usage yet). Toasty supports SQL and NoSQL databases, including
11+
DynamoDB and Cassandra (soon).
12+
13+
Projects that use Toasty start by creating a schema file to define the
14+
application's data model. For example, this is the contents of the
15+
[`hello-toasty/schema.toasty`](https://github.com/tokio-rs/toasty/blob/main/examples/hello-toasty/schema.toasty)
16+
file.
17+
18+
```rust
19+
model User {
20+
#[key]
21+
#[auto]
22+
id: Id,
23+
24+
name: String,
25+
26+
#[unique]
27+
email: String,
28+
29+
todos: [Todo],
30+
31+
moto: Option<String>,
32+
}
33+
34+
model Todo {
35+
#[key]
36+
#[auto]
37+
id: Id,
38+
39+
#[index]
40+
user_id: Id<User>,
41+
42+
#[relation(key = user_id, references = id)]
43+
user: User,
44+
45+
title: String,
46+
}
47+
```
48+
49+
Using the Toasty CLI tool, you will generate all necessary Rust code for working with this data model. The generated code for the above schema is here.
50+
51+
Then, you can easily work with the data model:
52+
53+
```rust
54+
// Create a new user and give them some todos.
55+
User::create()
56+
.name("John Doe")
57+
58+
.todo(Todo::create().title("Make pizza"))
59+
.todo(Todo::create().title("Finish Toasty"))
60+
.todo(Todo::create().title("Sleep"))
61+
.exec(&db)
62+
.await?;
63+
64+
// Load the user from the database
65+
let user = User::find_by_email("[email protected]").get(&db).await?
66+
67+
// Load and iterate the user's todos
68+
let mut todos = user.todos().all(&db).await.unwrap();
69+
70+
while let Some(todo) = todos.next().await {
71+
let todo = todo.unwrap();
72+
println!("{:#?}", todo);
73+
}
74+
```
75+
76+
# Why an ORM?
77+
78+
Historically, Rust has been positioned as a systems-level programming language.
79+
On the server side, Rust has grown fastest for use cases like databases,
80+
proxies, and other infrastructure-level applications. Yet, when talking with
81+
teams that have adopted Rust for these infrastructure-level use cases, it isn't
82+
uncommon to hear that they start using Rust more often for higher-level use
83+
cases, such as more traditional web applications.
84+
85+
The common wisdom is to maximize productivity when performance is less critical.
86+
I agree with this position. When building a web application, performance is a
87+
secondary concern to productivity. So why are teams adopting Rust more often
88+
where performance is less critical? It is because once you learn Rust, you can
89+
be very productive.
90+
91+
Productivity is complex and multifaceted. No one would disagree that Rust's
92+
edit-compile-test cycle could be quicker. This friction is countered by fewer
93+
bugs, production issues, and a robust long-term maintenance story (Rust's borrow
94+
checker tends to incentivize more maintainable code). Additionally, because Rust
95+
can work well for many use cases, whether infrastructure-level server cases,
96+
higher-level web applications, or even in the client (browser via WASM and iOS,
97+
MacOS, Windows, etc. natively), Rust has an excellent code-reuse story. Internal
98+
libraries can be written once and reused in all of these contexts.
99+
100+
So, while Rust might not be the most productive programming language for
101+
prototyping, it is very competitive for projects that will be around for years.
102+
103+
Okay, so why an ORM? A full-featured library ecosystem for the given use case is
104+
a big piece of the productivity puzzle. Rust has a vibrant ecosystem but has
105+
historically focused more on that infrastructure-level use case. Fewer libraries
106+
target the higher-level web application use case (though, as of recently, that
107+
is changing). Also, many of the libraries that do exist today emphasize APIs
108+
that maximize performance at the expense of ease of use. There is a gap in
109+
Rust's ecosystem. Many teams I spoke with reported that the current state of
110+
Rust's ORM libraries is a big friction point (more than one opted to implement
111+
their in-house database abstraction to deal with this friction). Toasty aims to
112+
fill some of that gap by focusing on that higher-level use case and prioritizing
113+
ease of use over maximizing performance.
114+
115+
# What makes an ORM easy to use?
116+
117+
Of course, this is the million-dollar question. The Rust community is still
118+
figuring out how to design libraries for ease of use. Rust's traits and
119+
lifetimes are compelling, can increase performance, and enable interesting
120+
patterns (e.g., the [typestate](https://cliffle.com/blog/rust-typestate/)
121+
pattern). However, overusing these capabilities also leads to libraries that are
122+
hard to use.
123+
124+
So, when building Toasty, I tried to be sensitive to this and focused on using
125+
traits and lifetimes minimally. This snippet is from code generated from the
126+
schema file by Toasty, and I expect this to be the most complicated type
127+
signature that 95% of Toasty users encounter.
128+
129+
```rust
130+
pub fn find_by_email<'a>(
131+
email: impl stmt::IntoExpr<'a, String>
132+
) -> FindByEmail<'a> {
133+
134+
let expr = User::EMAIL.eq(email);
135+
let query = Query::from_expr(expr);
136+
FindByEmail { query }
137+
}
138+
```
139+
140+
This does include a lifetime to avoid copying data into the query builder, and I
141+
am still on the fence about it. Based on user feedback, I might remove lifetimes
142+
entirely in the future.
143+
144+
Another aspect of ease of use is minimizing boilerplate. Rust already has a
145+
killer feature for this: procedural macros. Most of you have already used Serde,
146+
so you know what a delight this can be. That said, I opted not to use procedural
147+
macros for Toasty, at least not initially.
148+
149+
Procedural macros generate a lot of hidden code at build time. This isn't a big
150+
deal for libraries like Serde because the Serde macros generate implementations
151+
of public traits (Serialize and Deserialize). Users of Serde aren't really
152+
expected to know the implementation details of those traits.
153+
154+
Toasty is a different story. Toasty will generate many public methods and types
155+
that you will use directly. In the "Hello Toasty" example, Toasty generates the
156+
`User::find_by_email` method. Instead of a procedural macro, I used an explicit
157+
code generation step, where Toasty generates code to a file you can open and
158+
read. Toasty will try to keep this generated code as readable as possible to
159+
make discovering generated methods easy. This added discoverability will result
160+
in an easier-to-use library.
161+
162+
Toasty is still early in development, and the API will evolve based on your
163+
feedback. At the end of the day, if you hit friction, I want to hear about it
164+
and fix it.
165+
166+
# SQL and NoSQL
167+
168+
Toasty supports both SQL and NoSQL databases. As of today, that means Sqlite and
169+
DyanmoDB, though adding support for other SQL databases should be pretty
170+
straightforward. I also plan to add support for Cassandra soon, but I hope
171+
others will also contribute to implementations for different databases.
172+
173+
To be clear, Toasty works with both SQL and NoSQL databases but does **not**
174+
abstract away the target database. An application written with Toasty for a SQL
175+
database will not transparently run on a NoSQL database. Conversely, Toasty does
176+
not abstract away NoSQL databases, and you need to understand how to model your
177+
schema to take advantage of the target database. What I have noticed with
178+
database libraries is that most of each library does the same thing, regardless
179+
of the backend data store: mapping data to structs and issuing basic Get,
180+
Insert, and Update queries.
181+
182+
Toasty starts with this standard feature set and exposes database-specific
183+
features on an opt-in basis. It will also help you avoid issuing inefficient
184+
queries for your target database by being selective about the query methods it
185+
generates.
186+
187+
# Next steps
188+
189+
You should try Toasty, try the examples, and play around with it. Today, Toasty
190+
is still in active development and not ready for real-world use. The immediate
191+
next step will be to fill those gaps. I am aiming to get Toasty ready for
192+
real-world use sometime next year (realistically, towards the end of the year).
193+
194+
Additionally, trying to support SQL and NoSQL the way Toasty does is novel (as
195+
far as I know). If you know prior art, especially pitfalls that previous
196+
attempts have hit, I would love to hear about it. I also know many of you have
197+
strong opinions on working with databases, ORMs, etc., and I am looking forward
198+
to those discussions. There is a #toasty channel in the Tokio
199+
[Discord](https://discord.gg/tokio) for discussion. Also, feel free to create
200+
issues on the [Github repo](https://github.com/tokio-rs/toasty) to propose
201+
features or start a conversation about API design and direction.
202+
203+
<div style="text-align:right">&mdash; Carl Lerche (<a href="https://github.com/carllerche">@carllerche</a>)</div>

0 commit comments

Comments
 (0)