Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,643 @@
|
||||
# ORM
|
||||
|
||||
V has a powerful, concise ORM baked in! Create tables, insert records, manage relationships, all
|
||||
regardless of the DB driver you decide to use.
|
||||
|
||||
Driver authors using the shared SQL generators can target SQLite, PostgreSQL, MySQL, and
|
||||
H2-backed connections with the built-in ORM dialect helpers.
|
||||
|
||||
## Nullable
|
||||
|
||||
For a nullable column, use an option field. If the field is non-option, the column will be defined
|
||||
with `NOT NULL` at table creation, except scalar foreign keys declared with `@[references]`, which
|
||||
default to nullable so omitted relations can be stored as `NULL`.
|
||||
|
||||
```v ignore
|
||||
struct Foo {
|
||||
notnull string
|
||||
nullable ?string
|
||||
}
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
### Structs
|
||||
|
||||
- `[table: 'name']` explicitly sets the name of the table for the struct
|
||||
- `[comment: 'table_comment']` explicitly sets the comment of the table for the struct
|
||||
- `[index: 'f1, f2, f3']` explicitly sets fields of the table (`f1`, `f2`, `f3`) as indexed
|
||||
- `[unique_key: 'f1, f2, f3']` adds a composite `UNIQUE` constraint for the listed fields
|
||||
|
||||
### Fields
|
||||
|
||||
- `[primary]` sets the field as the primary key
|
||||
- `[unique]` gives the field a `UNIQUE` constraint
|
||||
- `[unique: 'foo']` adds the field to a `UNIQUE` group
|
||||
- `[skip]` or `[sql: '-']` field will be skipped
|
||||
- `[sql: type]` where `type` is a V type such as `int` or `f64`
|
||||
- `[serial]` or `[sql: serial]` lets the DB backend choose a column type for an auto-increment field
|
||||
- `[sql: 'name']` sets a custom column name for the field
|
||||
- `[sql_type: 'SQL TYPE']` explicitly sets the type in SQL
|
||||
- `[sql_select: 'SQL expression']` uses a custom expression in `SELECT` for the field
|
||||
- `[default: 'raw_sql']` inserts `raw_sql` verbatim in a "DEFAULT" clause when
|
||||
creating a new table, allowing for SQL functions like `CURRENT_TIME`. For raw strings,
|
||||
surround `raw_sql` with backticks (\`).
|
||||
|
||||
- `[fkey: 'parent_id']` sets foreign key for an field which holds an array
|
||||
- `[references]` or `[references: 'tablename']` or `[references: 'tablename(field_id)']`
|
||||
- `[comment: 'field_comment']` set comment
|
||||
- `[index]` creates index
|
||||
|
||||
## Usage
|
||||
> [!NOTE]
|
||||
> For using the Function Call API for `orm`, please check [`Function Call API`](#function-call-api).
|
||||
|
||||
Here are a couple example structs showing most of the features outlined above.
|
||||
|
||||
```v ignore
|
||||
import time
|
||||
|
||||
@[table: 'foos']
|
||||
struct Foo {
|
||||
id int @[primary; sql: serial]
|
||||
name string
|
||||
created_at time.Time @[default: 'CURRENT_TIME']
|
||||
updated_at ?string @[sql_type: 'TIMESTAMP']
|
||||
deleted_at ?time.Time
|
||||
children []Child @[fkey: 'parent_id']
|
||||
}
|
||||
|
||||
struct Child {
|
||||
id int @[primary; sql: serial]
|
||||
parent_id int
|
||||
name string
|
||||
}
|
||||
```
|
||||
|
||||
`sql_select` is useful for backend-specific read transformations such as:
|
||||
|
||||
```v ignore
|
||||
geom string @[sql_type: 'geometry'; sql_select: 'ST_AsEWKT(geom)']
|
||||
```
|
||||
|
||||
To use the ORM, there is a special interface that lets you use the structs and V itself in queries.
|
||||
This interface takes the database instance as an argument.
|
||||
|
||||
```v ignore
|
||||
import db.sqlite
|
||||
|
||||
db := sqlite.connect(':memory:')!
|
||||
|
||||
sql db {
|
||||
// query; see below
|
||||
}!
|
||||
```
|
||||
|
||||
### Data Scope
|
||||
|
||||
`orm.DB` can wrap an `orm.Connection` with automatic scope filters. Scope filters
|
||||
are useful for request-level tenancy, soft deletes, and row-level access checks.
|
||||
|
||||
```v ignore
|
||||
mut db := orm.new_db(raw_db, orm.DataScope{
|
||||
filters: [
|
||||
orm.QueryFilter{
|
||||
field: 'tenant_id'
|
||||
value: orm.Primitive(tenant_id)
|
||||
mode: .dynamic
|
||||
},
|
||||
orm.QueryFilter{
|
||||
field: 'shop_id'
|
||||
value: orm.Primitive(shop_id)
|
||||
mode: .dynamic
|
||||
},
|
||||
orm.QueryFilter{
|
||||
field: 'deleted_at'
|
||||
operator: .is_null
|
||||
mode: .dynamic
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
users := sql db {
|
||||
select from User
|
||||
}!
|
||||
```
|
||||
|
||||
`QueryFilter.mode` must be explicitly set to `.static` or `.dynamic` (there is
|
||||
no default — `.unset` causes a runtime error). Static filters are reserved for
|
||||
future compiler-generated scope clauses. The runtime `orm.DB` wrapper applies
|
||||
only filters explicitly marked with `mode: .dynamic`. Invalid dynamic filters
|
||||
return an error instead of being silently skipped.
|
||||
|
||||
Call `db.unscoped()` to return a new `orm.DB` value that skips all scope filters.
|
||||
Call `db.unscoped('tenant_id')` to skip only selected fields.
|
||||
|
||||
> [!TIP]
|
||||
> This guide uses the built-in `db.sqlite` module. If you want SQLite without first installing
|
||||
> system-level SQLite development files, the V team also maintains the
|
||||
> [`sqlite`](https://vpm.vlang.io/packages/sqlite) VPM package.
|
||||
>
|
||||
> Install it with `v install sqlite` and change `import db.sqlite` to `import sqlite`.
|
||||
> The package keeps the same API while bundling SQLite for you.
|
||||
|
||||
When you need to reference the table, simply pass the struct itself.
|
||||
|
||||
```v ignore
|
||||
import models.Foo
|
||||
|
||||
struct Bar {
|
||||
id int @[primary; sql: serial]
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table models.Foo
|
||||
create table Bar
|
||||
}!
|
||||
```
|
||||
|
||||
### Create & Drop Tables
|
||||
|
||||
You can create and drop tables by passing the struct to `create table` and `drop table`.
|
||||
|
||||
```v ignore
|
||||
import models.Foo
|
||||
|
||||
struct Bar {
|
||||
id int @[primary; sql: serial]
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table models.Foo
|
||||
drop table Bar
|
||||
}!
|
||||
```
|
||||
|
||||
### Insert Records
|
||||
|
||||
To insert a record, create a struct and pass the variable to the query. Again, reference the struct
|
||||
as the table.
|
||||
|
||||
```v ignore
|
||||
foo := Foo{
|
||||
name: 'abc'
|
||||
created_at: time.now()
|
||||
// updated_at defaults to none
|
||||
// deleted_at defaults to none
|
||||
children: [
|
||||
Child{
|
||||
name: 'abc'
|
||||
},
|
||||
Child{
|
||||
name: 'def'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
foo_id := sql db {
|
||||
insert foo into Foo
|
||||
}!
|
||||
```
|
||||
|
||||
You can insert a flat array of records in one statement. Bulk inserts currently
|
||||
support primitive, enum, and `time.Time` fields. Rows with `serial` or `default`
|
||||
fields fall back to per-row inserts so each row keeps normal default handling.
|
||||
|
||||
```v ignore
|
||||
users := [
|
||||
User{
|
||||
id: 1
|
||||
name: 'Alice'
|
||||
},
|
||||
User{
|
||||
id: 2
|
||||
name: 'Bob'
|
||||
},
|
||||
]
|
||||
|
||||
sql db {
|
||||
insert users into User
|
||||
}!
|
||||
```
|
||||
|
||||
If the `id` field is marked as `sql: serial` and `primary`, the insert expression
|
||||
returns the database ID of the newly added object. Getting an ID of a newly
|
||||
added DB row is often useful.
|
||||
|
||||
When inserting, `[sql: serial]` fields, and fields with a `[default: 'raw_sql']`
|
||||
attribute, are not sent to the database when the value being sent is the default
|
||||
for the V struct field (e.g., 0 int, or an empty string). This allows the
|
||||
database to insert default values for auto-increment fields and where you have
|
||||
specified a default.
|
||||
|
||||
### Upsert
|
||||
|
||||
`upsert` inserts a row or updates the matching row when one of the table's
|
||||
primary or unique keys already exists.
|
||||
|
||||
```v ignore
|
||||
foo := Foo{
|
||||
name: 'abc'
|
||||
}
|
||||
|
||||
sql db {
|
||||
upsert foo into Foo
|
||||
}!
|
||||
```
|
||||
|
||||
`upsert` currently supports flat ORM rows with primitive, enum, and `time.Time`
|
||||
fields.
|
||||
|
||||
### Select
|
||||
|
||||
You can select rows from the database by passing the struct as the table, and
|
||||
use V syntax and functions for expressions. Selecting returns an array of the
|
||||
results.
|
||||
|
||||
```v ignore
|
||||
result := sql db {
|
||||
select from Foo where id == 1
|
||||
}!
|
||||
|
||||
foo := result.first()
|
||||
```
|
||||
|
||||
You can also select a subset of struct fields. The result type stays `[]Foo`;
|
||||
the selected fields are populated and the rest keep their zero values. Use the
|
||||
V struct field names here; `@[sql: 'column_name']` and `@[sql_select: 'expr']`
|
||||
are still applied automatically.
|
||||
|
||||
```v ignore
|
||||
partial := sql db {
|
||||
select id, name from Foo where id > 1
|
||||
}!
|
||||
```
|
||||
|
||||
```v ignore
|
||||
result := sql db {
|
||||
select from Foo where id > 1 && name != 'lasanha' limit 5
|
||||
}!
|
||||
```
|
||||
|
||||
```v ignore
|
||||
result := sql db {
|
||||
select from Foo where id > 1 order by id desc
|
||||
}!
|
||||
```
|
||||
|
||||
Dynamic ORM blocks can build `WHERE` and `SET` data conditionally. Commas between
|
||||
emitted dynamic `where` items are joined with `AND`; use `&&` and `||` inside an
|
||||
item for explicit boolean conditions.
|
||||
|
||||
```v ignore
|
||||
where_filter := {
|
||||
if name := req.name {
|
||||
name == name
|
||||
},
|
||||
id == user_id || tenant_id == tenant_id
|
||||
}
|
||||
|
||||
rows := sql db {
|
||||
dynamic select from Foo where where_filter
|
||||
}!
|
||||
|
||||
update_data := {
|
||||
name == new_name
|
||||
}
|
||||
|
||||
sql db {
|
||||
dynamic update Foo set update_data where {
|
||||
id == user_id || tenant_id == tenant_id
|
||||
}
|
||||
}!
|
||||
```
|
||||
|
||||
ORM select expressions also support built-in aggregate functions. `count` keeps
|
||||
its legacy syntax, while the other aggregates use SQL-like function calls.
|
||||
|
||||
```v ignore
|
||||
total_age := sql db {
|
||||
select sum(age) from Foo
|
||||
}!
|
||||
|
||||
average_age := sql db {
|
||||
select avg(age) from Foo where id > 1
|
||||
}!
|
||||
|
||||
lowest_name := sql db {
|
||||
select min(name) from Foo
|
||||
}!
|
||||
|
||||
highest_created_at := sql db {
|
||||
select max(created_at) from Foo
|
||||
}!
|
||||
```
|
||||
|
||||
`sum`, `avg`, `min`, and `max` return options so empty result sets can surface
|
||||
SQL `NULL` as `none`. `count` continues to return `int`.
|
||||
|
||||
### Transactions
|
||||
|
||||
ORM transactions work with both `sql tx {}` and the function-call API.
|
||||
|
||||
```v ignore
|
||||
import orm
|
||||
|
||||
orm.transaction[int](mut db, fn (mut tx orm.Tx) !int {
|
||||
user := User{
|
||||
name: 'Alice'
|
||||
}
|
||||
sql tx {
|
||||
insert user into User
|
||||
}!
|
||||
return tx.last_id()
|
||||
})!
|
||||
```
|
||||
|
||||
For manual control, start a transaction explicitly and commit or roll it back yourself.
|
||||
|
||||
```v ignore
|
||||
import orm
|
||||
|
||||
mut tx := orm.begin(mut db)!
|
||||
sql tx {
|
||||
update User set name = 'Bob' where id == 1
|
||||
}!
|
||||
tx.commit()!
|
||||
```
|
||||
|
||||
Nested transactions use savepoints instead of a second `BEGIN`.
|
||||
|
||||
```v ignore
|
||||
import orm
|
||||
|
||||
orm.transaction[int](mut db, fn (mut tx orm.Tx) !int {
|
||||
tx.transaction[int](fn (mut nested orm.Tx) !int {
|
||||
sql nested {
|
||||
delete from User where id == 2
|
||||
}!
|
||||
return 0
|
||||
})!
|
||||
return 0
|
||||
})!
|
||||
```
|
||||
|
||||
Transaction helpers use each driver's default transaction mode. v1 does not expose isolation
|
||||
levels or SQLite begin-mode configuration yet.
|
||||
|
||||
### Update
|
||||
|
||||
You can update fields in a row using V syntax and functions. Again, pass the struct
|
||||
as the table.
|
||||
|
||||
```v ignore
|
||||
sql db {
|
||||
update Foo set updated_at = time.now() where name == 'abc' && updated_at is none
|
||||
}!
|
||||
```
|
||||
|
||||
You can update multiple rows from an array in one statement by using the array
|
||||
variable in each assigned value and in the key comparison.
|
||||
|
||||
```v ignore
|
||||
updates := [
|
||||
User{
|
||||
id: 1
|
||||
name: 'Alicia'
|
||||
},
|
||||
User{
|
||||
id: 2
|
||||
name: 'Robert'
|
||||
},
|
||||
]
|
||||
|
||||
sql db {
|
||||
update User set name = updates.name where id == updates.id
|
||||
}!
|
||||
```
|
||||
|
||||
For a Rails-style full-record save, load a struct, mutate it, then call `orm.save`.
|
||||
The helper uses the struct primary key, or an `id` field when present, for the
|
||||
`WHERE` clause and updates the remaining mapped fields automatically.
|
||||
|
||||
```v ignore
|
||||
import orm
|
||||
|
||||
mut foo := (sql db {
|
||||
select from Foo where id == 1
|
||||
}!).first()
|
||||
foo.name = 'updated'
|
||||
foo.updated_at = time.now()
|
||||
|
||||
orm.save(db, foo)!
|
||||
```
|
||||
|
||||
Note that `is none` and `!is none` can be used to select for NULL fields.
|
||||
|
||||
### Delete
|
||||
|
||||
You can delete rows using V syntax and functions. Again, pass the struct
|
||||
as the table.
|
||||
|
||||
```v ignore
|
||||
sql db {
|
||||
delete from Foo where id > 10
|
||||
}!
|
||||
```
|
||||
|
||||
### time.Time Fields
|
||||
|
||||
It's definitely useful to cast a field as `time.Time` so you can use V's built-in time functions;
|
||||
however, this is handled a bit differently than expected in the ORM. `time.Time` fields are
|
||||
created as integer columns in the database. Because of this, the usual time functions
|
||||
(`current_timestamp`, `NOW()`, etc) in SQL do not work as defaults.
|
||||
|
||||
## Example
|
||||
|
||||
```v ignore
|
||||
import db.pg
|
||||
|
||||
struct Member {
|
||||
id string @[default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
|
||||
name string
|
||||
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
|
||||
}
|
||||
|
||||
fn main() {
|
||||
db := pg.connect(pg.Config{
|
||||
host: 'localhost'
|
||||
port: 5432
|
||||
user: 'user'
|
||||
password: 'password'
|
||||
dbname: 'dbname'
|
||||
})!
|
||||
|
||||
defer {
|
||||
db.close()
|
||||
}
|
||||
|
||||
sql db {
|
||||
create table Member
|
||||
}!
|
||||
|
||||
new_member := Member{
|
||||
name: 'John Doe'
|
||||
}
|
||||
|
||||
sql db {
|
||||
insert new_member into Member
|
||||
}!
|
||||
|
||||
selected_members := sql db {
|
||||
select from Member where name == 'John Doe' limit 1
|
||||
}!
|
||||
john_doe := selected_members.first()
|
||||
|
||||
sql db {
|
||||
update Member set name = 'Hitalo' where id == john_doe.id
|
||||
}!
|
||||
}
|
||||
```
|
||||
|
||||
## Function Call API
|
||||
You can utilize the `Function Call API` to work with `ORM`. It provides the
|
||||
capability to dynamically construct SQL statements. The Function Call API
|
||||
supports common operations such as `Create Table`/`Drop Table`/`Insert`/`Delete`/`Update`/`Select`,
|
||||
and offers convenient yet powerful features for constructing `WHERE` clauses,
|
||||
`SET` clauses, `SELECT` clauses, and more.
|
||||
|
||||
A complete example is available [here](https://github.com/vlang/v/blob/master/vlib/orm/orm_func_test.v).
|
||||
|
||||
Below, we illustrate its usage through several examples.
|
||||
|
||||
1. Define your struct with the same method definitions as before:
|
||||
|
||||
```v ignore
|
||||
@[table: 'sys_users']
|
||||
struct User {
|
||||
id int @[primary;serial]
|
||||
name string
|
||||
age int
|
||||
role string
|
||||
status int
|
||||
salary int
|
||||
title string
|
||||
score int
|
||||
created_at ?time.Time @[sql_type: 'TIMESTAMP']
|
||||
}
|
||||
```
|
||||
|
||||
2. Create a database connection:
|
||||
|
||||
```v ignore
|
||||
mut db := sqlite.connect(':memory:')!
|
||||
defer { db.close() or {} }
|
||||
```
|
||||
|
||||
3. Create a `QueryBuilder` (which also completes struct mapping):
|
||||
|
||||
```v ignore
|
||||
mut qb := orm.new_query[User](db)
|
||||
```
|
||||
|
||||
4. Create a database table:
|
||||
|
||||
```v ignore
|
||||
qb.create()!
|
||||
```
|
||||
|
||||
5. Insert multiple records into the table:
|
||||
|
||||
```v ignore
|
||||
qb.insert_many(users)!
|
||||
```
|
||||
|
||||
6. Delete records (note: `delete()` must follow `where()`):
|
||||
|
||||
```v ignore
|
||||
qb.where('name = ?','John')!.delete()!
|
||||
```
|
||||
|
||||
7. Query records (you can specify fields of interest via `select`):
|
||||
|
||||
```v ignore
|
||||
// Returns []User with only 'name' populated; other fields are zero values.
|
||||
only_names := qb.select('name')!.query()!
|
||||
```
|
||||
|
||||
8. Update records (note: `update()` must be placed last):
|
||||
|
||||
```v ignore
|
||||
qb.set('age = ?, title = ?', 71, 'boss')!.where('name = ?','John')!.update()!
|
||||
```
|
||||
|
||||
For a full-record update without spelling out each `set(...)` clause, use `orm.save`:
|
||||
|
||||
```v ignore
|
||||
selected := qb.where('name = ?', 'John')!.query()!
|
||||
mut john := selected.first()
|
||||
john.age = 72
|
||||
john.title = 'lead'
|
||||
orm.save(db, john)!
|
||||
```
|
||||
|
||||
9. Query aggregate values:
|
||||
|
||||
```v ignore
|
||||
total_age := qb.sum('age')!
|
||||
average_score := qb.avg('score')!
|
||||
first_name := qb.min('name')!
|
||||
latest_created_at := qb.max('created_at')!
|
||||
count := qb.count()!
|
||||
|
||||
assert total_age.as_int()? == 42
|
||||
assert average_score.as_f64()? == 9.5
|
||||
assert first_name.as_string()? == 'Alice'
|
||||
assert latest_created_at.as_time()? == created_at
|
||||
```
|
||||
|
||||
`sum`, `avg`, `min`, and `max` return an `AggregateValue`. Use
|
||||
`as_int()`, `as_f64()`, `as_string()`, or `as_time()` to unwrap the typed
|
||||
value, or check `has_value` for empty result sets. `count` returns `int`.
|
||||
|
||||
To remove duplicate rows from a query, mark it as `DISTINCT` before `query()`:
|
||||
|
||||
```v ignore
|
||||
distinct_roles := qb.select('role')!.distinct()!.query()!
|
||||
```
|
||||
|
||||
9. Drop the table:
|
||||
|
||||
```v ignore
|
||||
qb.drop()!
|
||||
```
|
||||
|
||||
10. Chainable method calls:
|
||||
Most Function Call API support chainable calls, allowing easy method chaining:
|
||||
|
||||
```v ignore
|
||||
final_users :=
|
||||
qb
|
||||
.drop()!
|
||||
.create()!
|
||||
.insert_many(users)!
|
||||
.set('name = ?', 'haha')!.where('name = ?', 'Tom')!.update()!
|
||||
.where('age >= ?', 30)!.delete()!
|
||||
.query()!
|
||||
```
|
||||
|
||||
11. Writing complex nested `WHERE` clauses:
|
||||
The API includes a built-in parser to handle intricate `WHERE` clause conditions. For example:
|
||||
|
||||
```v ignore
|
||||
where('created_at IS NULL && ((salary > ? && age < ?) || (role LIKE ?))', 2000, 30, '%employee%')!
|
||||
```
|
||||
|
||||
Note the use of placeholders `?`.
|
||||
The conditional expressions support logical operators including `AND`, `OR`, `||`, and `&&`.
|
||||
Named arrays can also be passed directly for `IN` clauses:
|
||||
|
||||
```v ignore
|
||||
user_ids := ['1', '2']
|
||||
users := qb.where('id IN ?', user_ids)!.query()!
|
||||
```
|
||||
Reference in new issue
Block a user