Introduction
This book covers every major feature of the aragog
library.
Note that everything in the lib is documented, don't forget to check the technical documentation for detailed information.
Aragog is an object-document mapper for Arango DB, including:
- Document mapping with rust structs
- complete CRUD operations with hooks (callbacks)
- Type safe query engine
- Graph and edges traversal
Set up ArangoDB
Installation (See official documentation Here)
- Download Link
- Run it with
/usr/local/sbin/arangod
The default installation contains one database_system
and a user namedroot
- Create a user and database for the project with the
arangosh
shell
arangosh> db._createDatabase("DB_NAME");
arangosh> var users = require("@arangodb/users");
arangosh> users.save("DB_USER", "DB_PASSWORD");
arangosh> users.grantDatabase("DB_USER", "DB_NAME");
It is a good practice to create a test db and a development db.
- you can connect to the newly created db with
$> arangosh --server.username $DB_USER --server.database $DB_NAME
Installation
Aragog CLI
Install the aragog migration and schema generation command line interface with cargo:
cargo install aragog_cli
Aragog Lib
Add to your cargo.toml
the following:
aragog = "0.17.1"
Cargo features
Async and Blocking
By default, all aragog
items are asynchronous, you can compile aragog
in a synchronous build using the blocking
feature:
aragog = { version = "0.17.1", features = ["blocking"] }
OpenSSL and Rustls
aragog
uses reqwest
to query ArangoDB. By default, OpenSSL is used, but you can compile aragog
to use rustls using the rustls
feature:
aragog = { version = "0.17.1", features = ["rustls"], default-features = false }
You need to disable the default features. Don't forget to add the derive
feature to use the derive macros.
Minimal Traits
If you don't need the following traits:
AuthorizeAction
New
Update
You can disable them with the minimal_traits
feature:
aragog = { version = "0.17.1", features = ["minimal_traits"] }
Initialization
In order to work, Aragog needs you to define your database schema in an schema.yaml
.
We provide aragog_cli for this purpose, generating a synchronized and versioned schema and a safe migration system.
With this schema you can initialize a database connection to provide a database access required for all database operations.
Schema Generation
aragog_cli provides the following basics:
Creating a Migration
Command: aragog create_migration MIGRATION_NAME
Creates a new migration file in SCHEMA_PATH/migrations/
. If the db
folder is missing it will be created automatically.
Launch migrations
Command: aragog migrate
Will launch every migration in SCHEMA_PATH/migrations/
and update the schema according to its current version.
If there is no schema it will be generated.
Note: ArangoDB doesn't handle transactional operations for collection, index and graph management
Rollback migrations
Command: aragog rollback
Will rollback 1 migration in SCHEMA_PATH/migrations/
and update the schema according to its current version.
Command: aragog rollback COUNT
Will rollback COUNT
migration in $CHEMA_PATH/migrations/
and update the schema according to its current version.
The schema is generated twice:
- once on the file system (
schema.yaml
)- once in the database, the snapshot (synchronized version)
This allows seamless deployment, as the migrations launch will check the current snapshot
Using aragog with an exiting database
The aragog_cli provides a discover
command creating a migration file for already initialized database and apply it to the schema.
The Database Connection
The database connection is the main handler of every ArangoDB communication. You need to initialize one to be able to use the other library features.
The connection loads a Database Schema (schema.yaml
) file. Use aragog_cli to create migrations and generate the file.
Note: You can also create your own file but it's not recommended
Creating a Database Connection
To connect to the database and initialize a database connection you may use the following builder pattern options:
#![allow(unused_variables)] fn main() { let db_connection = DatabaseConnection::builder() // You can specify a host and credentials with this method. // Otherwise, the builder will look for the env vars: `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD`. .with_credentials("http://localhost:8529", "db", "user", "password") // You can specify a authentication mode between `Basic` and `Jwt` // Otherwise the default value will be used (`Basic`). .with_auth_mode(AuthMode::Basic) // You can specify some operations options that will be used for every `write` operations like // `create`, `save` and `delete`. .with_operation_options(OperationOptions::default()) // You can specify a schema path to initialize the database connection // Otherwise the env var `SCHEMA_PATH` or the default value `config/db/schema.yaml` will be used. .with_schema_path("config/db/schema.yaml") // If you prefer you can use your own custom schema .with_schema(DatabaseSchema::default()) // The schema wil silently apply to the database, useful only if you don't use the CLI and migrations .apply_schema() // You then need to build the connection .build() .await .unwrap(); }
Using env vars
None of the builder options are mandatory, the following works perfectly if all required environment variables are set:
#![allow(unused_variables)] fn main() { let db_connection = DatabaseConnection::builder() .build() .await .unwrap(); }
The env vars are the following:
Name | Description |
---|---|
DB_HOST | The ArangoDB host, usually http://localhost:8259 |
DB_NAME | The ArangoDB database name |
DB_USER | The Database user you want to use |
DB_PASSWORD | The DB_USER password |
SCHEMA_PATH | The path of the schema file, by default config/db/schema.yaml |
It is recommended to leave the
SCHEMA_PATH
unset, as the default value is idiomatic
Technical notes
DatabaseAccess
trait
The object DatabaseConnection
is the default handler for every database operations but you can create your own.
Every operation taking the connection as an argument is taking a Generic type implementing DatabaseAccess
,
so you can implement it on your own struct.
Note: This is not recommended, modification to
DatabaseAccess
can happen without considering them as breaking.
Trait object
All aragog
API using a DatabaseAccess
type also use ?Sized
(Sized
restriction relaxation)
This means you can use dynamically typed trait objects.
Example extract from boxed example:
#![allow(unused_variables)] fn main() { pub struct BoxedConnection { pub connection: Box<dyn DatabaseAccess>, } impl BoxedConnection { pub fn connection(&self) -> &dyn DatabaseAccess { self.connection.deref() } } }
Database truncation
The DatabaseConnection
provides a truncate_database
method but you should use it only for testing purposes,
it is highly destructive as it will drop every collections known to the connection.
The Record
trait
This trait defines aragog
ODM (Object-Document mapper).
Every type implementing this trait becomes a Model that can be mapped to an ArangoDB collection document.
Note: enums don't work as records but can be record fields
When declaring a model like the following:
#![allow(unused_variables)] fn main() { use aragog::Record; #[derive(Serialize, Deserialize, Clone, Record)] pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } }
To derive Record
your structure needs to derive or implement Serialize
, Deserialize
and Clone
which are needed
to store the document data.
We don't specify the _key
field, as we describe the document's data;
Note: An ArangoDB document is identified by its
_key
field which is its primary identifier and_id
and_rev
fields not yet used byaragog
.
Custom collection name
By default, the collection name associated with the model will be the same. A User
struct deriving Record
will be stored in a User
collection (case sensitive).
In the case were your model and collection name don't match you can specify a collection_name
attribute along with the derive macro:
#![allow(unused_variables)] fn main() { use aragog::Record; #[derive(Serialize, Deserialize, Clone, Record)] #[collection_name = "Users"] pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } }
In this example, the User
models will be synced with the Users
collection.
Synced documents
To create a document in the database we need to use the aragog
generic struct DatabaseRecord<T>
.
DatabaseRecord
describes a document synchronized with the database:
#![allow(unused_variables)] fn main() { // The User document data let user = User { username: String::from("LeRevenant1234"), first_name: String::from("Robert"), last_name: String::from("Surcouf"), age: 18 }; // We create the document on the database collection "User", returning a `DatabaseRecord<User>` let mut user_record = DatabaseRecord::create(user, &database_connection).await.unwrap(); // We can now access the unique `_key` of the document let document_key = user_record.key(); // The key can be used to retrieve documents, returning again a `DatabaseRecord<User>` let found_user = User::find(document_key, &database_connection).await.unwrap(); // We can access the document data from the database record assert_eq!(user.username, found_user.username); }
key
is the document primary identifier, certifying write action in the database collectionrecord
is the document data, a generic containing your struct implementing theRecord
trait
Document operations
Documents can be:
- created with
DatabaseRecord::create
- retrieved with
YourRecord::find
orDatabaseRecord::find
(not recommended) - saved with
DatabaseRecord::save
- deleted with
DatabaseRecord::delete
The DatabaseRecord
structure wraps all ODM operations for any struct implementing Record
Complete Example:
use aragog::{Record, DatabaseConnection, DatabaseRecord, Validate, AuthMode}; use serde::{Serialize, Deserialize}; use tokio; #[derive(Serialize, Deserialize, Clone, Record)] pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } #[tokio::main] async fn main() { // Database connection Setup let database_connection = DatabaseConnection::builder() .build() .await .unwrap(); // Define a document let mut user = User { username: String::from("LeRevenant1234"), first_name: String::from("Robert"), last_name: String::from("Surcouf"), age: 18 }; // We create the user let mut user_record = DatabaseRecord::create(user, &database_connection).await.unwrap(); // You can access and edit the document user_record.username = String::from("LeRevenant1524356"); // And directly save it user_record.save(&database_connection).await.unwrap(); // Or delete it user_record.delete(&database_connection).await.unwrap(); }
Operation options
All the write operations (create, save and delete) provide a variant _with_option
:
create_with_options
save_with_options
delete_with_options
These methods allow to customize some aspects of the operation:
wait_for_sync
: Should aragog wait for the operations to be written on disk? (by default the collection behavior is kept)ignore_revs
: Should ArangoDB ignore the revision conflict (true
by default)ignore_hooks
: Should the operation skip the related Hooks ?
These options are available but you should use them sparingly. Prefer defining a global option settings directly in the DatabaseConnection if you find yourself in a situation where you want:
- To always or never wait for sync
- To always or never ignore the revision system
- To always skip the hooks
Keep in mind that all write operations also have force_
variants which:
- explicitly ignore the revision system
- explicitly ignore the hooks
No matter what the global options are.
Hooks
The Record
trait provides the following hooks:
- before hooks:
before_create
: executed before document creation (DatabaseRecord::create
)before_save
: executed before document save (DatabaseRecord::save
)before_delete
: executed before document deletion (DatabaseRecord::delete
)before_write
: executed before both document creation and save.before_all
: executed before document creation, save and deletion.
- after hooks:
after_create
: executed after the document creation (DatabaseRecord::create
)after_save
: executed after the document save (DatabaseRecord::save
)after_delete
: executed after the document deletion (DatabaseRecord::delete
)after_write
: executed after both document creation and save.after_all
: executed after both document creation, save and deletion.
You can register various methods in these hooks with the following syntax:
#![allow(unused_variables)] fn main() { #[before_create(func = "my_method")] }
The hooked methods can follow various patterns using the following options:
is_async
the method is asyncdb_access
the method uses the db access
By default both options are set to false
.
You can combine options to have an async
hook with db access to execute document operations automatically.
If you combine a lot of operations, like creating documents in hooks or chaining operations make sure to:
- avoid circular operations
- use Transaction for safety
Hook Patterns
Simple hook with no options
#![allow(unused_variables)] fn main() { #[before_create(func = "my_method")] }
my_method can be either:
-
#![allow(unused_variables)] fn main() { fn my_method(&self) -> Result<(), aragog::Error> }
-
#![allow(unused_variables)] fn main() { fn my_method(&mut self) -> Result<(), aragog::Error> }
Async hook
#![allow(unused_variables)] fn main() { #[before_create(func = "my_method", is_async = true)] }
my_method can be either:
-
#![allow(unused_variables)] fn main() { async fn my_method(&self) -> Result<(), aragog::Error> }
-
#![allow(unused_variables)] fn main() { async fn my_method(&mut self) -> Result<(), aragog::Error> }
If you use
aragog
with theblocking
feature then this option will have no effect.
Hook with database access
#![allow(unused_variables)] fn main() { #[before_create(func = "my_method", db_access = true)] }
my_method can be either:
-
#![allow(unused_variables)] fn main() { fn my_method<D>(&self, db_access: &D) -> Result<(), aragog::Error> where D: aragog::DatabaseAccess }
-
#![allow(unused_variables)] fn main() { fn my_method<D>(&mut self, db_access: &D) -> Result<(), aragog::Error> where D: aragog::DatabaseAccess }
If you want to use the database access, using also
is_async = true
would be recommended
Technical notes
The Validate
derive case
If you derive the Validate trait, you may want validations to be launched automatically in hooks.
#![allow(unused_variables)] fn main() { #[derive(Serialize, Deserialize, Clone, Record, Validate)] #[before_write(func = "validate")] // This hook will launch validations before `create` and `save` pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } }
Forbidden method names
When using a hook like the following:
#[before_create(func("my_method"))]
The called method names can't be:
before_create_hook
before_save_hook
before_delete_hook
after_create_hook
after_save_hook
after_delete_hook
to avoid unexpected behaviors like unwanted recursive hooks.
Try using explicit names for your hooks
Direct implementation
You can implement Record
directly instead of deriving it.
We strongly suggest you derive the
Record
trait instead of implementing it, as in the future more hooks may be added to this trait without considering it breaking changes
You need to specify the COLLECTION_NAME
const which, when deriving, takes the name of the structure.
You also need to implement directly the different hooks.
Example:
#![allow(unused_variables)] fn main() { use aragog::Record; #[derive(Serialize, Deserialize, Clone)] pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } impl Record for User { const COLLECTION_NAME :&'static str = "User"; async fn before_create_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where D: DatabaseAccess + ?Sized { // Your implementation Ok(()) } async fn before_save_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where D: DatabaseAccess + ?Sized { // Your implementation Ok(()) } async fn before_delete_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where D: DatabaseAccess + ?Sized { // Your implementation Ok(()) } async fn after_create_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where D: DatabaseAccess + ?Sized { // Your implementation Ok(()) } async fn after_save_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where D: DatabaseAccess + ?Sized { // Your implementation Ok(()) } async fn after_delete_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where D: DatabaseAccess + ?Sized { // Your implementation Ok(()) } } }
Unstable hooks state
The hook macros work pretty well but are difficult to test, especially compilation errors: Are the errors messages relevant? correctly spanned? etc.
So please report any bug or strange behaviour as this feature is still in its early stages.
The Validate
trait
As aragog
is trying to be a complete ODM, being able to add validations to models is a useful feature.
Macro validations
Let's take the record example model:
#![allow(unused_variables)] fn main() { use aragog::{Record, Validate}; #[derive(Serialize, Deserialize, Clone, Record, Validate)] pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub roles: Vec<String>, pub age: usize, } }
We added the Validate
trait derive, making this model perform validations before being written to the database.
Field validations
We can add quite some pre-made validation operations as following:
#![allow(unused_variables)] fn main() { use aragog::{Record, Validate}; #[derive(Serialize, Deserialize, Clone, Record, Validate)] pub struct User { // The *username* field must contain exactly 10 characters #[validate(length = 10)] pub username: String, // The *username* field must have at least 3 characters #[validate(min_length = 3)] pub first_name: String, // The *username* field must have its lenth between 3 and 30 #[validate(min_length = 5, max_length = 30)] pub last_name: String, // Each role must have an exact length of 5 #[validate_each(length = 5)] pub roles: Vec<String>, // The *age* field must be at least 18 #[validate(greater_or_equal(18))] pub age: usize, } }
When trying to create or save a User
document all validations must match
or a Error::ValidationError
will be returned with an explicit message.
The current available field attribute validation operation macros (can be chained):
-
String
or string slice fields:length(VAL)
validates the field has exact length of VALmin_length(VAL)
validates the field has a minimum length of VALmax_length(VAL)
validates the field has a maximum length of VALregex(REGEX)
validates the field matches the REGEX regular expression (which can be consts likeValidate::SIMPLE_EMAIL_REGEX
)
-
Numeric fields or ordered types
greater_than(VAL)
validated the field is greater than VALgreater_or_equal(VAL)
validated the field is greater or equal to VALlesser_than(VAL)
validated the field is lesser than VALlesser_or_equal(VAL)
validated the field is lesser or equal to VAL
-
Vector fields or ordered iterable types
max_count(VAL)
validates the field has a maximal number of elements of VALmin_count(VAL)
validates the field has a minimal number of elements of VALcount(VAL)
validates the field has a number of elements of VAL
Note: These operations use the
Iterator::count
method which might be expensive -
Other
is_some
validates theOption
field contains a valueis_none
validates theOption
field does not contain a valuefunc(FUNC)
calls the FUNC method (see Extra Validations)call_validations
calls the validations of the field allowing to propagate the validation calls. The field must be an type implementingValidate
Note: The macro doesn't guarantee the order of validations
Validate comparison between custom types
The following validation operations:
greater_than(VAL)
greater_or_equal(VAL)
lesser_than(VAL)
lesser_or_equal(VAL)
Can allow custom types implementing PartialOrd
Note: It also requires
Display
to generate the error, andCopy
for the borrow checker
So you can use a custom struct or enum like this:
#![allow(unused_variables)] fn main() { #[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] enum CustomOrd { A, B, C, } #[derive(Validate)] struct Comparator { #[validate(lesser_than(CustomOrd::C), greater_than(CustomOrd::A))] pub field: CustomOrd, } }
Extra validations
On more complex cases, simple field validations are not enough and you may want to add custom validations.
use aragog::{Record, Validate};
#[derive(Serialize, Deserialize, Clone, Record, Validate)]
+ #[validate(func("custom_validations"))] // We added this global validation attribute on top of the struct
pub struct User {
+ #[validate(length = 10, func("validate_username"))] // We added this field validation attribute
pub username: String,
#[validate(min_length = 3)]
pub first_name: String,
#[validate(min_length = 5, max_length = 30)]
pub last_name: String,
#[validate(greater_or_equal(18))]
pub age: usize,
+ // These two fields require a more complex validation
+ pub phone: Option<String>,
+ pub phone_country_code: Option<String>,
}
+ impl User {
+ // We added the global custom validation method (uses multi-fields)
+ fn custom_validations(&self, errors: &mut Vec<String>) {
+ if self.phone.is_some() || self.phone_country_code.is_some() {
+ // We use built-in validation methods
+ Self::validate_field_presence("phone", &self.phone, errors);
+ Self::validate_field_presence("phone_country_code", &self.phone_country_code, erros);
+ }
+ }
+
+ // We added the field custom validation method (field-specific)
+ fn validate_username(field_name: &str, value: &str, errors: &mut Vec<String>) {
+ if value == "SUPERADMIN" {
+ // We can push our own validation errors
+ errors.push(format!("{} can't be SUPERADMIN", field_name))
+ }
+ }
+ }
The macro attribute #[validate(func("METHOD"))]"
must link to an existing method of your struct.
This method can follow various patterns:
- global validation method (top of the struct)
#![allow(unused_variables)] fn main() { fn my_method(&self, errors: &mut Vec<String>) {} }
- field validation method
#![allow(unused_variables)] fn main() { fn my_method(field_name: &str, field_value: &T, errors: &mut Vec<String>) {} }
T
being your field type
Note: The method can have any visibility and can return whatever you want
the errors
argument is a mutable array of error messages it contains all current errors and you can push your own errors in it.
When the validate()
method is called, this errors
vector is used to build the error message.
Technical notes
Validate
with Record
If you derive the Record trait, you may want validations to be launched automatically in record hooks.
#![allow(unused_variables)] fn main() { #[derive(Serialize, Deserialize, Clone, Record, Validate)] #[before_write(func = "validate")] // This hook will launch validations before `create` and `save` pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } }
Enum validations
Enums can derive Validate
but field validation attributes are not supported.
Forbidden method name
When using a custom validation method like the following:
#[validate(func("my_method"))]
The called method names can't be validations
to avoid unexpected behaviors like recursive validations.
This is caused by the Validate
method validations
already being built and called by the derive macro.
Try using explicit names for your custom validation methods
If your objective is to call the validations of a compound object implementing Validate
use the call_validations
operation.
Direct implementation
You can implement Validate
directly instead of deriving it.
We suggest you derive the
Validate
trait instead of implementing it unless you need specific operation order
You need to specify the validations
method which, when deriving is filled with all the macro attributes
Example:
#![allow(unused_variables)] fn main() { use aragog::Validate; pub struct User { pub username: String, pub first_name: String, pub last_name: String, pub age: usize } impl Validate for User { fn validations(&self, errors: &mut Vec<String>) { // Your validations } } }
Unstable state
The validation macros work pretty well but are difficult to test, especially the compilation errors: Are the errors messages relevant? correctly spanned? etc.
So please report any bug or strange behaviour as this feature is still in its early stages
The EdgeRecord
struct
This struct defines the OGM (Object Graph mapper) aspect of aragog
, an Edge Model that can be mapped to an ArangoDB edge document.
An Edge document is part of an Edge Collection, and links regular Documents together
To use edges you need to define a Record for your edge collection:
#![allow(unused_variables)] fn main() { use aragog::Record; #[derive(Serialize, Deserialize, Clone, Record)] pub struct ChildOf { pub notes: Option<String>, pub adopted: bool } }
The ChildOf collection must be an edge collection
And now ChildOf
can be used as a EdgeRecord<ChildOf>
.
Linking documents
ArangoDB edge documents have two mandatory additional fields:
_from
containing the id of the target document_to
containing the id of the target document
This fields are wrapped in the EdgeRecord
struct, this is why you don't need to worry about specifyng it yourself
In order to link two Person
:
#![allow(unused_variables)] fn main() { let parent = Person { first_name: String::from("Charles-Ange"), last_name: String::from("Surcouf"), }; let parent_record= DatabaseRecord::create(parent, &db_connection).await.unwrap(); let child = Person { first_name: String::from("Robert"), last_name: String::from("Surcouf") }; let child_record= DatabaseRecord::create(child, &db_connection).await.unwrap(); }
We can do this manually:
#![allow(unused_variables)] fn main() { let edge_document = EdgeRecord::new(parent_record.id(), child_record.id(), ChildOf { notes: None, adopted: false, }).unwrap(); let edge_record = DatabaseRecord::create(edge_document, db_connection).await.unwrap(); }
or use the safer built in method:
#![allow(unused_variables)] fn main() { let edge_record = DatabaseRecord::link(&parent_record, &child_record, &db_connection, ChildOf { notes: None, adopted: false, } }).await.unwrap(); }
In both cases we have edge_record
of type DatabaseRecord<EdgeRecord<ChildOf>>
.
Both DatabaseRecord and EdgeRecord implement
Deref
andDerefMut
towards the inner type so you can access inner values:#![allow(unused_variables)] fn main() { edge_record.adopted = true; }
Validation and hooks
EdgeRecord
validates the format of its _from
and _to
fields and calls the hooks of the inner document.
Retrieval
If you wish to retrieve an edge document from its key
or a query you need to specify the EdgeRecord
wrapper to use the edge document features.
example:
#![allow(unused_variables)] fn main() { // These will work but you won't have the `from` and `to` value let edge = ChildOf::find("key", &db_access).await.unwrap(); let edge: DatabaseRecord<ChildOf> = DatabaseRecord::find("key", &db_access).await.unwrap(); // These will work and retrieve also the `from`and `to` values let edge = EdgeRecord::<ChildOf>::find("key", &db_access).await.unwrap(); let edge: DatabaseRecord<EdgeRecord<ChildOf>> = DatabaseRecord::find("key", &db_access).await.unwrap(); }
The Query system
You can retrieve document from the database two ways:
aragog
provides an AQL query builder system, allowing safer queries than direct string literals.
For a created object like the following:
#![allow(unused_variables)] fn main() { #[derive(Serialize, Deserialize, Record, Clone)] struct User { first_name: String, last_name: String, age: u16, } let user = User { first_name: "Robert".to_string(), last_name: "Surcouf".to_string(), age: 25, }; DatabaseRecord::create(user, &database_connection).await.unwrap(); }
You can define a query:
#![allow(unused_variables)] fn main() { let query = User::query() .filter(Filter::new( Comparison::field("last_name").equals_str("Surcouf")) .and(Comparison::field("age").greater_than(15)) ); }
Typed querying
Typed querying will allow only one type of document to be retrieved, in this case User collection documents.
In case of corrupted documents they may not be returned, see safe querying for resilient querying
- Through
Record::get
:
#![allow(unused_variables)] fn main() { let result = User::get(&query, &database_connection).await.unwrap(); }
- Through
DatabaseRecord::get
(requires type):
#![allow(unused_variables)] fn main() { let result :QueryResult<User> = DatabaseRecord::get(&query, &database_connection).await.unwrap(); }
- Through
Query::call
(requires type):
#![allow(unused_variables)] fn main() { let result :QueryResult<User> = query.call(&database_connection).await.unwrap() }
Safe querying
safe querying will allow multiple types of document to be retrieved as json objects (UndefinedRecord
) and then dynamically parsed.
This version may be slightly slower, but you have a guarantee to retrieve all documents
- Through
Query::raw_call
:
#![allow(unused_variables)] fn main() { let result = query.raw_call(&database_connection).await.unwrap() }
- Through
DatabaseAccess::query
(requires type):
#![allow(unused_variables)] fn main() { let result = database_connection.query(query).await.unwrap(); }
The QueryResult<UndefinedRecord>
provides a get_records
method to dynamically retrieve custom Record
types.
Batch calls
Each and every query variant shown above have a batched version:
Record::get
=>Record::get_in_batches
DatabaseRecord::get
=>DatabaseRecord::get_in_batches
Query::call
=>Query::call_in_batches
Query::raw_call
=>Query::raw_call_in_batches
DatabaseAccess::query
=>DatabaseAccess::query_in_batches
They will return a QueryCursor
instead of a QueryResult
allowing to customize the number of returned document and easy iteration through the returned batches.
If you use the
blocking
feature,QueryCursor
has anIterator
implementation. Otherwise use thenext_batch
method
Query Object
You can initialize a query in the following ways:
- The recommended way:
Object::query()
(only works ifObject
implementsRecord
)
- Unsafe ways:
Query::new("CollectionName")
query!("CollectionName")
You can customize the query with the following methods:
filter()
you can specify AQL comparisonsprune()
you can specify blocking AQL comparisons for traversal queriessort()
you can specify fields to sort withlimit()
you can skip and limit the query resultsdistinct()
you can skip duplicate documents
The order of operations will be respected in the rendered AQL query (except for
distinct
)
Then you can call a query in the following ways:
query.call::<Object>(&database_connection)
Object::get(&query, &database_connection
Which will return a JsonQueryResult
containing a Vec
of serde_json::Value
.
JsonQueryResult
can return deserialized models as DatabaseRecord
by calling .get_records::<T>()
Filter
You can initialize a Filter
with Filter::new(comparison)
Each comparison is a Comparison
struct built via ComparisonBuilder
:
#![allow(unused_variables)] fn main() { // for a simple field comparison // Explicit Comparison::field("some_field").some_comparison("compared_value"); // Macro compare!(field "some_field").some_comparison("compared_value"); // for field arrays (see ArangoDB operators) // Explicit Comparison::all("some_field_array").some_comparison("compared_value"); // Macro compare!(all "some_field_array").some_comparison("compared_value"); // Explicit Comparison::any("some_field_array").some_comparison("compared_value"); // Macro compare!(any "some_field_array").some_comparison("compared_value"); // Explicit Comparison::none("some_field_array").some_comparison("compared_value"); // Macro compare!(none "some_field_array").some_comparison("compared_value"); }
All the currently implemented comparison methods are listed under ComparisonBuilder documentation page.
Filters can be defined explicitly like this:
#![allow(unused_variables)] fn main() { let filter = Filter::new(Comparison::field("name").equals_str("felix")); }
or
#![allow(unused_variables)] fn main() { use aragog::query::{Filter, Comparison}; let filter :Filter = Comparison::field("name").equals_str("felix").into(); }
Example
#![allow(unused_variables)] fn main() { let query = Query::new("Company").filter( Filter::new( Comparison::field("company_name").not_like("%google%")) .and(Comparison::field("company_age").greater_than(15)) .or(Comparison::any("emails").like("%gmail.com")) .and(Comparison::field("roles").in_str_array(&["SHIPPER", "FORWARDER"])) ); }
Traversal Querying
You can use graph features with sub-queries with different ways:
Straightforward Traversal query
- Explicit way
#![allow(unused_variables)] fn main() { let query = Query::outbound(1, 2, "edgeCollection", "User/123"); let query = Query::inbound(1, 2, "edgeCollection", "User/123"); let query = Query::any(1, 2, "edgeCollection", "User/123"); // Named graph let query = Query::outbound_graph(1, 2, "NamedGraph", "User/123"); let query = Query::inbound_graph(1, 2, "NamedGraph", "User/123"); let query = Query::any_graph(1, 2, "NamedGraph", "User/123"); }
- Implicit way from a
DatabaseRecord<T>
#![allow(unused_variables)] fn main() { let query = user_record.outbound_query(1, 2, "edgeCollection"); let query = user_record.inbound_query(1, 2, "edgeCollection"); // Named graph let query = user_record.outbound_graph(1, 2, "NamedGraph"); let query = user_record.inbound_graph(1, 2, "NamedGraph"); }
Sub queries
Queries can be joined together through
- Edge traversal:
#![allow(unused_variables)] fn main() { let query = Query::new("User") .join_inbound(1, 2, false, Query::new("edgeCollection")); }
- Named Graph traversal:
#![allow(unused_variables)] fn main() { let query = Query::new("User") .join_inbound(1, 2, true, Query::new("SomeGraph")); }
It works with complex queries:
#![allow(unused_variables)] fn main() { let query = Query::new("User") .filter(Comparison::field("age").greater_than(10).into()) .join_inbound(1, 2, false, Query::new("edgeCollection") .sort("_key", None) .join_outbound(1, 5, true, Query::new("SomeGraph") .filter(Comparison::any("roles").like("%Manager%").into()) .distinct() ) ); }
Transactions
aragog
supports transactional operations without using a specific API thanks to the new Transaction
Object.
Creating a transaction
To initiate a transaction we need to use the DatabaseConnection
, as the transaction will create an equivalent DatabaseAccess
object:
The TransactionDatabaseConnection
, that can be used instead of the classic connection to use the transactional features.
Example:
#![allow(unused_variables)] fn main() { // We build the connection let database_connection = DatabaseConnection::builder() .build().await.unwrap(); // We instantiate a new transaction let transaction = Transaction::new(&database_connection).await.unwrap(); }
Transaction states
An ArangoDB transaction has three states:
- Running
- Committed
- Aborted
After successfully initializing a Transaction
Object, a Running transaction is created.
We can now use its connection:
Example:
#![allow(unused_variables)] fn main() { let database_connection = DatabaseConnection::builder() .build().await.unwrap(); // Instantiate a new transaction let transaction = Transaction::new(&database_connection).await.unwrap(); // Retrieve the connection let transaction_connection = transaction.database_connection(); // We use the transaction connection instead of the classic connection DatabaseRecord::create( Dish { name: "Pizza".to_string(), price: 10, }, transaction_connection ).await.unwrap(); // We commit the transaction transaction.commit().await.unwrap(); }
The create operations is using the transaction, meaning it won't be written in ArangoDB until the transaction is committed. The operation will simply be cancelled if the transaction is aborted.
Make sure to always commit or abort a transaction !
Safe execution
To avoid remembering to commit and manually handling when to abort a transaction, prefer using the safe execution.
The safe execution allows to execute multiple operations in a block and make sure the transaction is either committed or aborted.
#![allow(unused_variables)] fn main() { let database_connection = DatabaseConnection::builder() .build().await.unwrap(); // Instantiate a new transaction let transaction = Transaction::new(&database_connection).await.unwrap(); // Safely execute operations: let output = transaction.safe_execute(|transaction_connection| async move { // We use the provided `transaction_connection` instead of the classic connection DatabaseRecord::create(Dish { name: "Pizza".to_string(), price: 10, }, &transaction_connection).await?; DatabaseRecord::create(Dish { name: "Pasta".to_string(), price: 8, }, &transaction_connection).await?; DatabaseRecord::create(Dish { name: "Sandwich".to_string(), price: 5, }, &transaction_connection).await?; // You can return any type of data here Ok(()) }).await.unwrap(); // The output allows to check the transaction state: Committed or Aborted assert!(output.is_committed()); }
If an operation fails in the safe_execute
block the transaction will be aborted and every operation cancelled.
Don't use
unwrap()
or any panicking functions in the block as the transaction won't be aborted.
The safe_execute
method returns a TransactionOutput
if everything went correctly (No Database or connection errors).
This output allows to check the state of the transaction, Aborted or Committed and retrieve the result of the block
stored as a generic.
Note: Transactions can be committed multiple times, so feel free to use multiple safe execution blocks.
Warning: An aborted transaction can no longer be committed ! Make sure to handle the
TransactionOuput
cases.
Custom transactions
The Transaction
object implements a builder pattern through TransactionBuilder
Restricted transactions
The Transaction::new
pattern build a valid transaction for all collections (defined in the schema).
You may want more restricted transactions, limited to a single Collection.
All structs deriving Record
, here User, have access to:
User::transaction
building a transaction on this collection only.User::transaction_builder
returning a builder for a transaction on this collection only.
Restricted transaction will fail if you try to Write on another collection.
Transaction options
The TransactionBuilder
allows optional parameters:
wait_for_sync
, forcing the transaction to be synced to the database on commit.lock_timeout
, specifying the transaction lock timeout (set to 60000 by default)collections
, specifying the allowed collections (all collections are allowed by default)
Technical notes
The transactions use the ArangoDB steam transaction API.