OxL Справочник OxL
РусскийEnglish

Хранилище

Все примеры используют SQLite в памяти (sqlite::memory:), поэтому не оставляют файлов и не требуют сервера. Чтобы работать с файлом или другой базой, смените строку подключения: sqlite://notes.db, postgres://…, mysql://… — код остаётся прежним. Операции ORM требуют эффекта db.

CRUD#

Задача. Создать, прочитать, обновить и удалить записи.

oxl✓ проверяется в CI
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: таблица создаётся автоматически.

Запросы#

Задача. Выбрать, отсортировать и разбить на страницы.

oxl✓ проверяется в CI
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(поле).

Транзакции#

Задача. Перевести деньги между счётами так, чтобы при любой неудаче не осталось «половины» операции.

oxl✓ проверяется в CI
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.

Ключ-значение#

Задача. Хранить небольшие записи по ключам с префиксным поиском.

oxl✓ проверяется в CI
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), поэтому та же программа в кластере получает отказоустойчивость без изменений кода.

Вернуться: кулинарная книга.

См. также

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства