Compare commits
2 Commits
f7cdcb1563
...
d82f9fe2f4
Author | SHA1 | Date | |
---|---|---|---|
d82f9fe2f4 | |||
59eb96b9c8 |
156
README.md
156
README.md
@ -1 +1,155 @@
|
||||
# Georm, a simple, opiniated SQLx ORM for PostgreSQL
|
||||
<h1 align="center">Georm</h1>
|
||||
<div align="center">
|
||||
<strong>
|
||||
A simple, opinionated SQLx ORM for PostgreSQL
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
<!-- Github Actions -->
|
||||
<a href="https://github.com/phundrak/georm/actions/workflows/ci.yaml?query=branch%3Amain">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/phundrak/georm/ci.yaml?branch=main&style=flat-square" alt="actions status" /></a>
|
||||
<!-- Version -->
|
||||
<a href="https://crates.io/crates/georm">
|
||||
<img src="https://img.shields.io/crates/v/georm.svg?style=flat-square"
|
||||
alt="Crates.io version" /></a>
|
||||
<!-- Discord -->
|
||||
<!-- Docs -->
|
||||
<a href="https://docs.rs/georm">
|
||||
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" /></a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<h4>What is Georm?</h4>
|
||||
</div>
|
||||
|
||||
Georm is a quite simple ORM built around
|
||||
[SQLx](https://crates.io/crates/sqlx) that gives access to a few
|
||||
useful functions when interacting with a database, implementing
|
||||
automatically the most basic SQL interactions you’re tired of writing.
|
||||
|
||||
<div align="center">
|
||||
<h4>Why is Georm?</h4>
|
||||
</div>
|
||||
|
||||
I wanted an ORM that’s easy and straightforward to use. I am aware
|
||||
some other projects exist, such as
|
||||
[SeaORM](https://www.sea-ql.org/SeaORM/), but they generally don’t fit
|
||||
my needs and/or my wants of a simple interface. I ended up writing the
|
||||
ORM I wanted to use.
|
||||
|
||||
<div align="center">
|
||||
<h4>How is Georm?</h4>
|
||||
</div>
|
||||
|
||||
I use it in a few projects, and I’m quite happy with it right now. But
|
||||
of course, I’m open to constructive criticism and suggestions!
|
||||
|
||||
<div align="center">
|
||||
<h4>How can I use it?</h4>
|
||||
</div>
|
||||
|
||||
Georm works with SQLx, but does not re-export it itself. To get
|
||||
started, install both Georm and SQLx in your Rust project:
|
||||
|
||||
```sh
|
||||
cargo add sqlx --features postgres,macros # and any other feature you might want
|
||||
cargo add georm
|
||||
```
|
||||
|
||||
As Georm relies heavily on the macro
|
||||
[`query_as!`](https://docs.rs/sqlx/latest/sqlx/macro.query_as.html),
|
||||
the `macros` feature is not optional. Declare your tables in your
|
||||
Postgres database (you may want to use SQLx’s `migrate` feature for
|
||||
this), and then declare their equivalent in Rust.
|
||||
|
||||
```sql
|
||||
CREATE TABLE biographies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE authors (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
biography_id INT,
|
||||
FOREIGN KEY (biography_id) REFERENCES biographies(id)
|
||||
);
|
||||
```
|
||||
|
||||
```rust
|
||||
pub struct Author {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
```
|
||||
|
||||
To link a struct to a table in your database, derive the
|
||||
`sqlx::FromRow` and the `georm::Georm` traits.
|
||||
```rust
|
||||
#[derive(sqlx::FromRow, Georm)]
|
||||
pub struct Author {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
```
|
||||
|
||||
Now, indicate with the `georm` proc-macro which table they refer to.
|
||||
```rust
|
||||
#[derive(sqlx::FromRow, Georm)]
|
||||
#[georm(table = "authors")]
|
||||
pub struct Author {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
```
|
||||
|
||||
Finally, indicate with the same proc-macro which field of your struct
|
||||
is the primary key in your database.
|
||||
```rust
|
||||
#[derive(sqlx::FromRow, Georm)]
|
||||
#[georm(table = "authors")]
|
||||
pub struct Author {
|
||||
#[georm(id)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
```
|
||||
|
||||
Congratulations, your struct `Author` now has access to all the
|
||||
functions described in the `Georm` trait!
|
||||
|
||||
<div align="center">
|
||||
<h4>Entity relationship</h4>
|
||||
</div>
|
||||
|
||||
It is possible to implement one-to-one, one-to-many, and many-to-many
|
||||
relationships with Georm. This is a quick example of how a struct with
|
||||
several relationships of different types may be declared:
|
||||
```rust
|
||||
#[derive(sqlx::FromRow, Georm)]
|
||||
#[georm(
|
||||
table = "books",
|
||||
one_to_many = [
|
||||
{ name = "reviews", remote_id = "book_id", table = "reviews", entity = Review }
|
||||
],
|
||||
many_to_many = [{
|
||||
name = "genres",
|
||||
table = "genres",
|
||||
entity = Genre,
|
||||
link = { table = "book_genres", from = "book_id", to = "genre_id" }
|
||||
}]
|
||||
)]
|
||||
pub struct Book {
|
||||
#[georm(id)]
|
||||
ident: i32,
|
||||
title: String,
|
||||
#[georm(relation = {entity = Author, table = "authors", name = "author"})]
|
||||
author_id: i32,
|
||||
}
|
||||
```
|
||||
|
||||
To read more about these features, you can refer to the [online
|
||||
documentation](https://docs.rs/sqlx/latest/georm/).
|
||||
|
@ -44,16 +44,6 @@ pub struct M2MLink {
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
//#[georm(
|
||||
// table = "users",
|
||||
// many_to_many = [
|
||||
// {
|
||||
// name = friends,
|
||||
// entity: User,
|
||||
// link = { table = "user_friendships", from: "user1", to "user2" }
|
||||
// }
|
||||
// ]
|
||||
//)]
|
||||
#[derive(deluxe::ParseMetaItem)]
|
||||
pub struct M2MRelationship {
|
||||
pub name: String,
|
||||
@ -134,7 +124,6 @@ struct GeormFieldAttributes {
|
||||
pub relation: Option<O2ORelationship>,
|
||||
}
|
||||
|
||||
// #[georm(relation = { name = profile, id = "id", entity = Profile, nullable })]
|
||||
#[derive(deluxe::ParseMetaItem, Clone, Debug)]
|
||||
pub struct O2ORelationship {
|
||||
pub entity: syn::Type,
|
||||
|
@ -1,73 +1,3 @@
|
||||
//! Creates ORM functionality for ``SQLx`` with `PostgreSQL`.
|
||||
//!
|
||||
//! This crate provides the trait implementation `Georm` which
|
||||
//! generates the following ``SQLx`` queries:
|
||||
//! - find an entity by id
|
||||
//!
|
||||
//! SQL query: `SELECT * FROM ... WHERE <id> = ...`
|
||||
//! - insert an entity into the database
|
||||
//!
|
||||
//! SQL query: `INSERT INTO ... (...) VALUES (...) RETURNING *`
|
||||
//! - update an entity in the database
|
||||
//!
|
||||
//! SQL query: `UPDATE ... SET ... WHERE <id> = ... RETURNING *`
|
||||
//! - delete an entity from the database using its id or an id
|
||||
//! provided by the interface’s user
|
||||
//!
|
||||
//! SQL query: `DELETE FROM ... WHERE <id> = ...`
|
||||
//! - update an entity or create it if it does not already exist in
|
||||
//! the database
|
||||
//!
|
||||
//! This macro relies on the trait `Georm` found in the `georm`
|
||||
//! crate.
|
||||
//!
|
||||
//! To use this macro, you need to add it to the derives of the
|
||||
//! struct. You will also need to define its identifier
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Add `#[georm(table = "my_table_name")]` atop of the structure,
|
||||
//! after the `Georm` derive.
|
||||
//!
|
||||
//! ## Entity Identifier
|
||||
//! You will also need to add `#[georm(id)]` atop of the field of your
|
||||
//! struct that will be used as the identifier of your entity.
|
||||
//!
|
||||
//! ## Column Name
|
||||
//! If the name of a field does not match the name of its related
|
||||
//! column, you can use `#[georm(column = "...")]` to specify the
|
||||
//! correct value.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "users")]
|
||||
//! pub struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: String,
|
||||
//! #[georm(column = "name")]
|
||||
//! username: String,
|
||||
//! created_at: Timestampz,
|
||||
//! last_updated: Timestampz,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! With the example of the `User` struct, this links it to the
|
||||
//! `users` table of the connected database. It will use `Users.id` to
|
||||
//! uniquely identify a user entity.
|
||||
//!
|
||||
//! # Limitations
|
||||
//! ## ID
|
||||
//! For now, only one identifier is supported. It does not have to be
|
||||
//! a primary key, but it is strongly encouraged to use Georm ID on a
|
||||
//! unique and non-null column of your database schema.
|
||||
//!
|
||||
//! ## Database type
|
||||
//!
|
||||
//! For now, only the ``PostgreSQL`` syntax is supported. If you use
|
||||
//! another database that uses the same syntax, you’re in luck!
|
||||
//! Otherwise, pull requests to add additional syntaxes are most
|
||||
//! welcome.
|
||||
|
||||
mod georm;
|
||||
use georm::georm_derive_macro2;
|
||||
|
||||
|
249
src/lib.rs
249
src/lib.rs
@ -1,3 +1,252 @@
|
||||
//! # Georm
|
||||
//!
|
||||
//! ## Introduction
|
||||
//!
|
||||
//! Georm is a simple, opinionated SQLx ORM for PostgreSQL.
|
||||
//!
|
||||
//! To automatically implement the `Georm` trait, you need at least:
|
||||
//! - to derive the `Georm` and `sqlx::FromRow` traits
|
||||
//! - use the `georm` proc-macro to indicate the table in which your entity
|
||||
//! lives
|
||||
//! - use the `georm` proc-macro again to indicate which field of your struct is
|
||||
//! the identifier of your entity.
|
||||
//!
|
||||
//! ## Simple usage
|
||||
//! Here is a minimal use of Georm with a struct:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "users")]
|
||||
//! pub struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The `User` type will now have access to all the functions declared in the
|
||||
//! `Georm` trait.
|
||||
//!
|
||||
//! ## One-to-one relationships
|
||||
//!
|
||||
//! You can then create relationships between different entities. For instance,
|
||||
//! you can use an identifier of another entity as a link to that other entity.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "profiles")]
|
||||
//! pub struct Profile {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(
|
||||
//! relation = {
|
||||
//! entity = User,
|
||||
//! name = "user",
|
||||
//! table = "users",
|
||||
//! remote_id = "id",
|
||||
//! nullable = false
|
||||
//! })
|
||||
//! ]
|
||||
//! user_id: i32,
|
||||
//! display_name: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This will give access to the `Profile::get_user(&self, pool: &sqlx::PgPool)
|
||||
//! -> User` method.
|
||||
//!
|
||||
//! Here is an explanation of what these different values mean:
|
||||
//!
|
||||
//! | Value Name | Explanation | Default value |
|
||||
//! |------------|-----------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//! | nullable | Whether the relationship can be broken | `false` |
|
||||
//!
|
||||
//! Note that in this instance, the `remote_id` and `nullable` values can be
|
||||
//! omitted as this is their default value. This below is a strict equivalent:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "profiles")]
|
||||
//! pub struct Profile {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(relation = { entity = User, table = "users", name = "user" })]
|
||||
//! user_id: i32,
|
||||
//! display_name: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## One-to-many relationships
|
||||
//!
|
||||
//! Sometimes, our entity is the one being referenced to by multiple entities,
|
||||
//! but we have no internal reference to these remote entities in our local
|
||||
//! entity. Fortunately, we have a way to indicate to Georm how to find these.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "posts")]
|
||||
//! struct Post {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(relation = { entity = User, table = "users", name = "user" })]
|
||||
//! author_id: i32,
|
||||
//! content: String
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "users",
|
||||
//! one_to_many = [{
|
||||
//! entity = Post,
|
||||
//! name = "posts",
|
||||
//! table = "posts",
|
||||
//! remote_id = "id"
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! As we’ve seen earlier, the struct `Post` has access to the method
|
||||
//! `Post::get_user(&self, pool: &sqlx::PgPool) -> User` thanks to the
|
||||
//! proc-macro used on `author_id`. However, `User` now has also access to
|
||||
//! `User::get_posts(&self, pool: &sqlx::PgPool) -> Vec<Post>`. And as you can
|
||||
//! see, `one_to_many` is an array, meaning you can define several one-to-many
|
||||
//! relationships for `User`.
|
||||
//!
|
||||
//! Here is an explanation of the values of `one_to_many`:
|
||||
//!
|
||||
//! | Value Name | Explanaion | Default Value |
|
||||
//! |------------|------------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//!
|
||||
//! As with one-to-one relationships, `remote_id` is optional. The following
|
||||
//! `User` struct is strictly equivalent.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "users",
|
||||
//! one_to_many = [{ entity = Post, name = "posts", table = "posts" }]
|
||||
//! )]
|
||||
//! struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Many-to-many relationships
|
||||
//!
|
||||
//! Many-to-many relationships between entities A and entities B with Georm rely
|
||||
//! on a third table which refers to both. For instance, the following SQL code
|
||||
//! describes a many-to-many relationship between books and book genre.
|
||||
//!
|
||||
//! ```sql
|
||||
//! CREATE TABLE books (
|
||||
//! id SERIAL PRIMARY KEY,
|
||||
//! title VARCHAR(100) NOT NULL
|
||||
//! );
|
||||
//!
|
||||
//! CREATE TABLE genres (
|
||||
//! id SERIAL PRIMARY KEY,
|
||||
//! name VARCHAR(100) NOT NULL
|
||||
//! );
|
||||
//!
|
||||
//! CREATE TABLE books_genres (
|
||||
//! book_id INT NOT NULL,
|
||||
//! genre_id INT NOT NULL,
|
||||
//! PRIMARY KEY (book_id, genre_id),
|
||||
//! FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
|
||||
//! FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! The table `books_genres` is the one defining the many-to-many relationship
|
||||
//! between the table `books` and the table `genres`. With Georm, this gives us
|
||||
//! the following code:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "books",
|
||||
//! many_to_many = [{
|
||||
//! name = "genres",
|
||||
//! entity = Genre,
|
||||
//! table = "genres",
|
||||
//! remote_id = "id",
|
||||
//! link = { table = "books_genres", from = "book_id", to = "genre_id" }
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct Book {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! title: String
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "genres",
|
||||
//! many_to_many = [{
|
||||
//! entity = Book,
|
||||
//! name = "books",
|
||||
//! table = "books",
|
||||
//! remote_id = "id",
|
||||
//! link = { table = "books_genres", from = "genre_id", to = "book_id" }
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct Genre {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! name: String
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This generates two methods:
|
||||
//! - `Book::get_genres(&self, pool: &sqlx::PgPool) -> Vec<Genre>`
|
||||
//! - `Genre::get_books(&self, pool: &sqlx::PgPool) -> Vec<Book>`
|
||||
//!
|
||||
//! As you can see, `many_to_many` is also an array, meaning we can define
|
||||
//! several many-to-many relationships for the same struct.
|
||||
//!
|
||||
//! Here is an explanation of the values behind `many_to_many`:
|
||||
//!
|
||||
//! | Value Name | Explanation | Default value |
|
||||
//! |------------|------------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//! | link.table | Name of the many-to-many relationship table | N/A |
|
||||
//! | link.from | Column of the linking table referring to this entity | N/A |
|
||||
//! | link.to | Column of the linking table referring to the remote entity | N/A |
|
||||
//!
|
||||
//! ## Limitations
|
||||
//! ### Database
|
||||
//!
|
||||
//! For now, Georm is limited to PostgreSQL. Other databases may be supported in
|
||||
//! the future, such as Sqlite or MySQL, but that is not the case yet.
|
||||
//!
|
||||
//! ## Identifiers
|
||||
//!
|
||||
//! Identifiers, or primary keys from the point of view of the database, may
|
||||
//! only be simple types recognized by SQLx. They also cannot be arrays, and
|
||||
//! optionals are only supported in one-to-one relationships when explicitly
|
||||
//! marked as nullables.
|
||||
|
||||
pub use georm_macros::Georm;
|
||||
|
||||
pub trait Georm<Id> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user