Blog post: RESTful API in Sync & Async Rust

I wrote this tutorial to show how to implement a RESTful API server in sync and async Rust! The sync implementation uses Diesel and Rocket and the async implementation uses sqlx and actix-web. Perfect for anyone looking to get into backend development in Rust!

Please let me know if anything is unclear, confusing, or inaccurate! Your feedback is very important and helps me improve the tutorial. Thanks!

6 Likes

Comments:

  • I would probably do my best to split it up such that the sync and async version is on two separate pages.
  • Your custom Deserialize impl with a visitor can be vastly simplified like this.
  • Typically I would recommend tracing as the logging library in async applications.
3 Likes

Nice article. One thing I noticed:

I don't think you mentioned the compile-time verification feature of sqlx, which is (in my opinion) a very unique and powerful feature. Otherwise I would not be confident enough to write SQL queries all the time.

Maybe you could use it or mention it somewhere.

2 Likes

As one of the maintainers of diesel I think I should clarify a few points here:

However, if we currently try to run cargo check we'd get this:

error[E0412]: cannot find type `Status_enum` in this scope
 --> src/schema.rs:14:19
  |
14 |         status -> Status_enum,
  |                   ^^^^^^^^^^^ not found in this scope

Uhh, what?

I would like to know what exactly you expect diesel to do if it encounters a user defined type. I mean we cannot know about them beforehand, so there is no way to provide a default implementation for them.

Unfortunately Diesel is kinda lame and does not support mapping DB enums to Rust enums out-of-the-box, so we have to pull in an unofficial 3rd-party library to do this for us:

First of all I find this language offensive. Also this statement is false. I mean if diesel wouldn't support mapping DB enums to Rust enums how would a third party library be able to support this? The only thing this library does is to provide a "nice" opinionated derive that simplifies one use case. This may fit with your needs or not, as it makes a few assumptions. The general rule for type mappings provided by diesel itself is that there must be exactly one canonical solution for this mapping. This is just not the case for mapping rust enums to database entries.

So it seems like we can craft almost every query our server needs to support using Diesel's DSL. Almost. One of the queries we'd like to support is returning a "board summary" which is the count of all the cards within a board grouped by their status. This is how we'd write the SQL query:

SELECT count(*), status
FROM cards
WHERE cards.board_id = $1
GROUP BY status;

Can we use Diesel's DSL to craft this query? Nope! Diesel, like all ORMs, is a leaky abstraction over SQL that only supports a tiny fraction of SQL's power, flexibility, and elegance.

I get it that you don't like ORM's and prefer to write SQL on your own. That's totally fine, but you should not present that as "objective" point then. Technically this statement is false. Diesel does provide support for commonly used parts of SQL out of the box. This query is kind of an exception here, as only the master branch supports it out of the box. This does not mean that it is not possible to express such queries using diesels query builder, as it is possible to provide arbitrary extensions as third party crate or even implement your own custom dsl if you don't like that one provided by diesel.

Why do structs which are the result of "regular" Diesel queries have to derive diesel::Queryable but structs which are the result of diesel::sql_query queries have to derive diesel::QueryableByName? It's a mystery!

That's explained in the documentation of the corresponding methods (which are linked from those traits). So calling it a "mystery" is just wrong.

I personally find it misleading that you explicitly outline this feature of sqlx as you don't mention guarantees provided by diesel with any word. Diesel's query dsl also provides guarantees in cases where this is not possible for any of the sqlx macros, like conditional queries, left joins, variable amount of bind parameters, ….

The DR server totally bombed. I have no clue how it's that slow. Yeah, it barely used any CPU and it barely used any memory but that's also because it barely processed any requests. It's also the only server that failed to process some requests! These numbers are just really, really bad. Not much else to say here.

Stating that you don't have any clue what makes this benchmark slow is a plain lie. Please clarify that this is entirely a rocket issue. For anyone else looking into this here are benchmarks results of diesel + sqlx for various backends. Have a look there, at least for me the results show quite clear that sqlx is less performant that diesel for almost tested workloads with quite a huge margin in most cases. You find the corresponding code here. If you believe diesel is the cause for this "slowness" your are in invited to submit a new benchmark there showcasing a slow operation. The only rule there is that the benchmark needs to work with the database crate itself, not with a web framework on top of it (as you would measure the performance of the web framework otherwise).

I'm gonna be upfront about my biases here: I love SQL and I hate ORMs.

In my opinion, SQL is already an amazing abstraction. It's high-level, elegant, declarative, flexible, composable, intuitive, and powerful. ORMs which attempt to abstract over SQL rarely capture all of these qualities, and usually the result is an underpowered, inflexible, leaky API that gives users the ability to perform only a small fraction of the queries that they could more easily and concisely express in SQL.

That's basically the starting point of your the comparison of the two crates through the whole article. It's totally fine not to like ORM's, but either your shouldn't let your bias influence the whole article or your should put that on top of the article and mark the corresponding parts as opinion.

Where I think Diesel v1.4 could improve:

Suggesting improvements is welcome at any time. Could you please clarify what exactly you expect for the following points. At least for me it's not clear what's your expecting here.

  • More docs, why are associations not covered in any guide?

We are happy to receive your contribution here. Otherwise any guide needs someone that writes the guide and diesel seeks for more maintains for quite a while yet.

  • More logging, if I set the log level to TRACE I see nothing from Diesel.

This is expected, we do not provide any logging at all, as there is no performant standard way to solve this. There are third party crates which provide custom connection implementation for some of the logging solutions in the ecosystem. Also you can provide you own logging connection implementation that does whatever you want.

  • Why do I need to find and download a separate unofficial 3rd-party crate to handle mapping DB enums to Rust enums?

Could you clarify what exactly you expect here? Please keep in mind that it needs to satisfy the rule that is must be the canonical way to provide this mappings.

  • Why so many macros? 11 inline macros and 6 derive macros is a lot but they don't exactly do a lot, except noticeably increase compile times. I feel like a lot of these could be consolidated.

Do you have numbers that bake the claim that does macros noticeably increase the compile time? Can you clarify which macros you consider duplicated?

  • Diesel overall is really underwhelming compared to other feature-rich and batteries-included ORMs available in other languages. I think calling Diesel an "ORM" is a bit generous, and falsely sets expectations. I've seen similar tools in other languages call themselves "micro ORMs" or "query builders" which I think would be much more appropriate descriptions for Diesel.

To be fair here diesel claims the following: "Diesel is a Safe, Extensible ORM and Query Builder for Rust" . I do not see how this implies a fully batteries-included ORM. That wouldn't fit in the rust ecosystem.

  • No support for async/await and it's not on the project's roadmap so it's not coming any time soon.

Stating that async/await is not on our roadmap is a plain lie. I see this as personal insult as this topic was discussed so many times. To clarify that again: async/await is on our road map, but the implementation is blocked on language level. Nobody on the corresponding rust team seem to care to fix the corresponding bug for the last 3 years or so. I'm personally not able to do that, as this seems to require major work on the compiler and on async/await itself. If you don't believe me it shouldn't be to hard for you to fix the transaction function in this playground, right?

1 Like

SQLx compile time verification feature is a nice thing on the first look, but it has some restrictions and problems which makes it harder to use in practice. I'm aware of at least the following problems:

  • It requires you to know the whole query at compile time. That's the case for most simple queries. As soon as you want to add conditional filters or a variable length bind list (for batch insert statements or IN expressions) you cannot use the compile time verification anymore as you don't have a complete query.
  • Due to the way the compile time verification is implemented it gets incomplete type information about joined tables. This means it fails to propagate the nullability of types of expressions coming from the right side of a left join.

Diesel's query ast provides similar guarantees for as SQLx compile time verification, but as part of the type system. This enables us to provide these guarantees also to the cases listed above. So claiming that these guarantees are unique to SQLx is a bit misleading in my opinion.

1 Like

What I meant with unique is that you can write raw SQL and have compile-time verification at the same time. The type information is (often enough) sufficient and you don't have to learn another ORM.

As far as I know this is neither the idea nor a part of any ORM. Which does not mean ORM are worse but just take a different approach here (with its own ups and downs).

1 Like

Just a heads up that Rocket 0.5 will support async.

1 Like

First of all thanks for the thorough and detailed critiques! Asking for and getting feedback can sometimes feel like pulling teeth and it's kinda rare to get substantive feedback.

I can understand my tone might have been a bit mean when discussing my gripes with Diesel, so I've updated the article to be more neutral and diplomatic.

This is true, but not in the way most users care about. I don't want something to be merely possible, I want it to be easy, and Diesel does not make it easy, even in the trivial case.

I can understand wanting to draw a firm line of where a library's responsibilities begin and end. With that said, have you considered doing something similar to Rocket? Rocket's main crate is rocket and contains all the core functionality of the framework but they also have an additional crate called rocket_contrib which contains all the nice-to-haves that users commonly ask for. I think something like an officially maintained diesel_contrib crate would be very nice.

Again, just because something is technically possible does not make it easy or convenient, and I want it to be easy and convenient. Implementing an alternative DSL on top of Diesel, despite being technically possible, is not something I have any time or interest in doing. Maybe I'd feel differently if it was well documented but it doesn't seem to be.

I read those docs before writing my article and they didn't make sense to me. Queryable deserializes rows by index and QueryableByName deserializes rows by name. Okay, but why? Why not just deserialize all rows by index and only have Queryable? Or why not just deserialize all rows by name and only have QueryableByName? It's still a mystery to me why both need to exist as part of Diesel's API. sqlx only has sqlx::FromRow and they seem to make it work with all kinds of queries... so why does Diesel need to export two different derive macros that have some subtle distinct that I, a user of the library, need to care about? The subtle distinction isn't even explained that well, like I said before I don't get why some rows have to be deserialized by index and others by name.

Given that Diesel generates a custom DSL from the DB schema I think it should be fairly obvious to readers that it's not possible to craft invalid queries using the DSL, so I didn't think it needed explicit mentioning. However, to be fair to Diesel, and to praise more of its strengths, I updated the article to mention that that Diesel's generated DSL guarantees at compile-time that all crafted queries are syntactically and semantically correct.

It's not at all a lie. Did you read my last comment in that issue thread? I honestly still have no clue why Rocket's performance is so bad, and I explain exactly why too.

I never threw Diesel under the bus or mentioned performance as a Diesel issue, but I can see how maybe some readers could get the impression that Diesel's performance may be poor given that it was used in the Rocket server which had terrible benchmark results. I think this request is fair so I've updated the article to clarify that the poor performance of the DR server is caused entirely by Rocket.

To be more fair to Diesel I added those links to my article.

That's fair, as mentioned above I re-phrased the sections that were critical of Diesel to be more unbiased and neutral, and saved my personal opinions for the end of the article.

Sure, but what about logging that's enabled just for debug builds for local development?

I was not aware of this, but this seems like another great thing that could go into a diesel_contrib crate so it's more easily discoverable.

Not sure what you mean when you say it has to be the canonical way. If a canonical way is not truly possible it'd still be very nice and convenient if something like the diesel-derive-enum was easier to find, and officially supported by the Diesel team, like maybe... if you had a diesel_contrib crate :slight_smile:

Unfortunately I do not have any hard numbers, but I can tell you that the DR server, for whatever reason, consistently took 5x-10x longer to build than the SA server on my machine, but again this might be entirely Rocket's fault and not Diesel.

But while we're on the topic of Diesel macros, I still don't understand why both Queryable and QueryableByName need to exist. Also, why do any of the macros used in the generated schema file, e.g. table!, joinable!, etc, have to exist? If the generated schema is never suppose to be edited by hand then why does it use macros at all, why not just output the code that the macros would output in the first place?

That is fair, however, Diesel is the premiere ORM library for Rust, so naturally people coming from other languages are going to compare it the premiere ORMs they're familiar with from those ecosystems, like ActiveRecord for Ruby, or Eloquent for PHP, or Hibernate for Java, and so on, and against those Diesel doesn't measure up. This is not Diesel's fault of course, this is more so just a consequence of Rust's relatively young ecosystem, but it is what it is. I would drop the "ORM" part from the tagline and change it to just "Diesel is a Safe, Extensible Query Builder for Rust."

Is it though? I read through most of the async/await related issues in the Diesel repo and my takeaway from those was that it's not on Diesel's roadmap.

I think we just disagree on what it means for a feature to be "on the roadmap". In my opinion, a feature is only on the roadmap if the people who are managing the roadmap have the means and resources to deliver that feature without any hard external dependencies or hard blockers. In this case, like you said, Diesel is hard blocked from implementing async/await by a compiler bug that nobody has the interest, motivation, and experience to fix. The bug has been around for 3 years and it can easily be here for 3 more, so how can you tell users that async/await support is on the Diesel roadmap when you can't even hazard a reasonable guess as to when it will be implemented and shipped? Again, this is my opinion, but all roadmap items should have some kind of reasonable schedule, timeline, or due date associated with them, otherwise the roadmap is not a roadmap, it's just a wishlist.

I updated my article to remove any mention of a roadmap, but I still state that I don't think async/await is coming to Diesel any time soon, since that still seems to be the case.

1 Like

Thank for updating the blog post. It reads much more balanced now. :+1:

Having a diesel_contrib crate is a nice idea in principle, but this would not solve the underlying issue why we don't include such things in the main crate. The truth is that there are just not enough maintainers. So to keep up with all the work we need to exclude as much work as possible from the main crate. I can understand that users would prefer to get a one stop solution, but I cannot see how this is possible, at least not without additional people spending time to contribute maintain the existing code.

There is a guide about this on our web page.

I don't feel that explaining the differences of indexing rows by index or field name is a topic for our documentation, but for the sake of completes here is an explanation. In general indexing by index is more performant that indexing by name, as this requires an additional lookup. On the other hand it's normally safer to index by name, instead of by index as you easily could mix up the indices. Diesels query dsl has enough information to make using the index safer. This is cannot be assumed for raw sql queries, as we don't know anything about them.


This benchmark shows quite good how large a potential difference between those two methods could be. (Also see the postgres variants)

My problem with the old text was the phrase: "any clue" what's causing this. We already know that this is somehow related to how rocket use threads. Increasing the number of threads seems to help a bit, but not much. This issue gives another hint. At least for me it was not clear that you run the benchmarks on MacOS. It seems quite likely that this is caused by the way your operating system handles threads, as this stackoverflow answers suggests. At least for me this questions the whole benchmark, as one of the implementations is basically crippled by the operating system. That's not your fault, it just shows that good benchmarks are really hard. The async variant is not affected by this, because that one basically brings it's own user land scheduler + thread system.

Logging for a querybuilder / ORM is really tricky. At least for diesel most of the times the actual sql query is not even known, therefore we cannot simply print it without reconstructing it via the query builder (which takes time, …). That's because diesel uses prepared statements quite a lot. In fact we skip the query builder for statically know queries normally completely, by using a statically at compile time calculated type id as index in the prepared statement cache. Adding logging would mean that we somehow need to construct the query for each request. So just adding logging to existing connections is not an option. Having that as separate connection type seems to be possible, but would require someone to work on this.

It would be great to get concrete numbers here, without the influence of other crates.
See the comments above for why Queryable and QueryableByName are distinct traits. We could couple them in a single derive, but that would definitively increase compile time because we would generate much more code than required.
For table! and joinable!: Having an auto generated schema is not the only way to use diesel. For example I sometimes prefer to write the table! statements I actually need by hand. That would not be possible if that would require to write all the hundredths of lines generated code. For an auto generated schema I believe that the code should be readable. For table! this is definitively the case. You can look at the generated schema and easily see the corresponding tables there, with all important information in one place. Finding that all in the generated code would require to go through quite a lot of lines.

I think linking the actual rustc issue would make the new formulation even better. Otherwise I should probably explain a bit how I work on diesel. I use diesel for some personal and commercial projects. I normally work on features that I need personally or that someone pays me to work on, as I need to pay my rent… from something. Generally I feel that is quite rude to expect people to provide some super shine feature for free, because someone believes it fits nicely in their business use case. Async await is definitively a feature that fits in this pocket (at least for me). Especially because a service at the size of crates.io is doing fine with using diesel, that at least suggests that the a blocking database layer is normally no performance bottleneck. Additionally do I personally not require this feature, which means it's not that high on my priority list to work on, but that does not mean I will stop anyone to work on it. So to have a present you a "reasonable schedule, timeline or due" for this feature: It will be implemented as soon as

  • Someone contributes the code
  • Someone pays me or any other contributor to write that code
  • I run out of other important features to implement and find some time to fix all the unfinished edges in rust's async ecosystem
  • Someone motivates me by providing at least initial work + a really good motivating why this may actually be a good feature to implement.

So yes, from that point of view it could take some more years before that feature land, but on the other hand you are welcome to start changing things.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.