Hello.
Some time ago I saw in a well-known blog a
post about how to implement a simple program working with a database on Go, and then
build a whole
REST service on its base. I decided to check how difficult it is to make a similar program on 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 notes.
For the impatient, this is a
complete GitHub
project . It includes the implementation of the REST service. I invite everyone else to read further.
In general, I will try to illustrate in detail the development process with all the errors relating to Rust, their causes and ways to resolve. I think the knowledge of typical problems and ways to solve them greatly helps beginners in the language. Do not be afraid of the compiler, it is your friend.
You will need an installed Rust (
how to install ). Any version after 1.0 should work - both stable and nightly. I tried several in the range of 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 project on Rust, which does not require special tricks with the assembly, our program will use Cargo. Create a new project:
$ cargo new --bin rust-phonebook
$ cd rust-phonebook
Cargo carefully creates a Git repository in the directory.
What it looks like $ git status
On branch master
Initial commit
Untracked files:
(use "git add <file> ..." to include it in)
.gitignore
Cargo.toml
src /
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!
After that, 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 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.
Codeextern crate postgres; use postgres::{Connection, SslMode}; struct Person { id: i32, name: String, data: Option<Vec<u8>> } 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 sort everything out in order.
fn main() { let conn = Connection::connect( "postgres://postgres:postgres@localhost", &SslMode::None) .unwrap();
The first line in our new main - connection to the database. Here it is necessary to immediately tell more.
We assume that the PostgreSQL server is running locally on the default port, and the username and password is “postgres”. To do this, we, of course, need to install PostgreSQL. You can see, for example,
this guide . Specify your username, which has access to the database, and its 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 building a 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 a 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 going. But if you did not start the server, you will receive the following error:
$ cargo run
Running `target / debug / rust-phonebook`
thread '<main>' 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 the immediate result of our .unwrap () - it causes a panic of the current thread if the Result was not Ok (_) - i.e. There was a connection error.
By the way, the backtrace for it can be seen if you start 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 '<main>' 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 <T, E> :: 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 - <unknown>
Fuh, just one line, and so many ways to nakosyachit! I hope you are not very scared and ready to continue.
The positive point 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 for .unwrap () will show you where to start. Further I will not stop 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
cut . This request has no parameters, so we do not pass them.
Why a slice, and not an array? A good style in Rust is to not take possession if objects are needed only for reading. Otherwise, we would have to clone the value to pass to the function, i.e. she would swallow 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 };
In principle, now there is no point in putting this data into a structure, but further it will help us. By the way, here is her announcement:
struct Person { id: i32, name: String, data: Option<Vec<u8>> }
Now perform the actual insert:
conn.execute( "INSERT INTO person (name, data) VALUES ($1, $2)", &[&me.name, &me.data]) .unwrap();
Here our query 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 structure fields.
Next, we prepare a request to the database to read what we have recorded:
let stmt = conn.prepare("SELECT id, name, data FROM person").unwrap();
I think nothing interesting. This is just the creation of a request object. Repeated requests it makes sense not to recreate, but to store, to increase performance. We could also immediately execute the request without creating a “cooked object”.
At the end we execute the request itself. Lets 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 empty again - & [].
Now we reassemble 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 the use of table field names.
Finally, we print a message with the result:
println!(" : {}", person.name); } }
The post turned out to be a long one, since 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 section, we will add the server configuration in the ini file. Stay with us!