DEV Community

Cover image for Rust & MySQL: JSON serialisation of result sets retrieved using crate sqlx.
Be Hai Nguyen
Be Hai Nguyen

Posted on

Rust & MySQL: JSON serialisation of result sets retrieved using crate sqlx.

We run a MySQL stored procedure which returns a result set which has date columns. Using crates serde and serde_json, we serialise this result set into a JSON array of objects, whereby date columns are in Australian date format of dd/mm/yyyy.

This post is an extension of Rust & MySQL: connect, execute SQL statements and stored procs using crate sqlx. We'll use the same Oracle Corporation MySQL test database, the same employees table and the same get_employees stored procedure.

To recap, the employees table has the following structure:

CREATE TABLE `employees` (
  `emp_no` int NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) COLLATE utf8mb4_unicode_ci NOT NULL,
  `last_name` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
  `gender` enum('M','F') COLLATE utf8mb4_unicode_ci NOT NULL,
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Enter fullscreen mode Exit fullscreen mode

And the get_employees stored procedure is:

DELIMITER $$
CREATE DEFINER=`root`@`%` PROCEDURE `get_employees`( pmLastName varchar(16), pmFirstName varchar(14) )
    READS SQL DATA
begin
  select * from employees e where (e.last_name like pmLastName)
    and (e.first_name like pmFirstName) order by e.emp_no;
end$$
DELIMITER ;
Enter fullscreen mode Exit fullscreen mode

The code is an extension of the final version of the code in Select data via running a stored procedure section of the previously mentioned post:

🚀 In this post, instead of manually formatting and printing each row of data, we serialise the entire result set into JSON and printing the JSON data out in a single operation.

The updated dependencies of the Cargo.toml file used in this post:

...
[dependencies]
async-std = "1.12.0"
sqlx = {version = "0.7", default-features = false, features = ["runtime-async-std", "macros", "mysql", "time"]}
time = {version = "0.3.22", default-features = false, features = ["formatting", "macros", "serde"]}
serde = {version = "1.0.188", features = ["derive"]}
serde_json = "1.0.107"
Enter fullscreen mode Exit fullscreen mode

● For time crate, we add serde crate feature, so that we can do date serialising.

● We add two more crates for serialising and deserialising: serde and serde_json.

The complete working example is presented below.

Content of src/main.rs:
Enter fullscreen mode Exit fullscreen mode
use sqlx::{FromRow, Pool, MySql, Error, MySqlPool, Row};
use sqlx::types::time::Date;
use async_std::task;
use serde::Serialize;

#[derive(FromRow, Serialize)]
struct Employee {    
    emp_no: i32,
    #[serde(with = "my_date_format")]
    birth_date: Date,
    first_name: String,
    last_name: String,    
    gender: String,
    #[serde(with = "my_date_format")]
    hire_date: Date,
}

mod my_date_format {
    use sqlx::types::time::Date;
    use time::macros::format_description;
    use serde::{self, Serializer};

    pub fn serialize<S>(
        date: &Date,
        serializer: S,
    ) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let format = format_description!("[day]/[month]/[year]");
        let s = &date.format(&format).unwrap();
        serializer.serialize_str(&s)
    }    
}

async fn connect() -> Result<Pool<MySql>, Error> {
    return MySqlPool::connect("mysql://root:pcb.2176310315865259@localhost:3306/employees").await;
}

async fn do_run_stored_proc() {
    let result = task::block_on(connect());

    match result {
        Err(err) => {
            println!("Cannot connect to database [{}]", err.to_string());
        }        

        Ok(pool) => {
            let query_result = sqlx::query("call get_employees(?, ?)")
                .bind("%chi").bind("%ak")
                .map(|row: sqlx::mysql::MySqlRow| { 
                    Employee {
                        emp_no: row.get(0),
                        birth_date: row.get(1),
                        first_name: row.get(2),
                        last_name: row.get(3),
                        gender: row.get(4),
                        hire_date: row.get(5)
                    }
                })
                .fetch_all(&pool).await.unwrap();

            let json = serde_json::to_string_pretty(&query_result).unwrap();
            println!("{}", json);
        }
    }
}

fn main() {
    task::block_on(do_run_stored_proc());
}
Enter fullscreen mode Exit fullscreen mode

Let's walk through the code:

● Trait serde::Serialize -- the struct Employee needs to implement this trait, so that it can be serialised.

● This documentation page Date in a custom format from crate serde provides an example for date time serialisation and deserialisation. The mod my_date_format above comes from this example, but I implement only the serialisation part.

The date format in pub fn serialize<S> has been discussed in the previous mentioned post, and in detail in Rust: baby step -- some preliminary look at date.

● The derive macro helper attribute #[serde(with = "my_date_format")] also comes from Date in a custom format. Through trial and error, I've found out that it needs to be added to above all fields which need to be serialised.

let json = serde_json::to_string_pretty(&query_result).unwrap(); is also from Date in a custom format. Although the use of the variable query_result is trial and error... and by sheer luck, I have it working the first time round. Originally I thought of extracting each row into a vector, then serialise the vector: but that is too much additional work. If you asked me why query_result works in this case, I would not be able to explain! Hopefully, I will come to understand this in the future.

Please note: the example code has been tested on both Windows 10 and Ubuntu 22.10.

The following screenshot shows the output of the above example:

085-01.png

One final point, we look at deleting and inserting data in Rust & MySQL: delete, insert data using crate sqlx, also using the employees table. The updated struct Employee in this post would still work in the just mentioned post. This is because when we manually create an instance of struct Employee, we have the two date columns in the correct format, hence no deserialisation is required:

                ...
                birth_date: date!(1999-11-24),
                hire_date: date!(2022-04-29)
                ...   
Enter fullscreen mode Exit fullscreen mode

Thank you for reading. I hope you find this post useful and stay safe as always.

✿✿✿

Feature image source:

Top comments (0)