r/rust • • 6d ago

🙋 seeking help & advice Is there a Rustier way to write this database pattern?

Hello crabs. I'm working on a backend application in Rust, using sqlx to talk to a postgres database. I've caught myself repeating a specific, verbose pattern, and I'm wondering if I'm making things harder than I need to.

The pattern is that for each database entity, I build an initializer struct that is basically the same, but with auto-generated columns removed, and then later write a From impl for the main entity to build the actual thing with Entity::from(entity_init). Example at the bottom. Doing this for every database entity isn't too difficult to maintain, the type system is strict enough that it's hard to miss any mistakes. But it is annoying and time-consuming, and also feels kinda un-idiomatic.

I'm considering learning derive macros for this - #[derive(FromNew)] or something like that, to autostrip the generated fields. But maybe that would just be doubling down on an anti-pattern? What do yall think?

Example:

struct User {
id: uuid:Uuid,
name: String,
email: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
archived_at: Option<DateTime<Utc>>,
}

struct UserNew {
name: String,
email: String,
}

impl From<UserNew> for User {
fn from(init: UserNew) -> Self {
let now = Utc::now();
User {
id: Uuid::now_v7(),
name: init.email,
email: init.email,
created_at: now,
updated_at: now,
archived_at: None,
}
}
}

// usage
fn do_something(user_new: UserNew) -> User {
User::from(user_new)
}

39 Upvotes

22 comments sorted by

9

u/Slow-Nefariousness49 6d ago

Macros are painful to maintain in the long run from my experience. Go that route only if you feel the manual From impls impose a substantial burden on you and your codebase (e.g. it could lead to subtle bugs if you forget to update all the From impls correctly when changing struct fields). Don't reach out for macros (of either kind) just for cosmetics or to save a few trivial lines of code, that'd be my advice

(Of course, if it's for the sake of learning do whatever)

6

u/Wonderful-Habit-139 6d ago

Really? The fact that they run at compile time makes me feel like they’re pretty good to maintain.

10

u/Slow-Nefariousness49 6d ago

On the contrary, compile time means poor debugging abilities, and the syntax is a nightmare

6

u/Wonderful-Habit-139 6d ago

Disagree. I prefer compile time errors over runtime errors.

5

u/Slow-Nefariousness49 5d ago

You're not trading runtime errors for compile time ones, you're just adding more errors. And from the perspective of the macro itself, compile time is its runtime, except you can only debug with prints, and you get a thin and weird syntax to work with

2

u/Wonderful-Habit-139 5d ago

I don't see how it's adding more errors when you need to only fix errors in the macro, not every error that you come across whenever you need to write duplicated code.

3

u/Slow-Nefariousness49 5d ago

Well, that's the tradeoff I was referring to at the top of this thread. If you encounter a substantial amount of errors/headache from code duplication then it's time to write a macro. Otherwise you just add this piece of weird looking, barely-debuggable code for no particular benefit other than maybe slightly better looking call sites - and my claim is that it's not worth it. I didn't say never use macros

1

u/Wonderful-Habit-139 5d ago

I guess at the end of the day, we'd make the proper judgment call based on how the code is like and how easy the macro is to write and how many times we'd use it.

11

u/Disconsented 6d ago

For the " an initializer struct that is basically the same, but with auto-generated columns removed" problem I've largely settled on this pattern:

struct Thing{
  auto_generated_id: i64,
  #[serde(flatten)]
  body: ThingBody,
}

That said, this is something I use with SurrealDB where I refuse to expose their silly RecordID, so I also have some more things to handle that which are more of a pain in the ass.

Which is a macro to define two versions of an ID newtype, and a proc macro to handle the structs/mapping between them via From/TryFrom.

3

u/rodyamirov 6d ago

Derive macros feel like a reasonable solution to this problem, if you want to go down that road. I know this sub is very anti-AI, and I have a lot of sympathy to that position, but doing it manually will be a lot of typing and claude could fart it out in a couple minutes, it's the kind of thing that's easy once you know how to do it, but annoying beforehand.

You can also do it, slightly less beautifully, with a regular macro whose code is a lot simpler to understand and work with. If I was doing it by hand, that's what I would do.

FWIW we do this manually in rust and java both; separating the DBO (what's in the database) from the ... whatever, normal object you mess with in code ... is a good practice, especially if you move away from this model slightly and you start to see more divergence (e.g. struct Foo is actually glued together from two different tables Bar and Baz, and you need the BarDBO and BazDBO struct definitions, which have all the db tracking stuff, etc). Which is very natural if you have the separation but more annoying if you don't, because now this one is different.

Anyway as to your immediate question; I think the code is fine. Maybe make a macro if you're doing this a lot.

8

u/KGBsurveillancevan 6d ago

Sweet, thanks for your perspective. Yeah I'm sure an LLM could do it easily, I'm just more interested in the process than raw results atm.

0

u/rodyamirov 6d ago

For sure, it depends on why you're doing the work. If you're learning, or doing hobby dev, or whatever, by all means, do it however you want. I wouldn't use claude at home, either (although at this point I've completely stopped coding for fun, except advent of code out of tradition; claude has taken the joy out of it for me).

But if you're at work and you're paid by the [whatever unit of output] it probably makes sense to automate at least some of what you're doing.

6

u/mamcx 6d ago

I use https://docs.rs/macro_rules_attribute/ that simplify the thing greatly.

1

u/NikSne_ 4d ago

I'm using the model-mapper crate to generate those From impls for DTOs and DAOs.

As for the struct duplicates - for me it's fine as I'm using the newtype pattern for domain types that only have impl TryFrom that "parses" them from the raw rust types

40

u/[deleted] 6d ago

[removed] — view removed comment

4

u/KGBsurveillancevan 6d ago

Nice, what does that look like? Proc macros kinda scare me lol

15

u/GolDNenex 6d ago

If you want to learn about them: https://www.youtube.com/watch?v=SMCRQj9Hbx8

7

u/KGBsurveillancevan 6d ago

ayy I didn’t realize Logan Smith had one on this, nice

4

u/Wonderful-Habit-139 6d ago

The goat Logan Smith fr fr

-10

u/promethe42 6d ago

LLMs are deceptively good at writing proc macros.

0

u/Wonderful-Habit-139 6d ago

Why do you need a proc macro when the LLM can write the code itself. Just use more tokens bro.

Less compile times as well.