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_connection
is defined, which takes adatabase_url
parameter as input and returns aResult
containing aDatabaseConnection
or aDbErr
(database error) if any occurs.Within the function, a
ConnectOptions
struct 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_logging
method is used to enable SQL query logging, andsqlx_logging_level
specifies the log level for the logging.Finally, the
Database::connect
method is invoked with the configuredConnectOptions
to 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_connection
function from the current crate,sea_orm
for database-related functionality, andtestcontainers
for 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_connection
function is called to establish a connection to the PostgreSQL database using the constructed connection string. The connection is awaited usingawait
and 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_string
and executed usingquery_one
. The result is awaited usingawait
and unwrapped usingunwrap()
.The retrieved query result is assigned to
query_res
as anOption<QueryResult>
.The value 1 is extracted from the query result using
try_get_by_index
and 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.