|
| 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">— Carl Lerche (<a href="https://github.com/carllerche">@carllerche</a>)</div> |
0 commit comments