Created
December 31, 2020 02:38
-
-
Save petersooley/9bb4d85b0114b53faa15cac7dc5b7f3c to your computer and use it in GitHub Desktop.
Hydrating values from a database with function overloading in Rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 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