Implementing the FilmRepository trait
Cool, let's create a new file called postgres_film_repository.rs
in the film_repository
folder and add the new module to the mod.rs
file in the same folder. This time don't use the pub
keyword when declaring the module.
The idea is that we will re-export the implementation as if it was coming from the film_repository
module. This way, we can hide the implementation details from the rest of the application.
#![allow(unused)] fn main() { mod postgres_film_repository; }
Implementation
Let's open the recently created postgres_film_repository.rs
file and add the following code:
#![allow(unused)] fn main() { pub struct PostgresFilmRepository { pool: sqlx::PgPool, } }
Note that this is a simple struct that holds a sqlx::PgPool
instance. This is the connection pool we will use to connect to the database.
We don't need to expose it, hence the pub
keyword is not used.
Now, let's add a new
associated function to the struct that will make us easier to create new instances of this struct:
#![allow(unused)] fn main() { impl PostgresFilmRepository { pub fn new(pool: sqlx::PgPool) -> Self { Self { pool } } } }
This sort of constructor pattern is very common in Rust and the convention is to use new
as the name of the associated function.
Next, let's implement the FilmRepository
trait for this struct:
#![allow(unused)] fn main() { #[async_trait::async_trait] impl FilmRepository for PostgresFilmRepository { async fn get_films(&self) -> FilmResult<Vec<Film>> { sqlx::query_as::<_, Film>( r#" SELECT id, title, director, year, poster, created_at, updated_at FROM films "#, ) .fetch_all(&self.pool) .await .map_err(|e| e.to_string()) } async fn get_film(&self, film_id: &uuid::Uuid) -> FilmResult<Film> { sqlx::query_as::<_, Film>( r#" SELECT id, title, director, year, poster, created_at, updated_at FROM films WHERE id = $1 "#, ) .bind(film_id) .fetch_one(&self.pool) .await .map_err(|e| e.to_string()) } async fn create_film(&self, create_film: &CreateFilm) -> FilmResult<Film> { sqlx::query_as::<_, Film>( r#" INSERT INTO films (title, director, year, poster) VALUES ($1, $2, $3, $4) RETURNING id, title, director, year, poster, created_at, updated_at "#, ) .bind(&create_film.title) .bind(&create_film.director) .bind(create_film.year as i16) .bind(&create_film.poster) .fetch_one(&self.pool) .await .map_err(|e| e.to_string()) } async fn update_film(&self, film: &Film) -> FilmResult<Film> { sqlx::query_as::<_, Film>( r#" UPDATE films SET title = $2, director = $3, year = $4, poster = $5 WHERE id = $1 RETURNING id, title, director, year, poster, created_at, updated_at "#, ) .bind(film.id) .bind(&film.title) .bind(&film.director) .bind(film.year as i16) .bind(&film.poster) .fetch_one(&self.pool) .await .map_err(|e| e.to_string()) } async fn delete_film(&self, film_id: &uuid::Uuid) -> FilmResult<uuid::Uuid> { sqlx::query_scalar::<_, uuid::Uuid>( r#" DELETE FROM films WHERE id = $1 RETURNING id "#, ) .bind(film_id) .fetch_one(&self.pool) .await .map_err(|e| e.to_string()) } } }
Don't forget to add the necessary imports:
#![allow(unused)] fn main() { use super::{FilmRepository, FilmResult}; use shared::models::{CreateFilm, Film}; }
Note that this code won't compile. Don't worry for the moment, we will fix it in a moment.
Take the time to review the code. Unfortunately, going deep into the details of SQLx is out of the scope of this tutorial. However, if you are interested in learning more about it, you can check the SQLx documentation.
Fixing the compilation error
If you check the compiler error you will see that it is complaining about the Film
struct. It is telling us that it doesn't implement the FromRow trait.
This is because we are using the query_as method from SQLx, which requires that the struct implements the FromRow trait.
4 | pub struct Film {
| --------------- doesn't satisfy `Film: FromRow<'r, PgRow>`
|
= note: the following trait bounds were not satisfied:
`Film: FromRow<'r, PgRow>`
Let's fix this by implementing the FromRow trait for the Film
struct.
We must do this in the shared
crate, because the Film
struct is defined there.
Add the SQLx dependency to the Cargo.toml
file in the shared
crate:
# database
sqlx = { version = "0.7", default-features = false, features = [
"tls-native-tls",
"macros",
"postgres",
"uuid",
"chrono",
"json",
] }
And then add the sqlx::FromRow
trait into the derive
attribute of the Film
and CreateFilm
structs.
Now we will hit another compiler error. FromRow doesn't work with u16
.
Let's add a new annotation to the year
field in both structs:
#![allow(unused)] fn main() { #[sqlx(try_from = "i16")] }
This is how the models.rs file should look like
This is how the models.rs file should look like
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; #[derive( Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, sqlx::FromRow, )] pub struct Film { pub id: uuid::Uuid, // we will be using uuids as ids pub title: String, pub director: String, #[sqlx(try_from = "i16")] pub year: u16, // only positive numbers pub poster: String, // we will use the url of the poster here pub created_at: Option<chrono::DateTime<chrono::Utc>>, pub updated_at: Option<chrono::DateTime<chrono::Utc>>, } #[derive( Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, sqlx::FromRow, )] pub struct CreateFilm { pub title: String, pub director: String, #[sqlx(try_from = "i16")] pub year: u16, pub poster: String, } }
Supporting WebAssmembly
We are almost done. The code is compiling but we still need to do some changes to make this shared
crate work in the browser.
Our frontend will be compiled to WebAssembly, so we need to make sure that the shared
crate can be compiled to WebAssembly.
The problem that we will face is that SQLx doesn't support WebAssembly yet.
So, how to solve this? Enter Cargo Features.
We will compile certain parts of the code only when a certain feature
is enabled.
Note that this is how tests works. If you remember when we looked at that, each testing module is preceded by #[cfg(test)]
annotation. This means that the code inside that module will only be compiled when the test
feature is enabled.
Adding the backend
feature
The idea is that we will only use the FromRow trait when the backend
feature is enabled.
This should be true for all the backend code (the api-lib
crate) but not for the frontend code.
Let's add the backend
feature to the Cargo.toml
file in the shared
crate:
[features]
backend = ["sqlx"]
Then modify the sqlx
dependency to make it optional:
sqlx = { version = "0.6.3", default-features = false, features = [ "runtime-actix-native-tls", "macros", "postgres", "uuid", "chrono", "json" ], optional = true }
That's it. As the SQLx dependency is now optional, it will only be used in case the backend
feature is enabled.
Using the sqlx
feature
Modify the models.rs
file in the shared
crate to look like this:
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "backend", derive(sqlx::FromRow))] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] pub struct Film { pub id: uuid::Uuid, pub title: String, pub director: String, #[cfg_attr(feature = "backend", sqlx(try_from = "i16"))] pub year: u16, pub poster: String, pub created_at: Option<chrono::DateTime<chrono::Utc>>, pub updated_at: Option<chrono::DateTime<chrono::Utc>>, } #[cfg_attr(feature = "backend", derive(sqlx::FromRow))] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] pub struct CreateFilm { pub title: String, pub director: String, #[cfg_attr(feature = "backend", sqlx(try_from = "i16"))] pub year: u16, pub poster: String, } }
But... the code doesn't compile!
Sure, no problem. We need to add the backend
feature to the Cargo.toml
file in the api-lib
crate:
# shared
- shared = { path = "../../shared" }
+ shared = { path = "../../shared", features = ["backend"] }
We should be good by now but there's still a small detail to cover.
We want our PostgresFilmRepository
struct to be available so we need expose it.
Head to the mod.rs
file in api > lib > src > film_repository
and add the following line:
#![allow(unused)] fn main() { pub use postgres_film_repository::PostgresFilmRepository; }
Try to build a new module called memory_film_repository
that implements the FilmRepository
trait and uses an in-memory data structure to store the films.
You can also add tests to your implementation.
HINT: You can take a look at the workshop GitHub repository if you get stuck.
Plenty of work in this section. Check that everything compiles and commit your changes:
git add .
git commit -m "add postgres film repository"