Getting started
This crate provides the #[ergol]
macro. You can use it by adding
ergol = "0.1"
to your dependencies.
It allows to persist the data in a database. For example, you just have to write
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: Option<i32>, } }
and the #[ergol]
macro will generate most of the code you will need. You'll
then be able to run code like the following:
extern crate tokio; extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: Option<i32>, } #[tokio::main] async fn main() -> Result<(), ergol::tokio_postgres::Error> { let (db, connection) = ergol::connect( "host=localhost user=ergol password=ergol dbname=ergol", ergol::tokio_postgres::NoTls, ) .await?; tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); // Drop the user table if it exists User::drop_table().execute(&db).await.ok(); // Create the user table User::create_table().execute(&db).await?; // Create a user and save it into the database let mut user: User = User::create("thomas", "pa$$w0rd", Some(28)).save(&db).await?; // Change some of its fields *user.age.as_mut().unwrap() += 1; // Update the user in the database user.save(&db).await?; // Fetch a user by its username thanks to the unique attribute let user: Option<User> = User::get_by_username("thomas", &db).await?; // Select all users let users: Vec<User> = User::select().execute(&db).await?; Ok(()) }
Ergol macro
The #[ergol]
macro transforms your struct in a table, and gives you methods
to easily access it.
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: i32, } }
The #[id]
attribute
In every table, a primary key is required. For the moment, ergol
requires to
have an id
column, which is an i32
field named id
.
The #[unique]
attribute
If a field is marked with the #[unique]
attribute, the macro will generate
extra methods to fetch an element from this attribute. For example, with the
#[unique] pub username: String
attribute, we can now use the following:
extern crate tokio; extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: i32, } #[tokio::main] async fn main() -> Result<(), ergol::tokio_postgres::Error> { let (db, connection) = ergol::connect( "host=localhost user=ergol password=ergol dbname=ergol", ergol::tokio_postgres::NoTls, ) .await?; tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); User::drop_table().execute(&db).await.ok(); User::create_table().execute(&db).await?; let user: Option<User> = User::get_by_username("thomas", &db).await?; Ok(()) }
Enums in ergol
To use an enum in an ergol
managed struct, this enum needs to derive
Debug
and PgEnum
. An ergol
managed enum cannot have attributes.
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub role: Role, } #[derive(PgEnum, Debug)] pub enum Role { Guest, Admin, } }
One-to-one and many-to-one relationships
Let's say you want a user to be able to have projects. You can use the
#[many_to_one]
attribute in order to do so. Let's take the following code as
an example:
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: Option<i32>, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[many_to_one(projects)] pub owner: User, } }
Once you have defined this struct, many more functions become available:
extern crate tokio; extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: Option<i32>, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[many_to_one(projects)] pub owner: User, } #[tokio::main] async fn main() -> Result<(), ergol::tokio_postgres::Error> { let (db, connection) = ergol::connect( "host=localhost user=ergol password=ergol dbname=ergol", ergol::tokio_postgres::NoTls, ) .await?; tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); // Drop the tables if they exist Project::drop_table().execute(&db).await.ok(); User::drop_table().execute(&db).await.ok(); // Create the tables User::create_table().execute(&db).await?; Project::create_table().execute(&db).await?; // Create two users and save them into the database let thomas: User = User::create("thomas", "pa$$w0rd", 28).save(&db).await?; User::create("nicolas", "pa$$w0rd", 28).save(&db).await?; // Create some projects for the user let project: Project = Project::create("My first project", &thomas).save(&db).await?; Project::create("My second project", &thomas).save(&db).await?; // You can easily find all projects from the user let projects: Vec<Project> = thomas.projects(&db).await?; // You can also find the owner of a project let owner: User = projects[0].owner(&db).await?; Ok(()) }
You can similarly have one-to-one relationship between a user and a project by
using the #[one_to_one]
attribute:
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: Option<i32>, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[one_to_one(project)] pub owner: User, } }
This will add the UNIQUE
attribute in the database and make the project
method only return an option:
extern crate tokio; extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: Option<i32>, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[one_to_one(project)] pub owner: User, } #[tokio::main] async fn main() -> Result<(), ergol::tokio_postgres::Error> { let (db, connection) = ergol::connect( "host=localhost user=ergol password=ergol dbname=ergol2", ergol::tokio_postgres::NoTls, ) .await?; tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); Project::drop_table().execute(&db).await.ok(); User::drop_table().execute(&db).await.ok(); User::create_table().execute(&db).await?; Project::create_table().execute(&db).await?; let thomas: User = User::create("thomas", "pa$$w0rd", 28).save(&db).await?; // You can easily find a user's project let project: Option<Project> = thomas.project(&db).await?; Ok(()) }
Note that that way, a project has exactly one owner, but a user can have no project.
Many-to-many relationships
Ergol also supports many-to-many relationships. In order to do so, you need to
use the #[many_to_many]
attribute:
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: i32, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[many_to_many(visible_projects)] pub authorized_users: User, } }
The same way, you will have plenty of functions that you will be able to use to manage your objects:
extern crate tokio; extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: i32, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[many_to_many(visible_projects)] pub authorized_users: User, } #[tokio::main] async fn main() -> Result<(), ergol::tokio_postgres::Error> { let (db, connection) = ergol::connect( "host=localhost user=ergol password=ergol dbname=ergol", ergol::tokio_postgres::NoTls, ) .await?; tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); Project::drop_table().execute(&db).await.ok(); User::drop_table().execute(&db).await.ok(); User::create_table().execute(&db).await?; Project::create_table().execute(&db).await?; User::create("thomas", "pa$$w0rd", 28).save(&db).await?; User::create("nicolas", "pa$$w0rd", 28).save(&db).await?; // Find some users in the database let thomas = User::get_by_username("thomas", &db).await?.unwrap(); let nicolas = User::get_by_username("nicolas", &db).await?.unwrap(); // Create a project let first_project = Project::create("My first project").save(&db).await?; // Thomas can access this project first_project.add_authorized_user(&thomas, &db).await?; // The other way round nicolas.add_visible_project(&first_project, &db).await?; // The second project can only be used by thomas let second_project = Project::create("My second project").save(&db).await?; thomas.add_visible_project(&second_project, &db).await?; // The third project can only be used by nicolas. let third_project = Project::create("My third project").save(&db).await?; third_project.add_authorized_user(&nicolas, &db).await?; // You can easily retrieve all projects available for a certain user let projects: Vec<Project> = thomas.visible_projects(&db).await?; // And you can easily retrieve all users that have access to a certain project let users: Vec<User> = first_project.authorized_users(&db).await?; // You can easily remove a user from a project let _: bool = first_project.remove_authorized_user(&thomas, &db).await?; // Or vice-versa let _: bool = nicolas.remove_visible_project(&first_project, &db).await?; // The remove functions return true if they successfully removed something. Ok(()) }
Extra information in a many-to-many relationship
It is possible to insert some extra information in a many to many relationship. The following exemple gives roles for the users for projects.
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: i32, } #[derive(PgEnum, Debug)] pub enum Role { Admin, Write, Read, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[many_to_many(projects, Role)] pub users: User, } }
With these structures, the signature of generated methods change to take a role as argument, and to return tuples of (User, Role) or (Project, Role).
extern crate tokio; extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] pub id: i32, #[unique] pub username: String, pub password: String, pub age: i32, } #[derive(PgEnum, Debug)] pub enum Role { Admin, Write, Read, } #[ergol] pub struct Project { #[id] pub id: i32, pub name: String, #[many_to_many(projects, Role)] pub users: User, } #[tokio::main] async fn main() -> Result<(), ergol::tokio_postgres::Error> { let (db, connection) = ergol::connect( "host=localhost user=ergol password=ergol dbname=ergol2", ergol::tokio_postgres::NoTls, ) .await?; tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); Project::drop_table().execute(&db).await.ok(); User::drop_table().execute(&db).await.ok(); Role::drop_type().execute(&db).await.ok(); Role::create_type().execute(&db).await?; User::create_table().execute(&db).await?; Project::create_table().execute(&db).await?; User::create("thomas", "pa$$w0rd", 28).save(&db).await?; User::create("nicolas", "pa$$w0rd", 28).save(&db).await?; let thomas = User::get_by_username("thomas", &db).await?.unwrap(); let nicolas = User::get_by_username("nicolas", &db).await?.unwrap(); let project = Project::create("My first project").save(&db).await?; project.add_user(&thomas, Role::Admin, &db).await?; nicolas.add_project(&project, Role::Read, &db).await?; for (user, role) in project.users(&db).await? { println!("{} has {:?} rights on project {:?}", user.username, role, project.name); } let project = Project::create("My second project").save(&db).await?; project.add_user(&thomas, Role::Admin, &db).await?; let project = Project::create("My third project").save(&db).await?; project.add_user(&nicolas, Role::Admin, &db).await?; project.add_user(&thomas, Role::Read, &db).await?; for (project, role) in thomas.projects(&db).await? { println!("{} has {:?} rights on project {:?}", thomas.username, role, project.name); } Ok(()) }
Using ergol with rocket
You can integrate ergol with rocket!
Note: ergol supports only rocket 0.5, which is not published on crates.io
yet. If you wish to use ergol with rocket, you must use ergol git version
instead of the crates.io version, in order to be able to use the with-rocket
feature. The dependency in your Cargo.toml
will look like this:
ergol = { git = "https://github.com/polymny/ergol", rev = "v0.1.0", features = ["with-rocket"] }
There is a little bit of boilerplate, but once everything is set up, you can use ergol in your routes!
extern crate ergol; #[macro_use] extern crate rocket; use rocket::fairing::AdHoc; use rocket::request::{FromRequest, Outcome, Request}; use rocket::State; use ergol::deadpool::managed::Object; use ergol::prelude::*; /// A wrapper for a database connection extrated from a pool. pub struct Db(Object<ergol::pool::Manager>); impl Db { /// Extracts a database from a pool. pub async fn from_pool(pool: ergol::Pool) -> Db { Db(pool.get().await.unwrap()) } } // This allows to pass directly Db instead of Ergol to the ergol's functions. impl std::ops::Deref for Db { type Target = Object<ergol::pool::Manager>; fn deref(&self) -> &Self::Target { &*&self.0 } } // This allows to use Db in routes parameters. #[rocket::async_trait] impl<'r> FromRequest<'r> for Db { type Error = (); async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { let pool = request.guard::<&State<ergol::Pool>>().await.unwrap(); let db = pool.get().await.unwrap(); Outcome::Success(Db(db)) } } #[ergol] pub struct Item { #[id] id: i32, name: String, count: i32, } #[get("/add-item/<name>/<count>")] async fn add_item(name: String, count: i32, db: Db) -> String { Item::create(name, count).save(&db).await.unwrap(); "Item added".into() } #[get("/")] async fn list_items(db: Db) -> String { let items = Item::select() .execute(&db) .await .unwrap() .into_iter() .map(|x| format!(" - {}: {}", x.name, x.count)) .collect::<Vec<_>>() .join("\n"); format!("{}\n{}", "List of items:", items) } #[rocket::main] async fn main() -> Result<(), rocket::Error> { // Setup rocket with its database connections pool. let rocket = rocket::build() .attach(AdHoc::on_ignite("Database", |rocket| async move { let pool = ergol::pool("host=localhost user=ergol password=ergol", 32).unwrap(); rocket.manage(pool) })) .mount("/", routes![list_items, add_item]) .ignite() .await?; // Get the pool from rocket. let pool = rocket.state::<ergol::Pool>().unwrap(); { // Reset the Db at startup (you may not want to do this, but it's cool for an example). let db = Db::from_pool(pool.clone()).await; Item::drop_table().execute(&db).await.ok(); Item::create_table().execute(&db).await.unwrap(); } // rocket.launch().await Ok(()) }
See the example for more details.
Migrations
Ergol comes with a (pretty simple for the moment) system for migrations.
In order to be able to manage your migrations, you need to install ergol cli:
cargo install ergol_cli
Saving migrations
When building your application, the ergol
proc macro automatically generates
json files for each marked structure. Those json files represent the state of
the database, and are stored in the migrations/current
directory.
You can freeze a certain state of your database by running
ergol save
It will copy the migrations/current
directory to a migrations/0
directory
(or migrations/n
n being the number of migrations you already have). It will
also add up.sql
and down.sql
to migrate from n - 1 to n and back.
Note: when adding new columns, you probably will need to specify default
values by editing directly the migrations/n/up.sql
.
For example, let's say I start an application with the following model:
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] id: i32, username: String, email: String, } }
The up.sql
will look like this
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR NOT NULL,
email VARCHAR NOT NULL
);
and the down.sql
will look like this
DROP TABLE users CASCADE;
I build my application, then run ergol save
, and have my
migrations/0/{up.sql,down.sql}
files.
Let's say I want to add a new attribute to my User
struct for the age of the
users.
#![allow(unused)] fn main() { extern crate ergol; use ergol::prelude::*; #[ergol] pub struct User { #[id] id: i32, username: String, email: String, age: i32, } }
If I run ergol save
again, I will have a migrations/1/up.sql
that will look
like this:
ALTER TABLE users ADD age INT NOT NULL DEFAULT /* TODO default value */;
You need to change this code to set the default value for the column.
Running ergol hint
will show the code that migrates from the last migration
to the current migration.
Running migrations
You can run all migrations by running ergol migrate
. Ergol creates a special
table that it uses to keep track of what migration the database is in, and will
run all migrations between the current migration of the database to the last
saved migrations.
Reset
The last useful command you can do with ergol is ergol reset
. It deletes the
whole database, and recreate it using only the last migration. It is
particularly useful when developping an app when you want to reset your database.