24 releases
new 0.5.0-rc.2 | Nov 12, 2024 |
---|---|
0.4.4 | Oct 2, 2024 |
0.4.0-rc.2 | Apr 14, 2024 |
0.3.12 | Nov 23, 2023 |
0.3.5 | Jul 29, 2022 |
#1757 in Database interfaces
206 downloads per month
210KB
5K
SLoC
microrm is a simple object relational manager (ORM) for sqlite.
Unlike many fancier ORM systems, microrm is designed to be lightweight, both in terms of runtime overhead and developer LoC. By necessity, it sacrifices flexibility towards these goals, and so can be thought of as more opinionated than, say, SeaORM or Diesel. Major limitations of microrm are:
- lack of database migration support
- limited vocabulary for describing object-to-object relations
microrm pushes the Rust type system somewhat to provide better ergonomics, so
the MSRV is currently 1.75. Don't be scared off by the web of traits in the
schema
module --- you should never need to interact with any of them!
lib.rs
:
microrm is a simple object relational manager (ORM) for sqlite.
Unlike many fancier ORM systems, microrm is designed to be lightweight, both in terms of runtime overhead and developer LoC. By necessity, it sacrifices flexibility towards these goals, and so can be thought of as more opinionated than, say, SeaORM or Diesel. The major limitations of microrm are lack of migrations (though support is planned!) and somewhat limited vocabulary for describing object-to-object relations. Despite this, microrm is usually powerful enough for most usecases where sqlite is appropriate.
There are three externally-facing components in microrm:
- Schema modelling (mostly by the
Datum
andEntity
traits) - Database querying (via
query::Queryable
,query::RelationInterface
andquery::Insertable
traits) - Command-line interface generation via the
clap
crate (seecli::Autogenerate
andcli::EntityInterface
; requires the optional crate featureclap
)
microrm pushes the Rust type system somewhat to provide better ergonomics, so the MSRV is
currently 1.75. Don't be scared off by the web of traits in the schema
module --- you should
never need to interact with most of them unless you're doing schema reflection.
Examples
KV-store
For the simplest kind of database schema, a key-value store, one possible microrm implementation of it might look like the following:
use microrm::prelude::*;
#[derive(Entity)]
struct KVEntry {
#[key]
key: String,
value: String,
}
#[derive(Default, Schema)]
struct KVSchema {
kvs: microrm::IDMap<KVEntry>,
}
let cpool = microrm::ConnectionPool::new(":memory:")?;
let mut lease = cpool.acquire()?;
let schema = KVSchema::default();
schema.install(&mut lease)?;
schema.kvs.insert(&mut lease, KVEntry {
key: "example_key".to_string(),
value: "example_value".to_string()
})?;
// can get with a String reference
assert_eq!(
schema.kvs.keyed(&String::from("example_key")).get(&mut lease)?.map(|v| v.value.clone()),
Some("example_value".to_string()));
// thanks to the QueryEquivalent trait, we can also just use a plain &str
assert_eq!(
schema.kvs.keyed("example_key").get(&mut lease)?.map(|v| v.value.clone()),
Some("example_value".to_string()));
// obviously, if we get another KV entry with a missing key, it doesn't come back...
assert_eq!(schema.kvs.keyed("another_example_key").get(&mut lease)?.is_some(), false);
// note that the above all return an Option<Stored<T>>. when using filters on arbitrary
// columns, a Vec<Stored<T>> is returned:
assert_eq!(
schema
.kvs
// note that the column constant uses CamelCase
.with(KVEntry::Value, "example_value")
.get(&mut lease)?
.into_iter()
.map(|v| v.wrapped().value).collect::<Vec<_>>(),
vec!["example_value".to_string()]);
Simple e-commerce schema
The following is an example of what a simple e-commerce website's schema might look like, tracking products, users, and orders.
use microrm::prelude::*;
#[derive(Entity)]
pub struct ProductImage {
// note that because this references an entity's autogenerated ID type,
// this is a foreign key. if the linked product is deleted, the linked
// ProductImages will also be deleted.
pub product: ProductID,
pub img_data: Vec<u8>,
}
#[derive(Entity, Clone)]
pub struct Product {
#[key]
pub title: String,
pub longform_body: String,
pub images: microrm::RelationMap<ProductImage>,
pub cost: f64,
}
// define a relation between customers and orders
pub struct CustomerOrders;
impl microrm::Relation for CustomerOrders {
type Domain = Customer;
type Range = Order;
const NAME: &'static str = "CustomerOrders";
// at most one customer per order
const INJECTIVE: bool = true;
}
#[derive(Entity)]
pub struct Customer {
pub orders: microrm::RelationDomain<CustomerOrders>,
// mark as part of the primary key
#[key]
pub email: String,
// enforce uniqueness of legal names
#[unique]
pub legal_name: String,
// elide the secrets from Debug output
#[elide]
pub password_salt: String,
#[elide]
pub password_hash: String,
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub enum OrderState {
AwaitingPayment,
PaymentReceived { confirmation: String },
ProductsReserved,
Shipped { tracking_no: String },
OnHold { reason: String },
}
#[derive(Entity)]
pub struct Order {
// use an ordinary type and do transparent JSON de/serialization
pub order_state: microrm::Serialized<Vec<OrderState>>,
pub customer: microrm::RelationRange<CustomerOrders>,
pub shipping_address: String,
// we may not have a billing address
pub billing_address: Option<String>,
// we'll assume for now that there's no product multiplicities, i.e. `contents` is not a multiset
pub contents: microrm::RelationMap<Product>,
}
#[derive(Default, Schema)]
pub struct ECommerceDB {
pub products: microrm::IDMap<Product>,
pub customers: microrm::IDMap<Customer>,
pub orders: microrm::IDMap<Order>,
}
// open a database instance
let cpool = microrm::ConnectionPool::new(":memory:")?;
let mut lease = cpool.acquire()?;
let schema = ECommerceDB::default();
schema.install(&mut lease);
// add an example product
let widget1 = schema.products.insert_and_return(&mut lease, Product {
title: "Widget Title Here".into(),
longform_body: "The first kind of widget that WidgetCo produces.".into(),
cost: 100.98,
images: Default::default()
})?;
// add an image for the product
widget1.images.insert(&mut lease, ProductImage {
product: widget1.id(),
img_data: [/* image data goes here */].into(),
});
// sign us up for this most excellent ecommerce website
let customer1 = schema.customers.insert_and_return(&mut lease, Customer {
email: "your@email.here".into(),
legal_name: "Douglas Adams".into(),
password_salt: "pepper".into(),
password_hash: "browns".into(),
orders: Default::default(),
})?;
// put in an order for the widget!
let mut order1 = schema.orders.insert_and_return(&mut lease, Order {
order_state: vec![OrderState::AwaitingPayment].into(),
customer: Default::default(),
shipping_address: "North Pole, Canada, H0H0H0".into(),
billing_address: None,
contents: Default::default(),
})?;
order1.contents.connect_to(&mut lease, widget1.id())?;
order1.customer.connect_to(&mut lease, customer1.id())?;
// Now get all products that customer1 has ever ordered
let all_ordered = customer1.orders.join(Order::Contents).get(&mut lease)?;
assert_eq!(all_ordered, vec![widget1]);
// process the payment for our order by updating the entity
order1.order_state.as_mut().push(
OrderState::PaymentReceived {
confirmation: "money received in full, i promise".into()
}
);
// now synchronize the entity changes back into the database
order1.sync(&mut lease)?;
Dependencies
~24MB
~467K SLoC