Skip to content

Instantly share code, notes, and snippets.

@petersooley
Created December 31, 2020 02:38
Show Gist options
  • Select an option

  • Save petersooley/9bb4d85b0114b53faa15cac7dc5b7f3c to your computer and use it in GitHub Desktop.

Select an option

Save petersooley/9bb4d85b0114b53faa15cac7dc5b7f3c to your computer and use it in GitHub Desktop.
Hydrating values from a database with function overloading in Rust
/// Getting values from a database often requires a transformation of raw
/// content into something suitable for your application. Using traits, I am
/// going to separate the low-level statements used to access database contents
/// and the transformation of that content into application values.
///
/// Rant: A common solution for this is to use an ORM which abstracts away
/// the database altogether. However, this has several downsides like not
/// being able to use the articulate and expressive language of the database
/// and also just dumbing down what you can do with the database.
/// Let's say our application needs to make some queries like these.
/// Note, you'll have more parameter data on these `struct`s for passing
/// through to the actual statements sent to the database.
struct AddThing; // execute and return T
struct FetchThing; // select one Option<T>
struct FetchThings; // select many Vec<T>
/// We can capture that behavior in a trait so that each statement more or
/// less looks the same and follows the same pattern for implementing it.
/// We need some type parameters because not every statement is going to
/// return the same thing. We've got `Raw` for the value from the database
/// and some `T` parameter which is helpful information about what kind
/// of application entity this statement is really going to provide in the end.
///
/// Note: associated types might be better, but our generics get a bit unweildy
/// later and I couldn't figure out how to get it to work with associated types.
trait Statement<Raw, T> {
fn exec(&self) -> Raw;
}
/// Let's also create some representation of a single record given to us by
/// the database. In most libraries, this is some elaborate dictionary of values.
/// For us, it just holds a number.
struct Row(usize);
/// Since a `Row` is normally a big complicated mess that we have to transform
/// into a nice entity, let's use a `Hydrator` trait to say that we know how
/// to do this transformation.
trait Hydrator {
fn hydrate(row: &Row) -> Self;
}
/// In our case, this hydration is really simple, but you'll be implementing
/// this for all your entities that you want to return from our database statements.
impl Hydrator for usize {
fn hydrate(row: &Row) -> usize {
row.0
}
}
/// Let's just implement these statements so we have an idea of how rows
/// and statements fit together.
/// The `AddThing` statement might execute something on the database and
/// return the new ID of that thing. Note we're using `usize` for both the
/// `Raw` and `T` type paramaters because we don't need any transformation.
impl Statement<usize, usize> for AddThing {
fn exec(&self) -> usize {
// pretend we're grabbing a single value from the database
// it will _not_ be transformed by `Runner`
19
}
}
/// The `FetchThing` statement is going to return an `Option<Row>` value.
/// We're also passing the `usize` type for the `T` type parameter. This
/// indicates that we want to transform the `Option<Row>` result into
/// an `Option<usize>` return value. More on this later.
impl Statement<Option<Row>, usize> for FetchThing {
fn exec(&self) -> Option<Row> {
// pretend we got a single row from the database,
// the `Row` will be transformed by `Runner`
Some(Row(23))
}
}
/// `FetchThings` is similar to `FetchThing` but instead of returning an
/// on `Option<Row>` it returns `Vec<Row>`. This will give us a transformation
/// that returns `Vec<usize>`.
impl Statement<Vec<Row>, usize> for FetchThings {
fn exec(&self) -> Vec<Row> {
// pretend we got a bunch fo rows from the database
// the `Row`s will be transformed by `Runner`
vec![Row(31), Row(37), Row(41)]
}
}
/// Now that we have our statements defined, let's provide a single object
/// that we can pass all of our statements to for executing them.
struct Runner;
/// We want our `Runner` to operate the same for each statement regardless of
/// some of the different types the statements are returning with an API
/// like: `runner.run(&stmt)`. This is basically function overloading, but in
/// Rust, we do that with traits. This trait has four types: the `Statement`, and
/// it's two parameters, `Raw` and `T` that we just pass through and then a new
/// type parameter `Ret` which is the transformed value that we're going to
/// bring to the table.
///
/// We are going to implement this trait on our Runner for three scenarios (and
/// no others). You can see why `Ret` is a different type than `T`:
/// - `T` -> `T`
/// - `Option<Row>` -> `Option<T>`
/// - `Vec<Row>` -> `Vec<T>`
///
/// One cool thing about this complexity is that our API is going to be
/// transparent to the parts of our application that are actually calling
/// invoking these statements. There won't need to be a big fuss over type
/// parameters at that point.
trait Executor<S: Statement<Raw, T>, Raw, T, Ret> {
fn run(&self, stmt: &S) -> Ret;
}
/// In the most basic case, there is no transformation and we can capture that
/// when `Raw` and `T` are the same types. So `Raw` all around! This is what
/// gets called for our `AddThing` statement.
impl<S: Statement<Raw, Raw>, Raw> Executor<S, Raw, Raw, Raw> for Runner {
fn run(&self, stmt: &S) -> Raw {
stmt.exec()
}
}
/// Now we are going to handle the case where the `Raw` value from the `Statement`
/// is an `Option<Row>` that we need to transform to an `Option<T>`. Pay
/// careful attention to the order of the parameter types here. We are replacing
/// `Raw` with a concrete type `Option<Row>` and then expressing that `Ret` is
/// going to now be `Option<T>`. We are also adding a bounds to `T` that
/// requires that `Hydrator` must be implemented for it otherwise we couldn't
/// transform the `Row` into a `T`.
impl<S: Statement<Option<Row>, T>, T> Executor<S, Option<Row>, T, Option<T>> for Runner
where
T: Hydrator,
{
fn run(&self, stmt: &S) -> Option<T> {
let row = stmt.exec()?;
Some(T::hydrate(&row))
}
}
/// This is more of the same. Instead of using `Option<Row>` we are using
/// `Vec<Row>` and doing the transformation through an iterator.
impl<S: Statement<Vec<Row>, T>, T> Executor<S, Vec<Row>, T, Vec<T>,> for Runner
where
T: Hydrator,
{
fn run(&self, stmt: &S) -> Vec<T> {
stmt.exec()
.into_iter()
.map(|row| T::hydrate(&row))
.collect()
}
}
/// Working with the database is now a matter of passing the `Statements`
/// to the `Runner` and you'll get back the application types you're needing
/// in this moment while the underlying transformation is handled in isolation.
fn main() {
println!("thing added {}", Runner.run(&AddThing));
if let Some(val) = Runner.run(&FetchThing) {
println!("the fetched thing {}", val);
}
for num in Runner.run(&FetchThings) {
println!("a fetched thing {}", num);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment