Data Sets
Traditional ORMs operate with records. If you have used SeaORM or Diesel - you need to temporarily "un-learn" those. Vantage syntax may look similar, but it's very different to the classic ORM pattern:
-
Vantage operates with Data Sets. A set can contain either a single record, no records or a huge number of records. Set represents records in remote database (or API) and does not store anything in memory.
-
Vantage executes operations remotely. Changing multiple records in ORM involves fetching all the records, modifying them and saving them back. Vantage prefers to execute changes remotely, if underlying DataSource supports it.
As a developer, you will always know when Vantage interacts with the database, because those operations are async. Majority of Vantage operations - like traversing relationship, calculating sub-queries - those are sync.
sql::Table and sql::Query
Vantage implements sql::Table and sql::Query - because we all love SQL. However you can define other data sources - such as NoSQL, REST API, GraphQL, etc. Those extensions do not need to be in same crate as Vantage. For the rest of this book I will only focus on SQL as a predominant use-case.
Vantage is quite fluid in the way how you use table and query, you can use one to
compliment or create another, but they serve different purpose:
sql::Tablehas a structure - fields, joins and relations are defined dynamically.sql::Queryon other hand is transient. It consists of smaller pieces which we callsql::Chunk.sql::Tablecan create and returnsql::Queryobject from various methods
sql::Chunk trait that is implemented by sql::Query (or other arbitrary expressions)
acts as a glue betwene tables. For instance when you run traverse relations:
#![allow(unused)] fn main() { let client = Client::table().with_id(1); let order_lines = client.ref_orders().ref_order_lines(); }
- Vantage executes
field queryoperation on a client set for fieldid. - Vantage creates
orderstable and addsconditiononclient_idfield. - Vantage executes
field queryoperation onorderstable for fieldid. - Vantage creates
order_linestable and addsconditiononorder_idfield.
Vantage weaves between sql::Table and sql::Query without reaching out to the
database behind a simple and intuitive code.
ReadableDataSet and WritableDataSet
Vantage provides two traits: ReadableDataSet and WritableDataSet. As name
suggests - you can fetch records from Readable set. You can add, modify or delete
records in Writable set.
Vantage implements those traits:
sql::Table- implements bothReadableDataSetandWritableDataSet.sql::Query- implementsReadableDataSetonly.
Operating with Data sets
At the very basic level - you can iterate through a readable data set.
#![allow(unused)] fn main() { let set_of_clients = Client::table(); for client in set_of_clients.get().await? { println!("{}", client.name); } }
There are more ways to fetch data from ReadableDataSet:
get- returns all records in a Vec using default entity typeget_as- return all records using a custom typeget_all_untyped- return all records as a raw JSON objectget_someandget-some_as- return only one record (or none)get_row_untyped- return single record as a raw JSON objectget_col_untyped- return only a single column as a raw JSON valuesget_one_untyped- return first column of a first row as a raw JSON value
In most cases you would use get and get_some:
#![allow(unused)] fn main() { let client = Client::table().with_id(1); let Some(client_data) = client.get_some().await? else { // fetch single record // handle error }; for client_order in client.orders().get().await? { // fetch multiple records println!("{}", client_order.id); } }
Creating Queries from Tables
Sometimes you do not want result, but would prefer a query object instead. This gives you a chance to tweak a query or use it elsewhere.
Vantage provides trait TableWithQueries that generates Query objects for you:
get_empty_query- returns a query with conditions and joins, but no fieldsget_select_query- likeget_empty_querybut adds all physical fieldsget_select_query_for_field_names- Provided with a slice of field names and expressions, only includes those into a query.get_select_query_for_field- Provided a query for individual field or expression, which you have to pass through an argument.get_select_query_for_fields- Provided a query for multiple fields
There are generally two things you can do with a query:
- Tweak and execute it
- Use it as a
Chunkelsewhere
Tweaking and Executing a query
#![allow(unused)] fn main() { let vip_orders = Client::table().add_condition(Client::table().is_vip().eq(true)).ref_orders(); let query = vip_orders .get_select_query_for_field_names(&["id", "client_id", "client"]) // excluding `total` here .with_column("total".to_string(), expr!("sum({})", vip_orders.total())) // add as aggregate .with_column("order_count".to_string(), expr!("count(*)")) .with_group_by(vip_orders.client_id()); let result = postgres().query_raw(&query).await?; }
Using a query as a Chunk elsewhere
#![allow(unused)] fn main() { // TODO - hypothetical example, not implemented in bakery_model let product_123 = Product::table().with_code("PRD-123"); let john = Client::table().with_email("john@example.com"); let new_order = Order::table() .insert(Order { product: product_123, client: john, quantity: 1, }).await?; }
#![allow(unused)] fn main() { // TODO: test let john = Client::table().with_email("john@example.com"); let order = Order::table() .with_condition(Order::table().client_id().in(john.get_select_query_for_field(john.id()))) }
Method get_ref() does exactly that, when you traverse relationships.
Conclusion
DataSet is a powerful concept that sets aside Vantage from the usual ORM pattern.
sql::Table and sql::Query are the building blocks you interract with most
often in Vantage.
Understanding this would allow you to implement missing features (such as table grouping) and eventually devleop extensions for your data model.