We create a REST service on PostgreSQL and Rust. Part 1: prototype

  • Tutorial
Hello.

Some time ago I saw a post on the notorious blog about how to implement on Go a simple program that works with a database, and then make an entire REST service on its basis . I decided to check how difficult it is to make a similar program in Rust and share the results.



We will start by working with the database and create a regular console application, and then add, so to speak, a REST frontend.

A few introductory remarks.

For the impatient, here is the finished project on GitHub. It includes the implementation of a REST service. I invite everyone else to read further.

In general, I will try to illustrate in detail the development process with all errors regarding Rust, their causes and solutions. I think knowing typical problems and how to solve them greatly helps newcomers to the language. Do not be afraid of the compiler, he is your friend.

You will need installed Rust ( how to install ). Any version after 1.0 should work - both stable and nightly. I tried several in the range 1.1-1.3.

The code itself is of prototype quality - I'm not trying to make a very reliable or readable program right now. Having understood it, you can think about the correctness and style later. However, this version was written very quickly.

Now to the point.

Like any Rust project that does not require special tricks with assembly, our program will use Cargo. Create a new project:

$ cargo new --bin rust-phonebook
$ cd rust-phonebook

Cargo carefully creates the Git repository in the directory.

What does it look like
$ git status
On branch master
Initial commit
Untracked files:
  (use "git add ... "to include in what will be committed)
	.gitignore
	Cargo.toml
	src /
nothing added to commit but untracked files present (use "git add" to track)


And we can immediately build and run our stub program:

$ cargo run
   Compiling rust-phonebook v0.1.0 (file: /// home / mkpankov / rust-phonebook)
     Running `target / debug / rust-phonebook`
Hello world!

Then we commit our changes to the repository and move on to the essence of our program.



Let's start with the simplest prototype that connects to the database, creates one table, adds one record there and reads it back.

First I will give the whole code, and then I will explain each part of it. Below is the contents of src / main.rs.

The code
extern crate postgres;
use postgres::{Connection, SslMode};
struct Person {
    id: i32,
    name: String,
    data: Option>
}
fn main() {
    let conn =
        Connection::connect(
            "postgres://postgres:postgres@localhost",
            &SslMode::None)
        .unwrap();
    conn.execute(
        "CREATE TABLE person (
           id              SERIAL PRIMARY KEY,
           name            VARCHAR NOT NULL,
           data            BYTEA
         )",
        &[])
        .unwrap();
    let me = Person {
        id: 0,
        name: "Михаил".to_string(),
        data: None
    };
    conn.execute(
        "INSERT INTO person (name, data) VALUES ($1, $2)",
        &[&me.name, &me.data])
        .unwrap();
    let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();
    for row in stmt.query(&[]).unwrap() {
        let person = Person {
            id: row.get(0),
            name: row.get(1),
            data: row.get(2)
        };
        println!("Нашли человека: {}", person.name);
    }
}


Let's take it in order.

fn main() {
    let conn =
        Connection::connect(
            "postgres://postgres:postgres@localhost",
            &SslMode::None)
        .unwrap();

The first line in our new main is connecting to the database. Here it is worth immediately telling more.

We assume that the PostgreSQL server is running locally on the default port, and the username and password are “postgres”. To do this, of course, we need to install PostgreSQL. You can see, for example, this guide . Indicate your username with access to the database and his password instead of "postgres: postgres".

In addition, do not forget to initialize the database.

The aforementioned Connection itself is a type from the postgres container ( documentation ). Therefore, we request its binding at the top of the file

extern crate postgres;

and enter into the scope of Connection and SslMode

use postgres::{Connection, SslMode};

If you try to build the program right now, you will get another error:

$ cargo build
   Compiling rust-phonebook v0.1.0 (file: ///home/mkpankov/rust-phonebook.finished)
src / main.rs: 1: 1: 1:23 error: can't find crate for `postgres`
src / main.rs: 1 extern crate postgres;
              ^ ~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `rust-phonebook`.
To learn more, run the command again with --verbose.

This means that the compiler did not find a suitable container. This is because we did not specify it in the dependencies of our project. Let's do it in Cargo.toml ( more ):

[dependencies]
postgres = "0.9"

Now everything should be collected. But if you did not start the server, you will get the following error:

 
$ cargo run
     Running `target / debug / rust-phonebook`
thread '
'panicked at' called `Result :: unwrap ()` on an `Err` value: IoError (Error {repr: Os {code: 111, message:" Connection refused "}}) ', ../src/libcore/ result.rs:732

This is a direct result of our .unwrap () - it causes a panic of the current thread if the Result was not Ok (_) - i.e. A connection error has occurred.

By the way, you can see the backtrace for it if you run the program with RUST_BACKTRACE = 1 set in the environment (it works only in the debug version of the program!).

Backtrace
$ RUST_BACKTRACE = 1 cargo run
     Running `target / debug / rust-phonebook`
thread '
'panicked at' called `Result :: unwrap ()` on an `Err` value: IoError (Error {repr: Os {code: 111, message:" Connection refused "}}) ', ../src/libcore/ result.rs:732 stack backtrace: 1: 0x56007b30a95e - sys :: backtrace :: write :: haf6e4e635ac76143Ivs 2: 0x56007b30df06 - panicking :: on_panic :: ha085a58a08f78856lzx 3: 0x56007b3049ae - rt :: unwind :: begin_unwind_inner :: hc90ee27246f12475C0w 4: 0x56007b304ee6 - rt :: unwind :: begin_unwind_fmt :: ha4be06289e0df3dbIZw 5: 0x56007b30d8d6 - rust_begin_unwind 6: 0x56007b3390c4 - panicking :: panic_fmt :: he7875691f9cbe589SgC 7: 0x56007b25e58d - result :: Result:: unwrap :: h10659124002062427088 at ../src/libcore/macros.rs:28 8: 0x56007b25dcfd - main :: h2f2e9aa4b99bad67saa at src / main.rs: 13 9: 0x56007b30d82d - __rust_try 10: 0x56007b30fbca - rt :: lang_start :: hefba4015e797c325hux 11: 0x56007b27d1ab - main 12: 0x7fb3f21076ff - __libc_start_main 13: 0x56007b25db48 - _start 14: 0x0 -


Fuh, just one line, and so many ways to mess up! I hope you are not very scared and ready to continue.

The positive thing here is that we explicitly say that we want to drop the program in case of a connection error. When we want to make a normal product out of our toy, a simple text search on .unwrap () will show you where to start. Further I will not dwell on this moment.

Create a table:

    conn.execute(
        "CREATE TABLE person (
           id              SERIAL PRIMARY KEY,
           name            VARCHAR NOT NULL,
           data            BYTEA
         )",
        &[])
        .unwrap();

Strange & [] at the end is an empty slice . This request has no parameters, so we do not pass them.

Why a slice, not an array? A good style in Rust is not to accept ownership if objects are read-only. Otherwise, we would have to clone the value to pass to the function, i.e. she would have swallowed him. Read more about ownership here .

Next, we create a structure that represents our record, which we will add to the table:

    let me = Person {
        id: 0,
        name: "Михаил".to_string(),
        data: None
    };

Fundamentally, now it makes no sense to add this data to the structure, but it will help us further. By the way, here is her announcement:

struct Person {
    id: i32,
    name: String,
    data: Option>
}

Now perform the actual insertion:

    conn.execute(
        "INSERT INTO person (name, data) VALUES ($1, $2)",
        &[&me.name, &me.data])
        .unwrap();

Here, our request already has parameters. They are substituted using string interpolation in the numbered fields $ 1, $ 2, etc. And now our parameter slice is not empty - it contains links to the corresponding fields of the structure.

Next, we prepare a query to the database to read what we wrote:

    let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();

I think nothing interesting. It is just creating a request object. Duplicate queries make sense not to recreate, but to store, to increase performance. We could also immediately execute the request without creating a “prepared object”.

At the end, we execute the request itself. Let's go through each line:

    for row in stmt.query(&[]).unwrap() {

Here we go around the array of query results. As always, the request might fail. The parameter list is again empty - & [].

Now again we collect the structure from the query results.

        let person = Person {
            id: row.get(0),
            name: row.get(1),
            data: row.get(2)
        };

Here we just take the fields by numbers, but in general the library allows you to use the names of the table fields.

Finally, we print the message with the result:

        println!("Нашли человека: {}", person.name);
    }
}

The post turned out to be long, because we got acquainted with the infrastructure, terminology and set up the environment, but I hope it will be useful as an illustration of the workflow.

In the next part, we will add the server configuration in the .ini file. Stay with us!

Also popular now: