Skip to content

Commit

Permalink
Simplify even further! (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
romaninsh authored Nov 21, 2024
1 parent 7572f97 commit a97d1df
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 118 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@

DORM is a type-safe, ergonomic database toolkit for Rust that focuses on developer productivity
without compromising performance. It allows you to work with your database using Rust's strong type
system while abstracting away the complexity of SQL querie. (Support for NoSQL databases is coming soon)
system while abstracting away the complexity of SQL queries. (Support for NoSQL databases is coming soon)

## Quick Start

Your application would typically require a model definition. Here is example:
[bakery_example](bakery_example/src/). You would also need a Postgres database populated with sample data
from [schema-pg.sql](bakery_example/schema-pg.sql) and create role `postgres`.
[bakery_model](bakery_model/src/). You would also need a Postgres database populated with sample data
from [schema-pg.sql](bakery_model/schema-pg.sql) and create role `postgres`.

Once this is in place, you can use DORM to interract with your data like this:

```rust
use dorm::prelude::*;
use bakery_example::*;
use bakery_model::*;

let set_of_clients = Client::table(); // Table<Postgres, Client>

Expand Down Expand Up @@ -57,7 +57,7 @@ WHERE client_id IN (SELECT id FROM client WHERE is_paying_client = true)
```

This illustrates how DORM combined specific rules of your code such as "only paying clients" with
the rules defined in the bakery_model, like "soft-delete enabled for Orders" and "prices are
the rules defined in the [bakery_model](bakery_model/src/), like "soft-delete enabled for Orders" and "prices are
actually stored in product table" and "order has multiple line items" to generate a single
and efficient SQL query.

Expand All @@ -67,7 +67,7 @@ and efficient SQL query.
- 🥰 **Complexity Abstraction** - Hide complexity away from your business logic
- 🚀 **High Performance** - Generates optimal SQL queries
- 🔧 **Zero Boilerplate** - No code generation or macro magic required
- 🧪 **Testing Ready** - First-class support for mocking and testing
- 🧪 **Testing Ready** - First-class support for mocking and unit-testing
- 🔄 **Relationship Handling** - Elegant handling of table relationships and joins
- 📦 **Extensible** - Easy to add custom functionality and non-SQL support

Expand All @@ -76,9 +76,11 @@ and efficient SQL query.
DORM is still in development. It is not in crates.io yet, so to install it you will need to clone
this repository and link it to your project manually.

If you like what you see so far - reach out to me on BlueSky: [nearly.guru](https://bsky.app/profile/nearly.guru)

## Introduction

(You ran run this [example](bakery_example/examples/0-intro.rs) with `cargo run --example 0-intro`)
(You can run this [example](bakery_model/examples/0-intro.rs) with `cargo run --example 0-intro`)

DORM interract with your data through a unique concept called "Data Sets". Your application will
work with different sets suc has "Set of Clients", "Set of Orders" and "Set of Products" etc.
Expand Down
124 changes: 13 additions & 111 deletions bakery_model/examples/0-intro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,9 @@ async fn create_bootstrap_db() -> Result<()> {
async fn main() -> Result<()> {
create_bootstrap_db().await?;

// Welcome to DORM demo.
// This example is explained in README.md <https://github.com/romaninsh/dorm>.
//
// DORM allows you to create types for your "Data Sets". It's easier to explain with example.
// Your SQL table "clients" contains multiple client records. We do not know if there are
// 10 clients or 100,000 in the table. We simply refer to them as "set of clients"

// Use a set of our clients as a type:
let set_of_clients = Client::table();

// As you would expect, you can iterate over clients easily.
Expand All @@ -38,15 +35,11 @@ async fn main() -> Result<()> {
println!("-------------------------------------------------------------------------------");
/////////////////////////////////////////////////////////////////////////////////////////

// In production applications you wouldn't be able to iterate over all the records like this,
// simply because of the large number of records. Which is why we need to narrow down our
// set_of_clients:

// Create and apply conditions to create a new set:
let condition = set_of_clients.is_paying_client().eq(&true);
let paying_clients = set_of_clients.with_condition(condition);

// Some operation do not require us to fetch all records. For instance if we just need to know
// count of paying clients we can use count():
// Generate count() Query from Table<Postgres, Client> and execute it:
println!(
"Count of paying clients: {}",
paying_clients.count().get_one_untyped().await?
Expand All @@ -56,49 +49,34 @@ async fn main() -> Result<()> {
println!("-------------------------------------------------------------------------------");
/////////////////////////////////////////////////////////////////////////////////////////

// Now that you have some idea of what a DataSet is, lets look at how we can reference
// related sets. Traditionally we could say "one client has many orders". In DORM we say
// "set of orders that reference set of clients". In this paradigm we only operate with
// "many-to-many" relationships.

// Traverse relationships to create order set:
let orders = paying_clients.ref_orders();

// Lets pay attention to the type here:
// set_of_cilents = Table<Postgres, Client>
// paying_clients = Table<Postgres, Client>
// orders = Table<Postgres, Order>
//
// Type is automatically inferred, I do not need to specify it. This allows me to define
// a custom method on Table<Postgres, Order> only and use it like this:

// Execute my custom method on Table<Postgres, Order> from bakery_model/src/order.rs:
let report = orders.generate_report().await?;
println!("Report:\n{}", report);

// Implementation for `generate_report` method is in bakery_model/src/order.rs and can be
// used anywhere. Importantly - this file also includes a unit-test for `generate_report`.
// My test uses a mock data source and is super fast, which is very important for large
// applications.
// Using this method is safe, because it is unit-tested.

/////////////////////////////////////////////////////////////////////////////////////////
println!("-------------------------------------------------------------------------------");
/////////////////////////////////////////////////////////////////////////////////////////

// One thing that sets DORM apart from other ORMs is that we are super-efficient at building
// queries. DataSets have a default entity type (in this case - Order) but we can supply
// our own type:

// Queries are built by understanding which fields are needed. Lets define a new entity
// type:
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
struct MiniOrder {
id: i64,
client_id: i64,
}
impl Entity for MiniOrder {}

// Entity (and dependant traits) are needed to load and store "MiniOrder" in our DataSet.
// Next I'll use `get_some_as` which gets just a single record. The subsequent
// scary-looking `get_select_query_for_struct` is just to grab and display the query
// to you, which would look like: SELECT id, client_id FROM .....

// Load a single order by executing a query like SELECT id, client_id FROM .....
let Some(mini_order) = orders.get_some_as::<MiniOrder>().await? else {
panic!("No order found");
};
Expand All @@ -110,8 +88,8 @@ async fn main() -> Result<()> {
.preview()
);

// Next lets assume, that we also want to know "order total" and "client name" in the next
// use-case.
// MegaOrder defines `client_name` and `total` - those are not physical fields, but rather
// defined through expressions/subqueries from related tables.
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
struct MegaOrder {
id: i64,
Expand All @@ -120,6 +98,7 @@ async fn main() -> Result<()> {
}
impl Entity for MegaOrder {}

// The code is almost identical to the code above, but the query is more complex.
let Some(mini_order) = orders.get_some_as::<MegaOrder>().await? else {
panic!("No order found");
};
Expand All @@ -131,83 +110,6 @@ async fn main() -> Result<()> {
.preview()
);

// OH WOW!! If you are have managed to run this code:
// > cargo run --example 0-intro
//
// You might be surprised about thequeries that were generated for you. They look scary!!!!
//
// SELECT id, client_id
// FROM ord
// WHERE client_id IN (SELECT id FROM client WHERE is_paying_client = true)
// AND is_deleted = false;
//
// Our struct only needed two fields, so only two fields were queried. That's great.
//
// You can also probably understand why "is_paying_client" is set to true. Our Order Set was derived
// from `paying_clients` Set which was created through adding a condition. Why is `is_deleted` here?
//
// As it turns out - our table definition is using extension `SoftDelete`. In the `src/order.rs`:
//
// table.with_extension(SoftDelete::new("is_deleted"));
//
// The second query is even more interesting:
//
// SELECT id,
// (SELECT name FROM client WHERE client.id = ord.client_id) AS client_name,
// (SELECT SUM((SELECT price FROM product WHERE id = product_id) * quantity)
// FROM order_line WHERE order_line.order_id = ord.id) AS total
// FROM ord
// WHERE client_id IN (SELECT id FROM client WHERE is_paying_client = true)
// AND is_deleted = false;
//
// There is no physical fied for `client_name` and instead DORM sub-queries
// `client` table to get the name. Why?
//
// The implementation is, once again, inside `src/order.rs` file:
//
// table
// .with_one("client", "client_id", || Box::new(Client::table()))
// .with_imported_fields("client", &["name"])
//
// The final field - `total` is even more interesting - it gathers information from
// `order_line` that holds quantities and `product` that holds prices.
//
// Was there a chunk of SQL hidden somewhere? NO, It's all DORM's query building magic.
//
// Look inside `src/order.rs` to see how it is implemented:
//
// table
// .with_many("line_items", "order_id", || Box::new(LineItem::table()))
// .with_expression("total", |t| {
// let item = t.sub_line_items();
// item.sum(item.total()).render_chunk()
// })
//
// Something is missing. Where is multiplication? Apparently item.total() is
// responsible for that, we can see that in `src/lineitem.rs`.
//
// table
// .with_one("product", "product_id", || Box::new(Product::table()))
// .with_expression("total", |t: &Table<Postgres, LineItem>| {
// t.price().render_chunk().mul(t.quantity())
// })
// .with_expression("price", |t| {
// let product = t.get_subquery_as::<Product>("product").unwrap();
// product.field_query(product.price()).render_chunk()
// })
//
// We have discovered that behind a developer-friendly and very Rust-intuitive Data Set
// interface, DORM offers some really powerful features to abstract away complexity.
//
// What does that mean to your developer team?
//
// You might need one or two developers to craft those entities, but the rest of your
// team can focus on the business logic - like improving that `generate_report` method!
//
// This highlights the purpose of DORM - separation of concerns and abstraction of complexity.
//
// Use DORM. No tradeoffs. Productive team! Happy days!
//
// To continue learning, visit: <https://romaninsh.github.io/dorm>, Ok?
Ok(())
}

0 comments on commit a97d1df

Please sign in to comment.