docs: complete rewrite of README
Replaces the existing README with a comprehensive guide that significantly improves the developer and user experience. The new README provides complete documentation for all Georm features and a detailed development setup guide.
This commit is contained in:
parent
11fce7a1e2
commit
6fba12da56
736
README.md
736
README.md
@ -7,7 +7,7 @@
|
|||||||
<h1 align="center">Georm</h1>
|
<h1 align="center">Georm</h1>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<strong>
|
<strong>
|
||||||
A simple, opinionated SQLx ORM for PostgreSQL
|
A simple, type-safe SQLx ORM for PostgreSQL
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
@ -15,7 +15,8 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<!-- Github Actions -->
|
<!-- Github Actions -->
|
||||||
<a href="https://github.com/phundrak/georm/actions/workflows/ci.yaml?query=branch%3Amain">
|
<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>
|
<img src="https://img.shields.io/github/actions/workflow/status/phundrak/georm/ci.yaml?branch=main&style=flat-square" alt="actions status" />
|
||||||
|
</a>
|
||||||
<!-- Version -->
|
<!-- Version -->
|
||||||
<a href="https://crates.io/crates/georm">
|
<a href="https://crates.io/crates/georm">
|
||||||
<img src="https://img.shields.io/crates/v/georm.svg?style=flat-square" alt="Crates.io version" />
|
<img src="https://img.shields.io/crates/v/georm.svg?style=flat-square" alt="Crates.io version" />
|
||||||
@ -24,90 +25,257 @@
|
|||||||
<a href="https://docs.rs/georm">
|
<a href="https://docs.rs/georm">
|
||||||
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" />
|
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square" alt="docs.rs docs" />
|
||||||
</a>
|
</a>
|
||||||
|
<!-- License -->
|
||||||
|
<a href="#license">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT%20OR%20GPL--3.0-blue?style=flat-square" alt="License" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## What is Georm?
|
## Overview
|
||||||
|
|
||||||
Georm is a quite simple ORM built around
|
Georm is a lightweight, opinionated Object-Relational Mapping (ORM) library built on top of [SQLx](https://crates.io/crates/sqlx) for PostgreSQL. It provides a clean, type-safe interface for common database operations while leveraging SQLx's compile-time query verification.
|
||||||
[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.
|
|
||||||
|
|
||||||
## Why is Georm?
|
### Key Features
|
||||||
|
|
||||||
I wanted an ORM that’s easy and straightforward to use. I am aware
|
- **Type Safety**: Compile-time verified SQL queries using SQLx macros
|
||||||
some other projects exist, such as
|
- **Zero Runtime Cost**: No reflection or runtime query building
|
||||||
[SeaORM](https://www.sea-ql.org/SeaORM/), but they generally don’t fit
|
- **Simple API**: Intuitive derive macros for common operations
|
||||||
my needs and/or my wants of a simple interface. I ended up writing the
|
- **Relationship Support**: One-to-one, one-to-many, and many-to-many relationships
|
||||||
ORM I wanted to use.
|
- **Defaultable Fields**: Easy entity creation with database defaults and auto-generated values
|
||||||
|
- **PostgreSQL Native**: Optimized for PostgreSQL features and data types
|
||||||
|
|
||||||
## How is Georm?
|
## Quick Start
|
||||||
|
|
||||||
I use it in a few projects, and I’m quite happy with it right now. But
|
### Installation
|
||||||
of course, I’m open to constructive criticism and suggestions!
|
|
||||||
|
|
||||||
## How can I use it?
|
Add Georm and SQLx to your `Cargo.toml`:
|
||||||
|
|
||||||
Georm works with SQLx, but does not re-export it itself. To get
|
```toml
|
||||||
started, install both Georm and SQLx in your Rust project:
|
[dependencies]
|
||||||
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros"] }
|
||||||
```sh
|
georm = "0.1"
|
||||||
cargo add sqlx --features postgres,macros # and any other feature you might want
|
|
||||||
cargo add georm
|
|
||||||
```
|
```
|
||||||
|
|
||||||
As Georm relies heavily on the macro
|
### Basic Usage
|
||||||
[`query_as!`](https://docs.rs/sqlx/latest/sqlx/macro.query_as.html),
|
|
||||||
the `macros` feature is not optional. Declare your tables in your
|
1. **Define your database schema**:
|
||||||
Postgres database (you may want to use SQLx’s `migrate` feature for
|
|
||||||
this), and then declare their equivalent in Rust.
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE biographies (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
content TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE authors (
|
CREATE TABLE authors (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
biography_id INT,
|
email VARCHAR(255) UNIQUE NOT NULL
|
||||||
FOREIGN KEY (biography_id) REFERENCES biographies(id)
|
);
|
||||||
|
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
published BOOLEAN DEFAULT FALSE,
|
||||||
|
author_id INT NOT NULL REFERENCES authors(id),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
2. **Define your Rust entities**:
|
||||||
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
|
```rust
|
||||||
#[derive(sqlx::FromRow, Georm)]
|
use georm::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)]
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
#[georm(table = "authors")]
|
#[georm(table = "authors")]
|
||||||
pub struct Author {
|
pub struct Author {
|
||||||
|
#[georm(id)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(table = "posts")]
|
||||||
|
pub struct Post {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub published: bool,
|
||||||
|
#[georm(relation = {
|
||||||
|
entity = Author,
|
||||||
|
table = "authors",
|
||||||
|
name = "author"
|
||||||
|
})]
|
||||||
|
pub author_id: i32,
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, indicate with the same proc-macro which field of your struct
|
3. **Use the generated methods**:
|
||||||
is the primary key in your database.
|
|
||||||
|
```rust
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
async fn example(pool: &PgPool) -> sqlx::Result<()> {
|
||||||
|
// Create an author
|
||||||
|
let author = Author {
|
||||||
|
id: 0, // Will be auto-generated
|
||||||
|
name: "Jane Doe".to_string(),
|
||||||
|
email: "jane@example.com".to_string(),
|
||||||
|
};
|
||||||
|
let author = author.create(pool).await?;
|
||||||
|
|
||||||
|
// Create a post
|
||||||
|
let post = Post {
|
||||||
|
id: 0,
|
||||||
|
title: "Hello, Georm!".to_string(),
|
||||||
|
content: "This is my first post using Georm.".to_string(),
|
||||||
|
published: false,
|
||||||
|
author_id: author.id,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
let post = post.create(pool).await?;
|
||||||
|
|
||||||
|
// Find all posts
|
||||||
|
let all_posts = Post::find_all(pool).await?;
|
||||||
|
|
||||||
|
// Get the post's author
|
||||||
|
let post_author = post.get_author(pool).await?;
|
||||||
|
|
||||||
|
println!("Post '{}' by {}", post.title, post_author.name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Defaultable Fields
|
||||||
|
|
||||||
|
For fields with database defaults or auto-generated values, use the `defaultable` attribute:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(sqlx::FromRow, Georm)]
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
#[georm(table = "authors")]
|
#[georm(table = "posts")]
|
||||||
|
pub struct Post {
|
||||||
|
#[georm(id, defaultable)]
|
||||||
|
pub id: i32, // Auto-generated serial
|
||||||
|
pub title: String,
|
||||||
|
#[georm(defaultable)]
|
||||||
|
pub published: bool, // Has database default (false)
|
||||||
|
#[georm(defaultable)]
|
||||||
|
pub created_at: chrono::DateTime<chrono::Utc>, // DEFAULT NOW()
|
||||||
|
pub author_id: i32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This generates a `PostDefault` struct for easier creation:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use georm::Defaultable;
|
||||||
|
|
||||||
|
let post_default = PostDefault {
|
||||||
|
id: None, // Let database auto-generate
|
||||||
|
title: "My Post".to_string(),
|
||||||
|
published: None, // Use database default
|
||||||
|
created_at: None, // Use database default (NOW())
|
||||||
|
author_id: 42,
|
||||||
|
};
|
||||||
|
|
||||||
|
let created_post = post_default.create(pool).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relationships
|
||||||
|
|
||||||
|
Georm supports comprehensive relationship modeling with two approaches: field-level relationships for foreign keys and struct-level relationships for reverse lookups.
|
||||||
|
|
||||||
|
#### Field-Level Relationships (Foreign Keys)
|
||||||
|
|
||||||
|
Use the `relation` attribute on foreign key fields to generate lookup methods:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(table = "posts")]
|
||||||
|
pub struct Post {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub title: String,
|
||||||
|
#[georm(relation = {
|
||||||
|
entity = Author, // Target entity type
|
||||||
|
table = "authors", // Target table name
|
||||||
|
name = "author", // Method name (generates get_author)
|
||||||
|
remote_id = "id", // Target table's key column (default: "id")
|
||||||
|
nullable = false // Whether relationship can be null (default: false)
|
||||||
|
})]
|
||||||
|
pub author_id: i32,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated method**: `post.get_author(pool).await? -> Author`
|
||||||
|
|
||||||
|
For nullable relationships:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(table = "posts")]
|
||||||
|
pub struct Post {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub title: String,
|
||||||
|
#[georm(relation = {
|
||||||
|
entity = Category,
|
||||||
|
table = "categories",
|
||||||
|
name = "category",
|
||||||
|
nullable = true // Allows NULL values
|
||||||
|
})]
|
||||||
|
pub category_id: Option<i32>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated method**: `post.get_category(pool).await? -> Option<Category>`
|
||||||
|
|
||||||
|
#### Struct-Level Relationships (Reverse Lookups)
|
||||||
|
|
||||||
|
Define relationships at the struct level to query related entities that reference this entity:
|
||||||
|
|
||||||
|
##### One-to-One Relationships
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(
|
||||||
|
table = "users",
|
||||||
|
one_to_one = [{
|
||||||
|
entity = Profile, // Related entity type
|
||||||
|
name = "profile", // Method name (generates get_profile)
|
||||||
|
table = "profiles", // Related table name
|
||||||
|
remote_id = "user_id", // Foreign key in related table
|
||||||
|
}]
|
||||||
|
)]
|
||||||
|
pub struct User {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated method**: `user.get_profile(pool).await? -> Option<Profile>`
|
||||||
|
|
||||||
|
##### One-to-Many Relationships
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(
|
||||||
|
table = "authors",
|
||||||
|
one_to_many = [{
|
||||||
|
entity = Post, // Related entity type
|
||||||
|
name = "posts", // Method name (generates get_posts)
|
||||||
|
table = "posts", // Related table name
|
||||||
|
remote_id = "author_id" // Foreign key in related table
|
||||||
|
}, {
|
||||||
|
entity = Comment, // Multiple relationships allowed
|
||||||
|
name = "comments",
|
||||||
|
table = "comments",
|
||||||
|
remote_id = "author_id"
|
||||||
|
}]
|
||||||
|
)]
|
||||||
pub struct Author {
|
pub struct Author {
|
||||||
#[georm(id)]
|
#[georm(id)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@ -115,83 +283,429 @@ pub struct Author {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Congratulations, your struct `Author` now has access to all the
|
**Generated methods**:
|
||||||
functions described in the `Georm` trait!
|
- `author.get_posts(pool).await? -> Vec<Post>`
|
||||||
|
- `author.get_comments(pool).await? -> Vec<Comment>`
|
||||||
|
|
||||||
## Entity relationship
|
##### Many-to-Many Relationships
|
||||||
|
|
||||||
|
For many-to-many relationships, specify the link table that connects the entities:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Example schema for books and genres
|
||||||
|
CREATE TABLE books (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(200) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE genres (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE book_genres (
|
||||||
|
book_id INT NOT NULL REFERENCES books(id),
|
||||||
|
genre_id INT NOT NULL REFERENCES genres(id),
|
||||||
|
PRIMARY KEY (book_id, genre_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
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
|
```rust
|
||||||
#[derive(sqlx::FromRow, Georm)]
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
#[georm(
|
#[georm(
|
||||||
table = "books",
|
table = "books",
|
||||||
one_to_one = [
|
|
||||||
{ name = "draft", remote_id = "book_id", table = "drafts", entity = Draft }
|
|
||||||
],
|
|
||||||
one_to_many = [
|
|
||||||
{ name = "reviews", remote_id = "book_id", table = "reviews", entity = Review },
|
|
||||||
{ name = "reprints", remote_id = "book_id", table = "reprints", entity = Reprint }
|
|
||||||
],
|
|
||||||
many_to_many = [{
|
many_to_many = [{
|
||||||
name = "genres",
|
entity = Genre, // Related entity type
|
||||||
table = "genres",
|
name = "genres", // Method name (generates get_genres)
|
||||||
entity = Genre,
|
table = "genres", // Related table name
|
||||||
link = { table = "book_genres", from = "book_id", to = "genre_id" }
|
remote_id = "id", // Primary key in related table (default: "id")
|
||||||
|
link = { // Link table configuration
|
||||||
|
table = "book_genres", // Join table name
|
||||||
|
from = "book_id", // Column referencing this entity
|
||||||
|
to = "genre_id" // Column referencing related entity
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
)]
|
)]
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
#[georm(id)]
|
#[georm(id)]
|
||||||
ident: i32,
|
pub id: i32,
|
||||||
title: String,
|
pub title: String,
|
||||||
#[georm(relation = {entity = Author, table = "authors", name = "author"})]
|
}
|
||||||
author_id: i32,
|
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(
|
||||||
|
table = "genres",
|
||||||
|
many_to_many = [{
|
||||||
|
entity = Book,
|
||||||
|
name = "books",
|
||||||
|
table = "books",
|
||||||
|
link = {
|
||||||
|
table = "book_genres",
|
||||||
|
from = "genre_id", // Note: reversed perspective
|
||||||
|
to = "book_id"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
)]
|
||||||
|
pub struct Genre {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To read more about these features, you can refer to the [online
|
**Generated methods**:
|
||||||
documentation](https://docs.rs/georm/).
|
- `book.get_genres(pool).await? -> Vec<Genre>`
|
||||||
|
- `genre.get_books(pool).await? -> Vec<Book>`
|
||||||
|
|
||||||
## Roadmap / TODO
|
#### Relationship Attribute Reference
|
||||||
|
|
||||||
The following features are being considered for future development:
|
| Attribute | Description | Required | Default |
|
||||||
|
|--------------|------------------------------------------------------|----------|---------|
|
||||||
|
| `entity` | Target entity type | Yes | N/A |
|
||||||
|
| `name` | Method name (generates `get_{name}`) | Yes | N/A |
|
||||||
|
| `table` | Target table name | Yes | N/A |
|
||||||
|
| `remote_id` | Target table's key column | No | `"id"` |
|
||||||
|
| `nullable` | Whether relationship can be null (field-level only) | No | `false` |
|
||||||
|
| `link.table` | Join table name (many-to-many only) | Yes* | N/A |
|
||||||
|
| `link.from` | Column referencing this entity (many-to-many only) | Yes* | N/A |
|
||||||
|
| `link.to` | Column referencing target entity (many-to-many only) | Yes* | N/A |
|
||||||
|
|
||||||
|
*Required for many-to-many relationships
|
||||||
|
|
||||||
|
#### Complex Relationship Example
|
||||||
|
|
||||||
|
Here's a comprehensive example showing multiple relationship types:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(sqlx::FromRow, Georm)]
|
||||||
|
#[georm(
|
||||||
|
table = "posts",
|
||||||
|
one_to_many = [{
|
||||||
|
entity = Comment,
|
||||||
|
name = "comments",
|
||||||
|
table = "comments",
|
||||||
|
remote_id = "post_id"
|
||||||
|
}],
|
||||||
|
many_to_many = [{
|
||||||
|
entity = Tag,
|
||||||
|
name = "tags",
|
||||||
|
table = "tags",
|
||||||
|
link = {
|
||||||
|
table = "post_tags",
|
||||||
|
from = "post_id",
|
||||||
|
to = "tag_id"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
)]
|
||||||
|
pub struct Post {
|
||||||
|
#[georm(id)]
|
||||||
|
pub id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
|
||||||
|
// Field-level relationship (foreign key)
|
||||||
|
#[georm(relation = {
|
||||||
|
entity = Author,
|
||||||
|
table = "authors",
|
||||||
|
name = "author"
|
||||||
|
})]
|
||||||
|
pub author_id: i32,
|
||||||
|
|
||||||
|
// Nullable field-level relationship
|
||||||
|
#[georm(relation = {
|
||||||
|
entity = Category,
|
||||||
|
table = "categories",
|
||||||
|
name = "category",
|
||||||
|
nullable = true
|
||||||
|
})]
|
||||||
|
pub category_id: Option<i32>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generated methods**:
|
||||||
|
- `post.get_author(pool).await? -> Author` (from field relation)
|
||||||
|
- `post.get_category(pool).await? -> Option<Category>` (nullable field relation)
|
||||||
|
- `post.get_comments(pool).await? -> Vec<Comment>` (one-to-many)
|
||||||
|
- `post.get_tags(pool).await? -> Vec<Tag>` (many-to-many)
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Core Operations
|
||||||
|
|
||||||
|
All entities implementing `Georm<Id>` get these methods:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Query operations
|
||||||
|
Post::find_all(pool).await?; // Find all posts
|
||||||
|
Post::find(pool, &post_id).await?; // Find by ID
|
||||||
|
|
||||||
|
// Mutation operations
|
||||||
|
post.create(pool).await?; // Insert new record
|
||||||
|
post.update(pool).await?; // Update existing record
|
||||||
|
post.create_or_update(pool).await?; // Upsert operation
|
||||||
|
post.delete(pool).await?; // Delete this record
|
||||||
|
Post::delete_by_id(pool, &post_id).await?; // Delete by ID
|
||||||
|
|
||||||
|
// Utility
|
||||||
|
post.get_id(); // Get entity ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### Defaultable Operations
|
||||||
|
|
||||||
|
Entities with defaultable fields get a companion `<Entity>Default` struct:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Create with defaults
|
||||||
|
post_default.create(pool).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Attributes Reference
|
||||||
|
|
||||||
|
#### Struct-level attributes
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[georm(
|
||||||
|
table = "table_name", // Required: database table name
|
||||||
|
one_to_one = [{ /* ... */ }], // Optional: one-to-one relationships
|
||||||
|
one_to_many = [{ /* ... */ }], // Optional: one-to-many relationships
|
||||||
|
many_to_many = [{ /* ... */ }] // Optional: many-to-many relationships
|
||||||
|
)]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Field-level attributes
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[georm(id)] // Mark as primary key
|
||||||
|
#[georm(defaultable)] // Mark as defaultable field
|
||||||
|
#[georm(relation = { /* ... */ })] // Define relationship
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
Georm is designed for zero runtime overhead:
|
||||||
|
|
||||||
|
- **Compile-time queries**: All SQL is verified at compile time
|
||||||
|
- **No reflection**: Direct field access, no runtime introspection
|
||||||
|
- **Minimal allocations**: Efficient use of owned vs borrowed data
|
||||||
|
- **SQLx integration**: Leverages SQLx's optimized PostgreSQL driver
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
| Feature | Georm | SeaORM | Diesel |
|
||||||
|
|----------------------|-------|--------|--------|
|
||||||
|
| Compile-time safety | ✅ | ✅ | ✅ |
|
||||||
|
| Relationship support | ✅ | ✅ | ✅ |
|
||||||
|
| Async support | ✅ | ✅ | ⚠️ |
|
||||||
|
| Learning curve | Low | Medium | High |
|
||||||
|
| Macro simplicity | ✅ | ❌ | ❌ |
|
||||||
|
| Advanced queries | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
### High Priority
|
### High Priority
|
||||||
- **Transaction Support**: Add comprehensive transaction support with
|
- **Transaction Support**: Comprehensive transaction handling with atomic operations
|
||||||
transaction-aware CRUD methods and relationship handling for atomic
|
- **Race Condition Fix**: Database-native UPSERT operations to replace current `create_or_update`
|
||||||
operations across multiple entities
|
|
||||||
- **Race Condition Fix**: Replace the current `create_or_update`
|
|
||||||
implementation with database-specific UPSERT operations (PostgreSQL
|
|
||||||
`ON CONFLICT`, MySQL `ON DUPLICATE KEY UPDATE`, SQLite `ON
|
|
||||||
CONFLICT`) to prevent race conditions
|
|
||||||
|
|
||||||
### Medium Priority
|
### Medium Priority
|
||||||
- **Multi-Database Support**: Extend Georm to support MySQL and SQLite
|
- **Multi-Database Support**: MySQL and SQLite support with feature flags
|
||||||
in addition to PostgreSQL, with database-specific optimizations and
|
- **Relationship Optimization**: Eager loading and N+1 query prevention
|
||||||
dialect handling
|
- **Composite Primary Keys**: Multi-field primary key support
|
||||||
- **Relationship Optimization**: Implement eager loading and N+1 query
|
- **Soft Delete**: Optional soft delete with `deleted_at` timestamps
|
||||||
prevention with circular dependency protection to dramatically
|
|
||||||
improve performance when working with related entities
|
|
||||||
- **Composite Primary Keys**: Add support for entities with multiple
|
|
||||||
primary key fields using auto-generated ID structs and type-safe
|
|
||||||
composite key handling
|
|
||||||
- **Soft Delete**: Implement optional soft delete functionality with
|
|
||||||
`deleted_at` timestamps, allowing entities to be marked as deleted
|
|
||||||
without physical removal
|
|
||||||
|
|
||||||
### Lower Priority
|
### Lower Priority
|
||||||
- **Migration Support**: Add optional migration utilities that
|
- **Migration Support**: Schema generation and evolution utilities
|
||||||
leverage SQLx's existing infrastructure for schema generation,
|
- **Enhanced Error Handling**: Custom error types with better context
|
||||||
verification, and evolution
|
|
||||||
- **Enhanced Error Handling**: Consider implementing custom error
|
|
||||||
types with better categorization and operation context while
|
|
||||||
maintaining compatibility with SQLx errors
|
|
||||||
- **Many-to-Many Relationship Improvements**: Add direct methods to
|
|
||||||
add or remove items from many-to-many relationships without manually
|
|
||||||
handling the join table
|
|
||||||
|
|
||||||
### Recently Completed
|
## Contributing
|
||||||
- ✅ **Defaultable Fields**: Support for fields with database defaults
|
|
||||||
or auto-generated values, creating companion structs with optional
|
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
||||||
fields for easier entity creation
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- **Rust 1.81+**: Georm uses modern Rust features and follows the MSRV specified in `rust-toolchain.toml`
|
||||||
|
- **PostgreSQL 12+**: Required for running tests and development
|
||||||
|
- **Git**: For version control
|
||||||
|
- **Jujutsu**: For version control (alternative to Git)
|
||||||
|
|
||||||
|
#### Required Tools
|
||||||
|
|
||||||
|
The following tools are used in the development workflow:
|
||||||
|
|
||||||
|
- **[just](https://github.com/casey/just)**: Task runner for common development commands
|
||||||
|
- **[cargo-deny](https://github.com/EmbarkStudios/cargo-deny)**: License and security auditing
|
||||||
|
- **[sqlx-cli](https://github.com/launchbadge/sqlx/tree/main/sqlx-cli)**: Database migrations and management
|
||||||
|
- **[bacon](https://github.com/Canop/bacon)**: Background code checker (optional but recommended)
|
||||||
|
|
||||||
|
Install these tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install just (task runner)
|
||||||
|
cargo install just
|
||||||
|
|
||||||
|
# Install cargo-deny (for auditing)
|
||||||
|
cargo install cargo-deny
|
||||||
|
|
||||||
|
# Install sqlx-cli (for database management)
|
||||||
|
cargo install sqlx-cli --no-default-features --features native-tls,postgres
|
||||||
|
|
||||||
|
# Install bacon (optional, for live feedback)
|
||||||
|
cargo install bacon
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/Phundrak/georm.git
|
||||||
|
cd georm
|
||||||
|
|
||||||
|
# Set up your PostgreSQL database and set DATABASE_URL
|
||||||
|
export DATABASE_URL="postgres://username:password@localhost/georm_test"
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
just migrate
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
just test
|
||||||
|
|
||||||
|
# Run linting
|
||||||
|
just lint
|
||||||
|
|
||||||
|
# Run security audit
|
||||||
|
just audit
|
||||||
|
|
||||||
|
# Run all checks (format, lint, audit, test)
|
||||||
|
just check-all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Available Commands (via just)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just # Default: run linting
|
||||||
|
just build # Build the project
|
||||||
|
just build-release # Build in release mode
|
||||||
|
just test # Run all tests
|
||||||
|
just lint # Run clippy linting
|
||||||
|
just audit # Run security and license audit
|
||||||
|
just migrate # Run database migrations
|
||||||
|
just format # Format all code
|
||||||
|
just format-check # Check code formatting
|
||||||
|
just check-all # Run all checks (format, lint, audit, test)
|
||||||
|
just clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running Specific Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run tests for a specific module
|
||||||
|
cargo test --test simple_struct
|
||||||
|
cargo test --test defaultable_struct
|
||||||
|
cargo test --test m2m_relationship
|
||||||
|
|
||||||
|
# Run tests with output
|
||||||
|
cargo test -- --nocapture
|
||||||
|
|
||||||
|
# Run a specific test function
|
||||||
|
cargo test defaultable_struct_should_exist
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development with Bacon (Optional)
|
||||||
|
|
||||||
|
For continuous feedback during development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run clippy continuously
|
||||||
|
bacon
|
||||||
|
|
||||||
|
# Run tests continuously
|
||||||
|
bacon test
|
||||||
|
|
||||||
|
# Build docs continuously
|
||||||
|
bacon doc
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Nix Development Environment (Optional)
|
||||||
|
|
||||||
|
If you use [Nix](https://nixos.org/), you can use the provided flake for a reproducible development environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enter the development shell with all tools pre-installed
|
||||||
|
nix develop
|
||||||
|
|
||||||
|
# Or use direnv for automatic environment activation
|
||||||
|
direnv allow
|
||||||
|
```
|
||||||
|
|
||||||
|
The Nix flake provides:
|
||||||
|
- Exact Rust version (1.81) with required components
|
||||||
|
- All development tools (just, cargo-deny, sqlx-cli, bacon)
|
||||||
|
- LSP support (rust-analyzer)
|
||||||
|
- SQL tooling (sqls for SQL language server)
|
||||||
|
|
||||||
|
**Nix flake contents:**
|
||||||
|
- **Rust toolchain**: Specified version with rustfmt, clippy, and rust-analyzer
|
||||||
|
- **Development tools**: just, cargo-deny, sqlx-cli, bacon
|
||||||
|
- **SQL tools**: sqls (SQL language server)
|
||||||
|
- **Platform support**: Currently x86_64-linux (can be extended)
|
||||||
|
|
||||||
|
#### Database Setup for Tests
|
||||||
|
|
||||||
|
Tests require a PostgreSQL database. Set up a test database:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Connect to PostgreSQL as superuser
|
||||||
|
CREATE DATABASE georm_test;
|
||||||
|
CREATE USER georm_user WITH PASSWORD 'georm_password';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE georm_test TO georm_user;
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DATABASE_URL="postgres://georm_user:georm_password@localhost/georm_test"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### IDE Setup
|
||||||
|
|
||||||
|
- Ensure `rust-analyzer` is configured
|
||||||
|
- Set up PostgreSQL connection for SQL syntax highlighting
|
||||||
|
|
||||||
|
#### Code Style
|
||||||
|
|
||||||
|
The project uses standard Rust formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
just format
|
||||||
|
|
||||||
|
# Check formatting (CI)
|
||||||
|
just format-check
|
||||||
|
```
|
||||||
|
|
||||||
|
Clippy linting is enforced:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run linting
|
||||||
|
just lint
|
||||||
|
|
||||||
|
# Fix auto-fixable lints
|
||||||
|
cargo clippy --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
|
||||||
|
* MIT License ([LICENSE-MIT](LICENSE-MIT.md) or http://opensource.org/licenses/MIT)
|
||||||
|
* GNU General Public License v3.0 ([LICENSE-GPL](LICENSE-GPL.md) or https://www.gnu.org/licenses/gpl-3.0.html)
|
||||||
|
|
||||||
|
at your option.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- Built on top of the excellent [SQLx](https://github.com/launchbadge/sqlx) library
|
||||||
|
- Inspired by [Hibernate](https://hibernate.org/)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user