Хранилище
Все примеры используют SQLite в памяти (sqlite::memory:), поэтому не оставляют файлов и не требуют сервера. Чтобы работать с файлом или другой базой, смените строку
подключения: sqlite://notes.db, postgres://…, mysql://… — код остаётся прежним. Операции ORM требуют эффекта db.
CRUD#
Задача. Создать, прочитать, обновить и удалить записи.
use std.orm { Db }
@table("notes")
data Note {
@pk @auto id: i64 // первичный ключ, значение выдаёт база
title: str
done: bool
}
fn main() io, db {
let db = Db.open("sqlite::memory:")! // в памяти; для файла: sqlite://notes.db
defer db.close()
db.insert(Note{ id: 0, title: "write docs", done: false })
let first = db.lastInsertId()
db.insert(Note{ id: 0, title: "ship it", done: false })
print("{db.count(Note)} notes, first id {first}")
match db.find(Note, first) {
Note n => {
db.update(Note{ id: n.id, title: n.title, done: true })
print("done: {db.find(Note, first)!.done}")
}
_ => print("no such note")
}
db.deleteById<Note, i64>(first)!
print("left: {db.all(Note).len()}")
print("missing: {db.find(Note, 99) is none}")
}Вывод
2 notes, first id 1 done: true left: 1 missing: true
@table("notes")превращаетdataв таблицу;@pk— первичный ключ,@auto— значение выдаёт база (передавайте0как заглушку).db.insert(…),db.update(…),db.find(Note, id)(T?),db.all(Note),db.count(Note),deleteById— основные операции. Вариантыtry…(tryInsert) возвращаютDbErrorвместо паники, например при конфликте ключа.defer db.close()— соединение закрывается при любом выходе.- Схему создаёт и обновляет миграция (
oxl db, §12.8); для примеров сsqlite::memory:таблица создаётся автоматически.
Запросы#
Задача. Выбрать, отсортировать и разбить на страницы.
use std.orm { Db }
@table("products")
data Product {
@pk id: i64
name: str
price: i64
category: str
}
fn main() io, db {
let db = Db.open("sqlite::memory:")!
defer db.close()
db.insert(Product{ id: 1, name: "pen", price: 3, category: "office" })
db.insert(Product{ id: 2, name: "desk", price: 250, category: "furniture" })
db.insert(Product{ id: 3, name: "lamp", price: 40, category: "furniture" })
db.insert(Product{ id: 4, name: "ink", price: 15, category: "office" })
let cheap = from p in db.Product where p.price < 50 order by p.price desc select p.name // запрос превращается в SQL при сборке
print("{cheap}")
let furniture = from p in db.Product where p.category == "furniture" order by p.name select (p.name, p.price)
for (name, price) in furniture { print("{name}: {price}") }
let page = from p in db.Product order by p.id skip 1 take 2 select p.id
print("{page}")
}Вывод
["lamp", "ink", "pen"] desk: 250 lamp: 40 [2, 3]
from p in db.Product where p.price < 50 order by p.price desc select p.name— выражение запроса. Для таблицы оно компилируется в SQL при сборке: значения передаются параметрами, а выражение, которое нельзя перевести в SQL, — ошибка компиляции (query-not-sql).select (p.name, p.price)возвращает кортежи;skip/take— постраничность.- Тот же синтаксис работает над списками в памяти (шаг 6 учебника): запрос можно сначала отладить на
List, затем направить на таблицу. - Анти-паттерн «N+1» (запрос в цикле по результатам другого запроса) определяет анализатор —
n-plus-one; связи загружаются черезload(поле).
Транзакции#
Задача. Перевести деньги между счётами так, чтобы при любой неудаче не осталось «половины» операции.
use std.orm { Db }
@table("accounts")
data Account { @pk id: i64; owner: str; cents: i64 }
fn transfer(db: &Db, from: i64, to: i64, amount: i64) -> bool io, db {
db.transaction(() => { // всё внутри — одна транзакция
let a = db.find(Account, from)
let b = db.find(Account, to)
match (a, b) {
(Account x, Account y) => {
if x.cents < amount { return false }
db.update(Account{ id: x.id, owner: x.owner, cents: x.cents - amount })
db.update(Account{ id: y.id, owner: y.owner, cents: y.cents + amount })
true
}
_ => false
}
})
}
fn main() io, db {
let db = Db.open("sqlite::memory:")!
defer db.close()
db.insert(Account{ id: 1, owner: "ann", cents: 1000 })
db.insert(Account{ id: 2, owner: "bob", cents: 0 })
print("{transfer(&db, 1, 2, 300)} {transfer(&db, 1, 2, 5000)} {transfer(&db, 1, 9, 1)}")
for a in db.all(Account) { print("{a.owner}: {a.cents}") }
}Вывод
true false false ann: 700 bob: 300
db.transaction(() => { … })выполняет всё тело как одну транзакцию: изменения фиксируются вместе.- Ранний
return falseвнутри тела допустим: неудачные переводы (false) ничего не изменили, на счетах остались 700 и 300 после единственного успешного. - Для операций, затрагивающих несколько узлов кластера, транзакции заменяют саги с компенсациями — пример bank-saga.
Ключ-значение#
Задача. Хранить небольшие записи по ключам с префиксным поиском.
use std.kv { Kv }
fn main() io, db {
let kv = Kv.open("recipes")!
kv.put("user:1", b"ann")!
kv.put("user:2", b"bob")!
kv.put("order:1", b"pen")!
let one = kv.get("user:1")!
print("user:1 present: {one is bytes}")
let users = kv.scan("user:", 10)!
print("{users.len()} users")
kv.delete("user:2")!
print("{kv.scan("user:", 10)!.len()} left")
}Вывод
user:1 present: true 2 users 1 left
Kv.open("recipes")открывает именованное встроенное хранилище; значения —bytes, поэтому сериализуйте данные сами (toJson,encode).scan(prefix, limit)перебирает ключи с общим префиксом по порядку;putIf(key, value, version)— условная запись для оптимистичной блокировки;watch(prefix)— поток изменений.- Встроенное хранилище реплицируется по кластеру (Raft), поэтому та же программа в кластере получает отказоустойчивость без изменений кода.
Вернуться: кулинарная книга.