Typed IDs with SeaORM
TLDR: Check out sea-orm-typed-id for a solution to typed IDs in SeaORM.
After working with various ORMs across languages — ActiveRecord, SQLAlchemy, Diesel, Prisma — SeaORM has been a breath of fresh air. Most aspects of it just feel right. However, one thing I missed is the ability to have typed IDs for entities to avoid potential errors, like accidentally swapping IDs between functions.
Imagine a function that takes both a game_id and a player_id as arguments:
async fn find_player(game_id: i32, player_id: i32) -> Result<Player, DbErr> {
// ...
}
It’s easy to accidentally swap these two arguments:
find_player(game.id, player.id);
// or
find_player(player.id, game.id); // oops!
To avoid such errors, we can leverage Rust’s New Type Idiom to define strongly-typed IDs:
pub struct CakeId(i32);
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "cakes")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: CakeId,
}
However, using CakeId
directly as an ID type results in compilation errors due to missing trait implementations:
...
error: could not compile `entities` (lib) due to 32 previous errors
The error reveals several missing traits, including PartialEq
, Eq
, Clone
, Debug
, and specific SeaORM traits like TryFromU64
and FromValueTuple
.
Making it work
To address these errors, we need to implement the necessary traits for our CakeId
type:
#[derive(
Clone,
Debug,
Eq,
PartialEq,
sea_orm::DeriveValueType,
)]
pub struct CakeId(i32);
impl sea_orm::TryFromU64 for CakeId {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
Ok(CakeId(i32::try_from_u64(n)?))
}
}
We also probably want to handle conversions between CakeId
and primitive types like i32
(which allows us to write CakeId::from(123)
or 123.into()
):
impl From<CakeId> for i32 {
fn from(value: CakeId) -> Self {
value.0
}
}
impl From<&CakeId> for i32 {
fn from(value: &CakeId) -> Self {
value.0
}
}
impl From<i32> for CakeId {
fn from(value: i32) -> Self {
CakeId(value)
}
}
impl From<&i32> for CakeId {
fn from(value: &i32) -> Self {
CakeId(*value)
}
}
And to support serialization:
#[derive(
// ...
serde::Deserialize,
serde::Serialize,
)]
pub struct CakeId(i32);
// ...
impl From<CakeId> for sea_orm::JsonValue {
fn from(value: CakeId) -> Self {
sea_orm::JsonValue::Number(value.0.into())
}
}
Automating the Boilerplate
To avoid writing this boilerplate for every ID type, we can create a macro to generate the necessary code:
#[macro_export]
macro_rules! define_id {
($name:ident) => {
#[derive(
// ...
)]
#[repr(transparent)]
pub struct $name(i32);
// impl ...
};
}
Eventually, you may end up with a reusable solution like sea-orm-typed-id. You can use it as a crate or copy it into your project and adapt it to your needs.
Typed IDs help prevent bugs and improve code clarity — an invaluable addition when working with complex applications.