Very interested in exploring how this will compare to Diesel [1] and SeaORM [2], the other two options in this space today. Joshua Mo at Shuttle did a comparison between Diesel and SeaORM in January of this year that was really interesting [3].
My first reaction is this feels like a nice middleground between Diesel and SeaORM.
The codegen part makes all columns and tables and stuff checked at compile-time (name and type) like Diesel, with a query builder that's more natural like SeaORM. I hope the query builder does not end up too magical like SQLAlchemy with its load of footguns, and stay close in spirit to Diesel that's "write sql in rust syntax".
I think time will tell, and for now I'm keeping my Diesel in production :D
Sea ORM is too opinionated in my experience. Even making migration is not trivial with their own DSL.
Diesel was ok, but I never use it anymore since rocket moved to async.
I'm mainly use sqlx, it's simple to use, there's query! and query_as! macro which is good enough for most of the case.
I use SQLx, but I'm not totally convinced it's better than writing raw SQL with the underlying postgres/sqlite3/mysql driver. The macros and typing fall apart as soon as you need anything more complicated than basic a SELECT with one to one relationships, much less one/many to many.
I remember fighting with handling enums in relations for a while, and now just default to manually mapping everything.
It's nice seeing more Django/Prisma style ORMs where the non-SQL source code is the source of truth for the schema and migrations are automatically generated.
Yes, plain sql is indeed the bees knees but there are good ORMs like django/ecto etc. that let you consider N+1 query issues ahead of time. Most ORMs these days have escape hatches anyway. Patience might be needed to keep it all tidy but they don't necessarily have to be a mess.
I don't get why to use an ORM in the first place. Just define a bunch of structs, run a query, map results to structs. It's a few lines of simple code. You're in control of everything (the SQL, the running, the mapping). It's transparent. With any ORM, you give away control and make everything more complex, only to make it slightly easier to run a query and map some results.
As said, they have cost me too much time and money already, moreso as other devs on the team(s) lent heavily into certain features and I had to rewrite a lot of code.
I wish the following three paragraphs were widely read and understood by all software developers, especially web developers:
> The common wisdom is to maximize productivity when performance is less critical. I agree with this position. When building a web application, performance is a secondary concern to productivity. So why are teams adopting Rust more often where performance is less critical? It is because once you learn Rust, you can be very productive.
> Productivity is complex and multifaceted. We can all agree that Rust's edit-compile-test cycle could be quicker. This friction is countered by fewer bugs, production issues, and a robust long-term maintenance story (Rust's borrow checker tends to incentivize more maintainable code). Additionally, because Rust can work well for many use cases, whether infrastructure-level server cases, higher-level web applications, or even in the client (browser via WASM and iOS, MacOS, Windows, etc. natively), Rust has an excellent code-reuse story. Internal libraries can be written once and reused in all of these contexts.
> So, while Rust might not be the most productive programming language for prototyping, it is very competitive for projects that will be around for years.
I'd add that a lot of the described advantages come from culture. For web applications manual memory management is 100% a friction instead of a relief. But the culture in Rust community in general, at least for the past ten years or so, is to encourage a coding style with inherently fewer bugs and more reusable, maintainable code, to the point of consistently want something to not happen if they weren't sure they got it right (one may argue that this is counter-production short-term).
It is this culture thing makes adopting Rust for web apps worthwhile - it counters the drawback of manual memory management.
If you hire an engineer already familiar with Rust you are sure you get someone who is sane. If you onboard someone with no Rust background you can be pretty sure that they are going to learn the right way (tm) to do everything, or fail to make any meaningful contribution, instead of becoming a -10x engineer.
If you work in a place with a healthy engineering culture, trains people well, with good infra, it doesn't really matter, you may as well use C++. But for us not so lucky, Rust helps a lot, and it is not about memory safety, at all.
I haven’t worked at a place that checks the above boxes for making C++ a great choice for bulletproof code. There seems to be large variation in C++ styles and quality across projects. But it seems to me that for orgs that indeed do C++ well, thanks to the supporting aspects above, moving to Rust might make things even smoother.
As time passes, the more I feel a minority in adoring rust, while detesting Async. I have attempted it a number of times, but it seems incompatible with my brain's idea of structure. Not asynchronous or concurrent programming, but Async/Await in rust. It appears that most of the networking libraries have committed to this path, and embedded it moving in its direction.
I bring this up because a main reason for my distaste is Async's incompatibility with non-Async. I also bring this up because lack of a Django or SQLAlchemy-style ORM is one reason I continue to write web applications in Python.
Async code is not incompatible with blocking one, in Rust it's quite straightforward to make the two interoperate: calling a blocking code from async is donne with spawn_blocking and the reverse (async from blocking code) is done with block_on.
> I bring this up because a main reason for my distaste is Async's incompatibility with non-Async. I also bring this up because lack of a Django or SQLAlchemy-style ORM is one reason I continue to write web applications in Python.
It is nice to see more ORMs, but inventing a new file format and language `toasty` isn't my cup of tea. I'd rather define the models in Rust and let the generator emit more Rust files.
Creating your own file format is always difficult. Now, you have to come up with syntax highlighting, refactoring support, go to definition, etc. When I prototype, I tend to rename a lot of my columns and move them around. That is when robust refactoring support, which the language's own LSP already provides, is beneficial, and this approach throws them all away.
My experience with Prisma, which has a very similar DSL for defining schemas, has changed my mind on this. Makes me much more productive when maintaining large schemas. I can make a one line change in the schema file and instantly have types, models, and up/down migrations generated and applied, and can be guaranteed correct. No issues with schema drift between different environments or type differences in my code vs db.
Prisma is popular enough it also has LSP and syntax highlighting widely available. For simple DSL this is actually very easy build. Excited to have something similar in Rust ecosystem.
I mostly agree with this, but the trouble is (probably) that proc-macros are heavy-handed, inflexible, and not great for compile times.
In this case, for example, it looks like the generated code needs global knowledge of related ORM types in the data model, and that just isn't supported by proc-macros. You could push some of that into the trait system, but it would be complex to the point where a custom DSL starts to look appealing.
Proc-macros also cannot be run "offline", i.e. you can't commit their output to version control. They run every time the compiler runs, slowing down `cargo check` and rust-analyzer.
You can absolutely do global knowledge in proc macros via the filesystem and commit their output to version control: https://github.com/trevyn/turbosql
Still WIP but made it past the hurdle of inserts, which I decided to generate a type-state builder pattern to enforce non-nullable fields and skip auto-fields.
This is more intended as a proof of concept but I’ll see how much I can grow it and whether I can dogfood at my job
For me diesel hits right balance since it is more a query builder and it is close to the SQL syntax. But sometimes it doesn't work because it is very strongly typed, right now I use sea-query for those scenarios and I built the bridge between the two.
The second that you would benefit from using a DBMS specific feature, the ORM begins getting in the way. It is highly unlikely that an ORM provides support, much less a good abstraction, over features that only 1/N supported DBMS have.
Your code ends up using the driver raw in these cases, so why not just use the driver for everything? Your codebase would be consistent at that point
>The second that you would benefit from using a DBMS specific feature, the ORM begins getting in the way.
You can extend diesel (and probably many other orms, Diesel is just particularly easy here) to support any db feature you want.
> It is highly unlikely that an ORM provides support, much less a good abstraction, over features that only 1/N supported DBMS have.
That depends on orm flexibility and popularity. It may not provide support OOTB, but can make it easy to add it.
> Your code ends up using the driver raw in these cases, so why not just use the driver for everything? Your codebase would be consistent at that point
Main point of using orm for me is that I have type verification, raw (as in text) breaks too easily.
I have found that ORM arguments in context don’t stick very well to Django’s ORM, but see the argument applying well to most all the others.
Case in point Django is really good about DB-specific functionality and letting you easily add in extension-specific stuff. They treat “you can only do this with raw” more or less as an ORM design API issue.
My biggest critique of Django’s ORM is its grouping and select clause behavior can be pretty magical, but I’ve never been able to find a good API improvement to tackle that.
Because you only need the specific features in a tiny amount of cases, while 99% is some flavour of SELECT * ... LEFT JOIN ... (If it's not, then sure, ORM would be annoying)
Making that 99% smaller, simpler and automatically mapping to common types makes development a lot easier/faster. This applies to pretty much any higher level language. It's why you can write in C, but embed an ASM fragment for that one very specific thing instead of going 100% with either one.
You’re probably making so much money that don’t care about your Database bill or query performance.
ORM is basically a no-code tool for databases, if that solves your problem great, but that’s not something that would scale beyond basic use.
Has it benefited you? Have you moved to a different underlying SQL software without having to make any changes to your codebase? Or some other benefit?
For me it’s speed of development. I’m frankly not very good at SQL, but an ORM in a familiar syntax to the language I use most (Typescript) increases my dev speed tremendously.
I also have a relatively successful saas that uses Prisma and it’s been phenomenal. Queries are more than fast enough for my use case and it allows me to just focus on writing more difficult business logic than dealing with complex joins
[1]: https://diesel.rs/
[2]: https://www.sea-ql.org/SeaORM/
[3]: https://www.shuttle.dev/blog/2024/01/16/best-orm-rust
The codegen part makes all columns and tables and stuff checked at compile-time (name and type) like Diesel, with a query builder that's more natural like SeaORM. I hope the query builder does not end up too magical like SQLAlchemy with its load of footguns, and stay close in spirit to Diesel that's "write sql in rust syntax".
I think time will tell, and for now I'm keeping my Diesel in production :D
I'm mainly use sqlx, it's simple to use, there's query! and query_as! macro which is good enough for most of the case.
I remember fighting with handling enums in relations for a while, and now just default to manually mapping everything.
Sooner or later we always hit the n+1 query problem which could only be resolved by a query builder or just plain old sql.
It always was a mess and these days I can't be bothered to try it even anymore because it has cost me a lot of hours and money.
On the other hand an async orm sounds like (n+1)(n+2)+...+(n+m) Problem
> The common wisdom is to maximize productivity when performance is less critical. I agree with this position. When building a web application, performance is a secondary concern to productivity. So why are teams adopting Rust more often where performance is less critical? It is because once you learn Rust, you can be very productive.
> Productivity is complex and multifaceted. We can all agree that Rust's edit-compile-test cycle could be quicker. This friction is countered by fewer bugs, production issues, and a robust long-term maintenance story (Rust's borrow checker tends to incentivize more maintainable code). Additionally, because Rust can work well for many use cases, whether infrastructure-level server cases, higher-level web applications, or even in the client (browser via WASM and iOS, MacOS, Windows, etc. natively), Rust has an excellent code-reuse story. Internal libraries can be written once and reused in all of these contexts.
> So, while Rust might not be the most productive programming language for prototyping, it is very competitive for projects that will be around for years.
It is this culture thing makes adopting Rust for web apps worthwhile - it counters the drawback of manual memory management.
If you hire an engineer already familiar with Rust you are sure you get someone who is sane. If you onboard someone with no Rust background you can be pretty sure that they are going to learn the right way (tm) to do everything, or fail to make any meaningful contribution, instead of becoming a -10x engineer.
If you work in a place with a healthy engineering culture, trains people well, with good infra, it doesn't really matter, you may as well use C++. But for us not so lucky, Rust helps a lot, and it is not about memory safety, at all.
As time passes, the more I feel a minority in adoring rust, while detesting Async. I have attempted it a number of times, but it seems incompatible with my brain's idea of structure. Not asynchronous or concurrent programming, but Async/Await in rust. It appears that most of the networking libraries have committed to this path, and embedded it moving in its direction.
I bring this up because a main reason for my distaste is Async's incompatibility with non-Async. I also bring this up because lack of a Django or SQLAlchemy-style ORM is one reason I continue to write web applications in Python.
So you use gevent/greenlet?
Creating your own file format is always difficult. Now, you have to come up with syntax highlighting, refactoring support, go to definition, etc. When I prototype, I tend to rename a lot of my columns and move them around. That is when robust refactoring support, which the language's own LSP already provides, is beneficial, and this approach throws them all away.
Prisma is popular enough it also has LSP and syntax highlighting widely available. For simple DSL this is actually very easy build. Excited to have something similar in Rust ecosystem.
In this case, for example, it looks like the generated code needs global knowledge of related ORM types in the data model, and that just isn't supported by proc-macros. You could push some of that into the trait system, but it would be complex to the point where a custom DSL starts to look appealing.
Proc-macros also cannot be run "offline", i.e. you can't commit their output to version control. They run every time the compiler runs, slowing down `cargo check` and rust-analyzer.
I have the afternoons of my past week trialling to see if you could achieve something similar to Toasty with just structs and proc macros.
https://github.com/jayy-lmao/sql-db-set-macros
Still WIP but made it past the hurdle of inserts, which I decided to generate a type-state builder pattern to enforce non-nullable fields and skip auto-fields. This is more intended as a proof of concept but I’ll see how much I can grow it and whether I can dogfood at my job
Deleted Comment
Ideally I would use something akin to Go Jet.
Great to see some development in this for Rust, perhaps after it becomes stable I may even switch my SaaS to it.
Your code ends up using the driver raw in these cases, so why not just use the driver for everything? Your codebase would be consistent at that point
You can extend diesel (and probably many other orms, Diesel is just particularly easy here) to support any db feature you want.
> It is highly unlikely that an ORM provides support, much less a good abstraction, over features that only 1/N supported DBMS have.
That depends on orm flexibility and popularity. It may not provide support OOTB, but can make it easy to add it.
> Your code ends up using the driver raw in these cases, so why not just use the driver for everything? Your codebase would be consistent at that point
Main point of using orm for me is that I have type verification, raw (as in text) breaks too easily.
Case in point Django is really good about DB-specific functionality and letting you easily add in extension-specific stuff. They treat “you can only do this with raw” more or less as an ORM design API issue.
My biggest critique of Django’s ORM is its grouping and select clause behavior can be pretty magical, but I’ve never been able to find a good API improvement to tackle that.
Making that 99% smaller, simpler and automatically mapping to common types makes development a lot easier/faster. This applies to pretty much any higher level language. It's why you can write in C, but embed an ASM fragment for that one very specific thing instead of going 100% with either one.
I also have a relatively successful saas that uses Prisma and it’s been phenomenal. Queries are more than fast enough for my use case and it allows me to just focus on writing more difficult business logic than dealing with complex joins