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::Table
has a structure - fields, joins and relations are defined dynamically.sql::Query
on other hand is transient. It consists of smaller pieces which we callsql::Chunk
.sql::Table
can create and returnsql::Query
object 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 query
operation on a client set for fieldid
. - Vantage creates
orders
table and addscondition
onclient_id
field. - Vantage executes
field query
operation onorders
table for fieldid
. - Vantage creates
order_lines
table and addscondition
onorder_id
field.
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 bothReadableDataSet
andWritableDataSet
.sql::Query
- implementsReadableDataSet
only.
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_some
andget-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_query
but 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
Chunk
elsewhere
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.