База данных
База данных COOPENOMICS — это встроенная распределённая система хранения данных платформы, организованная в таблицы для управления состояниями смарт-контрактов. Изменение данных осуществляется через специальные методы, называемые действиями, которые задаются в коде смарт-контрактов.
В процессе синхронизации каждая нода воспроизводит все действия из всех транзакций и последовательно восстанавливает актуальное состояние базы данных. База данных платформы хранится в оперативной памяти каждой PEER-ноды и используется ими для проверки новых транзакций, а также, для чтения состояния внешними приложениями.
Информацию из базы данных можно извлекать посредством API, пользуясь GET запросами с параметрами для поиска информации на контрактах. А поскольку эта база данных формируется и поддерживается всеми PEER-нодами одновременно, то она - распределённая, и одинаковая для всех.
Сравним с обычной базой данных
POSTGRES - это реляционная база данных, которая позволяет выполнять операции создания, удаления, редактирования и чтения любой записи в любой таблице. База данных, при этом, никаким образом не ограничивает разработчиков в том, как с ней работать, и не требует соблюдения каких-либо бизнес-правил при изменении данных в ней. Реализация бизнес-правил выносится разработчиками за пределы самой базы данных в отдельные приложения, которые решают: когда и кому можно прочитать, добавить, изменить или обновить данные. POSTGRES к этим правилам никакого отношения не имеет и никаких ограничений не накладывает. Бизнес-правила и информация в базе данных никак не связаны.
COOPENOMICS - это распределенная база данных, которая хранит информацию и бизнес-правила по её изменению в смарт-контрактах. Информация описываются структурами данных, а правила - программным кодом. Правила, которые описаны в программном коде смарт-контрактов позволяют изменить состояние распределенной базы данных только в том случае, если все программные условия смарт-контракта выполнены. И если обычная база никаким образом не регламентирует разработчикам когда они могут добавить информацию, а когда удалить, то смарт-контракты в распределенной базе данных - это делают. Мы не можем создать запись, если в смарт-контракте указано, что для этого пользователь должен быть пайщиком, или удалить её, если пайщик - активен. Таким образом, в COOPENOMICS база данных и правила по изменению информации в ней неразрывно связаны.
Действия и таблицы в COOPENOMICS неразрывно связаны, образуя единое целое: каждое изменение в таблице возможно только через выполнение действия, определённого в коде смарт-контракта. Нельзя напрямую изменить данные таблицы, поскольку все модификации выполняются через строго заданные правила, описанные в действиях. Это гарантирует, что любые изменения данных проходят проверку логики контракта и согласуются с правилами платформы.
Типы данных¶
Все структуры данных в COOPENOMICS описываются с помощью следующих типов:
Тип данных | Описание |
---|---|
uint64_t |
64-битное беззнаковое целое число. Используется для хранения идентификаторов и других числовых значений. |
uint32_t |
32-битное беззнаковое целое число. Применяется для меньших чисел, например, счетчиков или меток времени. |
uint16_t |
16-битное беззнаковое целое число. Используется для хранения небольших числовых данных. |
uint8_t |
8-битное беззнаковое целое число. Часто используется для хранения флагов или маленьких чисел. |
int64_t |
64-битное знаковое целое число. Применяется для значений, которые могут быть отрицательными. |
int32_t |
32-битное знаковое целое число. Используется для хранения небольших отрицательных или положительных чисел. |
int16_t |
16-битное знаковое целое число. Для небольших числовых данных. |
int8_t |
8-битное знаковое целое число. Часто используется для флагов или небольших чисел. |
float64_t |
64-битное число с плавающей точкой. Используется для хранения вещественных чисел с высокой точностью. |
float32_t |
32-битное число с плавающей точкой. Используется для вещественных чисел меньшей точности. |
bool |
Логический тип. Хранит значение true или false . |
name |
Специальный тип, представляющий имя аккаунта или идентификатор. Ограничен 12 символами a-z и цифрами 1-5 . |
asset |
Тип для хранения токенов с их количеством и символом валюты. |
checksum256 |
256-битный хеш. Применяется для хранения контрольных сумм или идентификаторов. |
time_point |
Тип для хранения меток времени с микросекундной точностью. |
time_point_sec |
Тип для хранения меток времени с секундной точностью. |
block_timestamp_type |
Тип для хранения временных меток блоков в сети. |
std::string |
Тип строки для хранения текстовых данных. |
std::vector<T> |
Массив данных заданного типа T . Используется для хранения списков или массивов данных. |
std::optional<T> |
Тип, указывающий на возможность наличия или отсутствия значения типа T . |
Структуры данных¶
Типы данных составляют структуры:
struct User {
uint64_t id; // Уникальный идентификатор пользователя
std::string name; // Имя пользователя
uint32_t age; // Возраст пользователя
};
Структуры могут содержать структуры:
struct Address {
std::string city; // Город
std::string street; // Улица
uint32_t house_number;// Номер дома
};
struct UserWithAddress {
uint64_t id; // Уникальный идентификатор
std::string name; // Имя
Address address; // Адрес пользователя
};
И массивы структур:
struct Post {
uint64_t id; // Уникальный идентификатор поста
std::string title; // Заголовок поста
std::string content; // Содержимое поста
};
struct UserWithPosts {
uint64_t id; // Уникальный идентификатор пользователя
std::string name; // Имя пользователя
std::vector<Post> posts; // Список постов пользователя
};
Также, поля в структурах могут быть не обязательными:
struct Profile {
uint64_t id; // Уникальный идентификатор
std::string username; // Имя пользователя
std::optional<std::string> bio; // Биография (может отсутствовать)
};
Таблицы¶
Таблицы определяются указанием в структуре макроса, ключа и типа индекса.
struct [[eosio::table]] Product { # (1)
uint64_t id; # (2)
std::string name; # (3)
uint64_t category_id; # (4)
double price; # (5)
uint64_t primary_key() const { return id; } # (6)
};
typedef eosio::multi_index<"products"_n, Product> product_table; # (7)
-
Здесь [[eosio::table]] - это макрос, указывающий что структура - это таблица, и она должна быть отображена в API смарт-контракта для доступа извне. Этот макрос используется компилятором при генерации бинарного интерфейса смарт-контракта, который затем будет установлен на платформе.
-
Уникальный идентификатор продукта
-
Название продукта
-
ID категории
-
Цена
-
Это основной индекс, который будет использоваться для проверки уникальности и поиска. Его тип - uint64_t, как и тип поля id, которое указано в качестве основного индекса.
-
typedef eosio::multi_index<"products"_n, Product> product_table; - это определение типа индекса в таблице, которое заявляет что таблица будет храниться в области памяти под именем "products"_n, там будет храниться объект типа Product, и имя этому индексу - product_table.
Макрос кодировки имени аккаунта
_n - это макрос, который говорит о том, что указанную строку необходимо перевести в кодировку имен аккаунтов; на платформе имена аккаунтов кодируются в числовые аналоги, т.е. если мы видим имя аккаунта в виде строки, то блокчейн "видит" большое число, которым закодирована строка. А т.к. в именах аккаунтов допустимы только буквы a-z и цифры 1-5, а также, длинна должна быть не более 12 символов, то ими мы и ограничены в создании пространства имен таблиц, по которым они будут доступны по API. Макрос _n - это быстрый перевод указанной строки в имя аккаунта, у которого есть функциональный аналог: name("products").
Чтобы обратиться к описанной таблице в смарт-контракте необходимо:
product_table table1("contractname"_n, "contractname".value); # (1)
auto product = table1.find(1); # (2)
print(product -> name); # (3)
-
Строим индекс, к которому мы будем обращаться. Где первый контракт в скобках указывает на то имя контракта, который является хранилищем данных. Каждый контракт может запрашивать информацию из любых других контрактов, если он осведомлён о её структуре. Второй же контракт указывает на область памяти, в которой хранится информация. В данном случае мы ищем информацию в области памяти самого контракта, однако, могли бы заменить её, например, на область памяти username.value, то говорило бы о том, что записи необходимо искать в области памяти пользователя.
-
Ищем продукт под индексом 1.
-
Распечатываем имя продукта в консоль.
Создание записи¶
Описанный выше пример показывает, как на платформе реализуется поиск информации. Но как добавлять, редактировать и удалять информацию? Для этого применяются методы emplace, modify и erase на индексах.
product_table table1("contractname"_n, "contractname".value); # (1)
table1.emplace("contractname"_n, [&](auto &row){ # (2)
row.id = 1; # (3)
row.name = "Ball"; # (4)
row.category_id = 1; # (5)
row.price = 100; # (6)
})
-
Вновь строим индекс в котором мы будем создавать запись в контракте "contractname" в глобальной области памяти самого контракта "contractname".
-
Вызываем метод emplace, где первый параметр - это имя плательщика за системный ресурс оперативную памяти (RAM), который будет расчитан исходя из того, какое количество информации будет фактически сохранено. Вторым параметром мы передаем лямбда-функцию с названием строки, которую будем менять. Обычно эта лямбда-функция всегда одинакова для всех вызовов и её просто нужно запомнить.
-
Устанавливаем идентификатор строки
-
Задаём значение поля name
-
Задаём значение категории
-
Задаём значение идентификатора категории
Редактирование записи¶
Для редактирования записи, нам нужно её найти и применить метод modify:
product_table table1("contractname"_n, "contractname".value); # (1)
auto product = table1.find(1); # (2)
table1.modify(product, "contractname"_n, [&](auto &row){ # (3)
row.price = 200; # (4)
});
- Формируем индекс
- Ищем запись по primary_key
- Вызываем метод modify, где первый аргумент - это указатель на ранее найденный продукт, второй - это имя аккаунта плательщика за RAM, и третий аргумент, как и ранее - это лямбда функция, которую мы применяем для того, чтобы получить доступ к строке row.
- Устанавливаем новую цену продукта
Удаление записи¶
Для удаления записи, аналогично, нам нужно её найти и применить метод erase:
product_table table1("contractname"_n, "contractname".value); # (1)
auto product = table1.find(1); # (2)
table1.erase(product); # (3)
- Формируем индекс
- Ищем запись по primary_key
- Вызываем метод erase и передаем указатель на ранее найденный продукт
Удаление записи производится из состояния распределенной базы данных, которое хранится в оперативной памяти всех PEER-нод одновременно. Поэтому, удаленные данные могут быть найдены только в HISTORY-нодах, которые содержат подробную информацию о всех действиях и всех изменениях в таблицах, которые они повлекли.
Действия¶
Программный код по изменению информации в таблицах распределенной базы данных всегда находится в действиях смарт-контрактах. Невозможно изменить информацию в таблицах минуя действия смарт-контракта. Действия же могут содержать дополнительные правила и проверки, которые отражают в себе реальные бизнес-правила, по которым функционирует система. Таким образом, бизнес-правила неразрывно связаны с информацией в базе данных.
Например, прежде чем добавить запись в таблицу заявок на поставку продукта, мы проверим - а является ли пользователь - пайщиком кооператива? Но до этого - нам необходимо объявить само действие и проверить права доступа.
[[eosio::action]] createorder(eosio::name username, uint64_t product_id) { # (1)
require_auth(username); # (2)
product_table table1("contractname"_n, "contractname".value); # (3)
auto product = table1.find(1); # (4)
eosio::check(product != table1.end(), "Продукт не найден"); # (5)
orders_table orders("contractname"_n, "contractname".value) # (6)
orders.emplace("contractname"_n, [&](auto &row){ # (7)
row.order_id = available_primary_key(); # (8)
row.username = username; # (9)
row.product_id = product_id; # (10)
});
}
-
Объявляем действие смарт-контракта как функцию с указанием макроса [[eosio::action]] и аргументами, которое действие будет принимать.
-
Указываем требуемую аудентификацию, которой контракт будет требовать при вызове действия. В данном случае контракт сможет исполнить действие только в том случае, если он будет вызвано пользователем с именем аккаунта username.
-
Формируем индекс продуктов
-
Ищем продукт
-
Проверяем существует ли продукт. В случае, если указатель product соответствует концу таблицу table1.end(), выполнение действия немедленно завершится с ошибкой, что продукт не найден.
-
Формируем индекс заявок
-
Как и ранее, применяем функцию emplace на индексе и извлекаем строку для добавления
-
Получаем доступный primary_key, который будет расчитан автоматически
-
Присваиваем имя пользователя
-
Присваиваем заказу идентификатор продукта
Вывод¶
Распределенная база включает в себя бизнес-правила, которые нельзя обойти: невозможно добавить запись в базу данных, не пройдя хотя бы одну из проверок в действии смарт-контракта. И таких проверок может быть много, и все они могут быть достаточно сложными.
Если в действиях смарт-контракта заложен программный код, который добавляет, редактирует или удаляет запись в базе данных, то это будет произведено на всех нодах автоматически после включения транзакции в цепочку блоков истории. Обмен информацией между нодами происходит дважды в секунду.
Смарт-контракты позволяют описывать структуры таблиц и типы данных в них, а затем, создавать, редактировать, искать и удалять информацию в базе данных посредством вызова действий. Логика работы с данными в таблицах смарт-контрактов лежит также в смарт-контрактах. А вся история вызовов - сохраняется в неразрывную цепочку истории.
Не каждое действие смарт-контракта может влиять на состояние таблиц распределенной базы данных - некоторые действия могут не вносить никаких изменений в таблицы, и при этом, исполняться. Это допустимо.
Все действия из транзакций применяются к состоянию базы данных последовательно. Если одно из действий в транзакции не может пройти проверку на математических условиях программного кода смарт-контракта, то такое действие отклоняется нодой автоматически вместе со всей транзакцией и всеми действиями в ней.
Программный код смарт-контрактов вызывается в момент применения действия из транзакции. Это происходит каждый раз, когда любая PEER-нода получает входящую транзакцию от пользователя или от других нод в процессе синхронизации.
Каждое принятое действие изменяет историю блокчейна, и может влиять на состояние распределенной базы данных, создавая, редактируя или удаляя записи в таблицах смарт-контрактов. Невозможно изменить таблицы смарт-контрактов в обход программного кода их действий - они неразрывно связаны в единое целое, и этим база данных COOPENOMICS отличается от любой другой базы данных.