Rust multi module microservices Part 3 - Database
Let's start with our first common module which will be fairly basic and provide a good base to start dealing with our database. Open the Cargo.toml inside the database crate(folder) and add the dependencies like the one below.
[package]
name = "database"
version = "0.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sea-orm = { workspace = true }
tracing = { workspace = true }
testcontainers = { workspace = true }
We are adding sea-orm and tracing which are needed for us to provide an implementation for creating a database connection and logging some information.
Now let's start with the implementation of our library in src/lib.rs. I have not followed all the best practices of proper file structuring for brevity but feel free to split it as you wish 😃
use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr};
use std::time::Duration;
use tracing::log;
pub async fn get_connection(database_url: &str) -> Result<DatabaseConnection, DbErr> {
let mut opt = ConnectOptions::new(database_url.to_owned());
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(10))
.acquire_timeout(Duration::from_secs(10))
.idle_timeout(Duration::from_secs(10))
.max_lifetime(Duration::from_secs(10))
.sqlx_logging(true)
.sqlx_logging_level(log::LevelFilter::Info);
return Database::connect(opt).await;
}
The code imports necessary dependencies such as
sea_orm,tracing, andstd::time::Duration.The function
get_connectionis defined, which takes adatabase_urlparameter as input and returns aResultcontaining aDatabaseConnectionor aDbErr(database error) if any occurs.Within the function, a
ConnectOptionsstruct is created and initialized with the provideddatabase_url.Various connection options are set using methods such as
max_connections,min_connections,connect_timeout,acquire_timeout,idle_timeout, andmax_lifetime. These options define parameters like the maximum and minimum number of connections, connection timeouts, and the lifespan of connections.The
sqlx_loggingmethod is used to enable SQL query logging, andsqlx_logging_levelspecifies the log level for the logging.Finally, the
Database::connectmethod is invoked with the configuredConnectOptionsto establish a database connection asynchronously usingawait. The resulting connection is returned as aResult.
In summary, this code sets up a database connection using SeaORM with customizable connection options, including connection pooling, timeouts, and logging.
Let's write a test to validate our implementation. I will not write unit tests with mocks again for brevity but rather try to keep the test environment as real as possible. That is where testcontainers come to our help 😎 Add the below test code to the same file lib.rs
#[cfg(test)]
mod tests {
use crate::get_connection;
use sea_orm::{ConnectionTrait, DatabaseBackend, QueryResult, Statement};
use testcontainers::{clients, images};
#[tokio::test]
async fn test_database_connection() {
let docker = clients::Cli::default();
let database = images::postgres::Postgres::default();
let node = docker.run(database);
let connection_string = &format!(
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
node.get_host_port_ipv4(5432)
);
let database_connection = get_connection(connection_string).await.unwrap();
let query_res: Option<QueryResult> = database_connection
.query_one(Statement::from_string(
DatabaseBackend::Postgres,
"SELECT 1;".to_owned(),
))
.await
.unwrap();
let query_res = query_res.unwrap();
let value: i32 = query_res.try_get_by_index(0).unwrap();
assert_eq!(1, value);
}
}
The code is wrapped in a
#[cfg(test)]attribute, indicating that it is specific to the test environment and should only be compiled and executed during testing.Various imports are made to include the necessary dependencies for testing:
get_connectionfunction from the current crate,sea_ormfor database-related functionality, andtestcontainersfor managing test containers.The actual test function is defined using the
#[tokio::test]attribute. This attribute indicates that the test function will run asynchronously using the Tokio runtime, which is commonly used for asynchronous testing in Rust.Inside the test function, a Docker client is created using
testcontainers::clients::Cli::default().A PostgreSQL image is specified as the test container using
images::postgres::Postgres::default(). We are using PostgreSQL because we configured SeaORM with that in the root Cargo.toml.The PostgreSQL container is started using
docker.run(database), which returns a handle to the running container.The connection string for the PostgreSQL database is constructed by retrieving the host port of the running container and formatting it appropriately.
The
get_connectionfunction is called to establish a connection to the PostgreSQL database using the constructed connection string. The connection is awaited usingawaitand unwrapped usingunwrap().A query is executed on the database connection to retrieve the value 1 from the database. The query is constructed using
Statement::from_stringand executed usingquery_one. The result is awaited usingawaitand unwrapped usingunwrap().The retrieved query result is assigned to
query_resas anOption<QueryResult>.The value 1 is extracted from the query result using
try_get_by_indexand assigned to the variablevalue.An assertion is made using
assert_eq!to compare the retrieved value with the expected value of 1. If the assertion fails, the test will fail.
In summary, this test code sets up a PostgreSQL test container, establishes a connection to it, executes a query, and verifies the result. It demonstrates a basic test case for checking the functionality of the get_connection function and querying the database. We have completed our database module which abstracts the connection and can be reused in other crates. In the next article, let's add implementation to another crate.