Entities and Schemas
While there are others for more specialized tasks, interacting with the microrm API starts with two derive macros: Entity and Schema. The first defines structured data types – entities – and the second defines collections of entities and some of their relations to make a comprehensive data type that describes the entire database. The schema type, along with microrm’s generic query interface, form what is sometimes called a “data access object”, or DAO.
For a simple example, suppose you want to store a set of configuration key-value pairs; the resulting specification could be something like the following:
#[derive(Entity)]
struct ConfigPair {
#[key]
key: String,
value: String,
}
#[derive(Schema)]
struct ConfigSchema {
config: microrm::Table<ConfigPair>,
}
The optional #[key] attribute, when applied to one or more fields, defines the primary key for the entity — a set of fields that collectively represent unique entities. You can define more than one search key for an entity, but everything beyond the primary key must be defined externally to the entity; see the section on indices for more information. Relatedly, the Table type specifies a standard database table for the entity parameter, and is the usual way to access entity instances. Finally, note that all fields must be of a type that implements Datum; see the chapter on Datums for more information.
With the above schema, you could then immediately begin writing and reading data to the database, as we’ll see in a minute. microrm maintains multiple handles to the database and uses a connection pool to allow tasks to borrow one of the database handles for a transaction. Transactions have a specific meaning in the context of databases; if you aren’t familiar with them, a good way to think of it is as an atomic unit of ‘work’ for the database, a set of reads and writes. Importantly, transactions have no success guarantees and can be aborted if the database engine detects a conflict. microrm provides an API that will retry transactions automatically if you so choose, or you may wish for the transaction abort error to bubble up if you have a more specific context.
Opening a connection to the database and doing some simple queries is now very straightforwards:
use microrm::prelude::*;
// connect...
let (cpool, schema) = microrm::ConnectionPool::open::<ConfigSchema>("path-to-db").expect("failed to connect to database");
// write some data
cpool.run_transaction(1, |txn| {
schema.config.insert(
txn,
ConfigPair {
key: "cache_path".into(),
value: "$HOME/.cache/...".into(),
}
)?;
Ok(())
}).expect("failed to write to database");
// read some data
let lang = cpool.run_transaction(
1,
// access via the primary key
|txn| schema.config.keyed("lang").get(txn)
}).expect("failed to read from database");
// lang is of type Option<microrm::Stored<ConfigPair>>
// read all the data
let config_pairs = cpool.run_transaction(
1,
|txn| schema.config.get(txn)
).expect("failed to read from database");
// config_pairs is of type Vec<microrm::Stored<ConfigPair>>
In general, queries are done by selecting a schema item (such as an Table) from the schema object and then applying qualifing clauses. Here keyed() uses the primary key to select an unique entity, and thus the return type of the query is an Option<>. The Stored type is a transparent wrapper that also contains the entity’s database ID, and can be used to synchronize local changes to an entity back to the database via sync().