MR::Tarantool::Box::Record
- объектная обертка над клиентом тарантула/октопуса MR::Tarantool::Box::XS
. Каждый класс соответствует неймспейсу коробки, каждый объект - одному туплу.
Класс тупла объявляется аналогично любому маусовскому классу, за иcключением того, что вместо класса Mouse
используется MR::Tarantool::Box::Record
. Это приводит к тому, что:
- класс автоматически становится наследником класса
MR::Tarantool::Box::Record::Object
; - к его метаклассу применяется роль
MR::Tarantool::Box::Record::Trait::Class
, для хранения структуры полей и опций коробки; - импортируются дополнительные sugar-функции:
iproto
,namespace
,shard_by
,has_field
,has_field_object
,has_index
,has_index_part
.
package Sample::Record;
use MR::Tarantool::Box::Record;
# Собственно класс здесь
no MR::Tarantool::Box::Record;
__PACKAGE__->meta->make_immutable();
1;
Используется для того, чтобы указать, какой инcтанс клиента iproto-xs использовать для подключения к кластеру. Принимает либо объект MR::IProto::XS
, либо параметры для его конструктора (либо имя параметра в onlineconf, если используется MR::OnlineConf::Generic::Trait::Class::TarantoolBoxRecord
):
iproto($iproto);
iproto masters => ['1.2.3.4:1234'];
iproto '/my/video/video-shard-box';
Задает номер неймспейса:
namespace 22;
Определят функцию выбора шарда для новой записи:
shard_by { $_[0]->user->video->video_shard_num() };
Аналогична has
и создает соответствующий атрибут, но помимо этого еще применяет к его метаклассу роль MR::Tarantool::Box::Record::Trait::Attribute::Field
.
Понимает следующие дополнительные опции:
number
- номер поля в тупла, автоматически вычисляется на основании порядка объявления полей в модуле;format
- односимвольный формат хранимых в поле данных (по умолчанию:&
), при отсутствии прочих опций допускается сокращенная форма записи;primary_key
- является ли поле первичным ключем;index
- имя индекса, если поле является индексным;uniq
- уникальный ли индекс (по умолчанию - уникальный);shard_by
- функция (или имя метода), используемая для вычисления шарда на основании данных этого поля;index_number
- номер индекса, автоматически вычисляется на основании порядка объявления индексов;selector
- имя метода-селектора, который нужно создать для соответствующего индекса;mutators
- список методов-модификаторов, которые нужно создать для поля для выполнения спец-операций, допустимо:inc
,dec
,add
,and
,or
,xor
,set_bit
,clear_bit
;serialize
иdeserialize
- функции для упаковки/распаковки данных, вызываются при каждом чтении/записи (без lazy), поэтому использовать очень осторожно во избежание ухудшения производительности;sequence
- является ли поле автоинкрементным;sequence_id
- номер сиквенса, по умолчанию совпадает с номером неймспейса;sequence_iproto
- кластер iproto, используемый для хранения неймспейса с сиквенсами;sequence_namespace
- номер неймспейса с сиквенсами;size
- длина поля в байтах, для числовых полей вычисляется автоматически (используется только для прогнозирования объема хранилища);min_size
- минимальная длина строкового поля в байтах (используется только для прогнозирования объема хранилища);max_size
- максимальная длина строкового поля в байтах (используется только для прогнозирования объема хранилища).
Порядок, в котором перечисляются поля в классе важен и должен строго соответствовать порядку полей в тупле.
has_field id => (
format => 'L',
selector => 'select_by_id',
index => 'primary_id',
sequence => 1,
primary_key => 1,
);
has_field first_name => '&';
has_field last_name => '&';
has_field shard_num => (
format => 'S',
default => sub { My::ShardBoxBase->get_shard_for_new_user($_[0]->id) },
);
has_field base => (
format => 'L',
serialize => sub { $_ ? unpack('N', Socket::inet_aton($_)) : 0 },
deserialize => sub { $_ ? Socket::inet_ntoa(pack('N', $_)) : undef },
);
has_field owner_id => (
format => 'l',
index => 'owner',
uniq => 0,
shard_by => sub { 'all' },
);
Позволяет связать поле с атрибутом-объектом, например id пользователя с объектом пользователя. Фактически обертка над has
и понимает следующие дополнительные параметры:
field
- имя поля, с которым связывается объект;key
- имя метода-акцессора объекта, по которому можно получить ключ, по которому осуществляется связь;shard_by
- функция (или имя метода объекта), по которой можно определить в каком шарде храниться запись;to_object
- функция (или имя метода), позволяющая преобразовать значение поля в объект;selectors
- хэшимя_индекса => имя_метода
селекторов, позволяющих делать запросы с указанием объектов вместо значений полей.
has_field owner_id => (
format => 'l',
index => 'owner',
uniq => 0,
shard_by => sub { 'all' },
);
has_field_object owner => (
is => 'ro',
isa => 'UGC::User',
field => 'owner_id',
key => 'ID',
shard_by => 'VideoShardNum',
selectors => { owner => 'select_by_owner' },
to_object => sub { UGC::User->SelectByID($_) },
);
Применяется для создания индексов (как правило, мультиколоночных, так как одноколоночные проще описывать прямо в has_field
). Понимает следующие опции:
number
- номер индекса, автоматически вычисляется на основании порядка объявления индексов;fields
- список участвующих в индексе полей;uniq
- уникальный ли индекс (по умолчанию: уникальный);primary_key
- индекс является первичным ключем;default
- использовать ли индекс по умолчанию;selector
- имя метода-селектора, который нужно создать для индекса;shard_by
- функция (или имя метода), используемая для вычисления шарда на основании данных полей индекса;default_limit
- лимит по умолчанию для запросов по неуникальному индексу.
В сокращенной форме допускается указывать только имя индекса и список полей, либо даже только имя или только список полей.
Порядок в котором перечисляются индексы (в том числе неявно в has_field
) важен и должен совпадать с порядком индексов в конфиге тарантула.
# сокращенные формы:
has_index birthday => ['birthday_month', 'birthday_day']; # уникальный индекс с именем "birthday" по полям "birthday_month" и "birthday_day"
has_index 'email'; # уникальный индекс с именем "email" по полю "email"
has_index ['user_id', 'item_id']; # уникальный индекс с именем "user_id,item_id" по полям "user_id" и "item_id"
# неуникальный индекс (полная форма записи):
has_index birthay => (
fields => ['birthday_month', 'birthday_day'],
uniq => 0,
);
Применяется для создания возможности делать выборки по части мультиколоночного индекса. Допустимы параметры:
index
- имя мультиколоночного индекса;count
- количество используемых полей мультиколоночного индекса (по умолчанию: 1);uniq
- уникальный ли индекс (по умолчанию: не уникальный);selector
- имя метода-селектора, который нужно создать для индекса;shard_by
- функция (или имя метода), используемая для вычисления шарда на основании данных полей индекса;default_limit
- лимит по умолчанию для запросов по неуникальному индексу.
has_index user_sort => ['user_id', 'sort_index'];
has_index_part user => 'user_sort'; # Индекс только по полю user_id из индекса user_sort
# эквивалентно
has_index_part user => (
index => 'user_sort',
count => 1,
);
Зачастую возникает необходимость сделать поле автоинкрементным. Для этого можно завести (можно даже в стороночке) тарантул/октопус с неймспейсом, в котором будут храниться текущие значения всех сиквенсов. При этом удобно, чтобы для автоинкрементных первичных ключей номер сиквенса (значение первичного ключа в этом неймсейсе) совпадал с номером неймспейса. Все прочие сиквенсы лучше нумеровать откуда-нибудь от миллиона. Для автоматического использования этой возможности достаточно указать при объявлении поля параметр sequence => 1
. Но, разумеется, если настройки кластера iproto и номер неймспейса для коробочки не указаны глобально, то придется указать и их. Чтобы задать эти настройки глобально можно (но не рекомендуется) сделать примерно следующее:
my $meta = MR::Tarantool::Box::Record->meta;
$meta->sequence_iproto($sequence_iproto);
$meta->sequence_namespace(5);
Вместо этого рекомендуется отнаследовать от MR::Tarantool::Box::Record
свой класс как будет рассказано ниже.
Перед первым применением сиквенс необходимо инициализировать примерно следующим образом:
Sample::Record->meta->initialize_sequence('id'); # id - имя поля, для которого инициализируем sequence
Иногда бывает необходимо использоваться наследников этих классов, например чтобы считать определенную статистику. Сделать это можно (но не рекомендуется) примерно так:
my $meta = MR::Tarantool::Box::Record->meta;
$meta->iproto_class('My::IProto::XS');
$meta->box_class('My::Tarantool::Box::XS');
Вместо этого рекомендуется отнаследовать от MR::Tarantool::Box::Record
свой класс как будет рассказано ниже.
Для удобного использования онлайнконфа для конфигурирования клиента тарантула/октопуса есть два класса: MR::OnlineConf::Generic::IProtoXS
и MR::OnlineConf::Generic::Trait::Class::TarantoolBoxRecord
. Оба находятся в пакете perl-MR-OnlineConf-Generic
.
Первый содержит в себе функцию get_iproto_config
, которая позволяет считать из онлайнконфа конфигурацию кластера. Понимает конфигурацию с любой из следующих структур дерева:
- без шардинга:
- варинат 1:
path
- список мастеров через запятуюpath/replica
- список реплик через запятую
- вариант 2:
path/master
- список мастеров через запятуюpath/replica
- список реплик через запятую
- варинат 1:
- с шардингом:
- варинат 1:
path/max-shard
- количество шардовpath/i
- список мастеров шарда i через запятуюpath/i/replica
- список реплик для шарда i через запятую
- вариант 2:
path/max-shard
- количество шардовpath/i/master
- список мастеров шарда i через запятуюpath/i/replica
- список реплик для шарда i через запятую
- варинат 1:
Второй класс является трэйтом для метакласса ActiveRecord и позволяет использовать при вызове sugar-функции iproto
имя параметра в онлайнконфе. Как подключать - описано чуть ниже.
Может быть удобно для определения глобальных настроек. Ниже пример, который позволяет:
- использовать своих наследников от
MR::IProto::XS
иMR::Tarantool::Box::XS
; - установить параметры неймспейса для сиквенсов;
- использовать обертку для онлайнконф.
package My::Tarantool::Box::Record;
use Mouse::Exporter;
use MR::Tarantool::Box::Record ();
use MR::OnlineConf::Generic::IProtoXS;
use My::IProto::XS;
use My::Tarantool::Box::XS;
Mouse::Exporter->setup_import_methods(
also => 'MR::Tarantool::Box::Record',
);
my $sequence_iproto = My::IProto::XS->new(%{MR::OnlineConf::Generic::IProtoXS::get_iproto_config('/my/system/sequence-box')});
sub init_meta {
my ($class, %args) = @_;
MR::Tarantool::Box::Record->init_meta(%args);
Mouse::Util::MetaRole::apply_metaroles(
for => $args{for_class},
class_metaroles => {
class => ['MR::OnlineConf::Generic::Trait::Class::TarantoolBoxRecord'],
},
);
my $meta = $args{for_class}->meta;
$meta->iproto_class('My::IProto::XS');
$meta->box_class('My::Tarantool::Box::XS');
$meta->sequence_iproto($sequence_iproto);
$meta->sequence_namespace(5);
return $meta;
}
1;
Для того, чтобы упростить написание конфигурации неймспейса и автоматизировать подсчет необходимой под него памяти, в метаклассе существует две вспомогательные функции: print_storage_config
и print_approximate_storage_size
.
Печатает в STDOUT
конфиг неймспейса. Можно скопипастить as is.
$ perl -MMR::mPOPlib -MMy::Video::Item -lwe 'My::Video::Item->meta->print_storage_config()'
# My::Video::Item
object_space[83].enabled = 1
object_space[83].index[0].type = "HASH"
object_space[83].index[0].unique = 1
object_space[83].index[0].key_field[0].fieldno = 0
object_space[83].index[0].key_field[0].type = "NUM64"
object_space[83].index[1].type = "TREE"
object_space[83].index[1].unique = 0
object_space[83].index[1].key_field[0].fieldno = 1
object_space[83].index[1].key_field[0].type = "NUM"
object_space[83].index[2].type = "TREE"
object_space[83].index[2].unique = 0
object_space[83].index[2].key_field[0].fieldno = 2
object_space[83].index[2].key_field[0].type = "NUM"
object_space[83].index[3].type = "TREE"
object_space[83].index[3].unique = 1
object_space[83].index[3].key_field[0].fieldno = 3
object_space[83].index[3].key_field[0].type = "NUM"
object_space[83].index[3].key_field[1].fieldno = 5
object_space[83].index[3].key_field[1].type = "NUM16"
Печатает в STDOUT
примерный размер хранилища. Учитывает размер памяти под хранение туплов, их фрагментацию, индексы и их заполнение. В качестве параметра принимает прогнозируемое количество записей. Если среди полей встречаются строки, то может понадобиться указать для них min_size
, max_size
либо size
. Печатает две чиселки: минимальный размер при размере строк min_size
и максимальный - для строк длиной max_size
. Если максимальный размер не определен, значит для какого-то из строковых полей не указан max_size
.
$ perl -MMR::mPOPlib -MMy::Video::Item -lwe 'My::Video::Item->meta->print_approximate_storage_size(500_000_000)'
98.84 Gb .. 247.85 Gb
В базовом классе объявлены следующие атрибуты:
shard_num
- номер шарда, из которого была получена запись;replica
- была ли запись получена из реплики;readonly
- запрещены ли над записью операции модификации (если справедливоreplica
, то будет справедливо иreadonly
);exists
- запись получена из коробки.
Базовый класс MR::Tarantool::Box::Record
содержит метод select
, который позволяет сделать запрос к кластеру тарантула/октопуса и получить одну или несколько записей параллельно. Применяется примерно так:
$class->select($field => $keys, %opts);
# Получение одной записи по полю id:
my $record = Sample::Record->select(id => 10);
# Несколько записей по полю id:
my $list = Sample::Record->select(id => [10, 20, 30]);
# Если применяется шардинг, то:
my $record = Sample::Record->select(id => 10, shard_num => 7);
# Для выборки из нескольких шардов сразу чуть сложнее (1 и 7 - номера шардов):
my $list = Sample::Record->select(id => { 1 => [10, 20, 30], 7 => [12, 14, 18] });
# сразу же инициализировать атрибут user имеющимися значениями:
my $list = Sample::Record->select(id => [10, 20, 30], objects => { user => { 10 => $u1, 20 => $2, 30 => $3 } });
# или проще, если есть коллекция, умеющая прикидываться хэшом:
my $list = Sample::Record->select(id => [10, 20, 30], objects => { user => $users });
Помимо этого, если указать для индексного поля атрибут selector
, то будет создан соответствующий метод и им можно будет пользоваться аналогично методу select
, но не указывая имя ключа:
has_field id => (
format => 'l',
selector => 'select_by_id',
index => 'primary_id',
primary_key => 1,
);
my $record = Sample::Record->select_by_id(10);
my $list = Sample::Record->select_by_id([10, 20, 30]);
Метод понимает следующие дополнительные опции:
noraise_unavailable
- не сбрасывать исключение, если не удалось получить данные из какого-либо шарда (по умолчанию 0);by_object
- вместо значений полей были переданы соответствующие объекты (по умолчанию 0);objects
- хэш уже готовых объектов дляhas_field_object
(используется для оптимизации, см. ниже);shard_num
- номер шарда;limit
- лимит для запросов по неуникальным индексам (обязателен, если не сконфигуренdefault_limit
соответствующего индекса);- а также передает все прочие параметры в запрос
MR::Tarantool::Box::XS->bulk()
.
В тех случаях, когда необходимо заселектить по индексу, для полей которого есть соответствующие has_field_object
, всегда следует предпочитать селектор, аргументами которого являются объекты (такие селекторы можно объявить в has_field_object
либо использовать обычный селектор с опцией by_object => 1
). Это позволяет автоматически, после выполнения селекта, проставить значения для соответствующих атрибутов, а также может использоваться при выполнении определения необходимого шарда при помощи метода из атрибута индекса shard_by
. В случае, если уже есть готовые объекты для полей не входящих в индекс, но которые хочется прокинуть либо в результат селекта либо в shard_by
, можно воспользоваться опцией objects
. objects
может содержать хэш, ключами в котором являются имена атрибутов, объявленных при помощи has_field_object
, а значениями хэши (либо коллекции, которые оверлоадят %{}
), ключи которых - значения полей, а значения - соответствующие им объекты.
Метод insert
позволяет записать все поля текущего объекта в коробку. Вызывается без параметров.
my $record = Sample::Record->new(
id => 10,
email => '[email protected]',
);
$record->insert();
Метод update
записывает в коробку все изменения, произведенные над объектом в порядке произведения изменений. Фактически, когда для объекта вызывается мутатор (в том числе акцессор записи), выполненное действие ставится в очередь, и при вызове метода update вся эта очередь скидывается как список операторов операции update
.
my $record = Sample::Record->select_by_id(10);
$record->name('Василий');
$record->inc_counter();
$record->update(); # Вот тут выполнится update с операциями: [ [ name => set => 'Василий' ], [ counter => num_add => 1 ] ]
Помимо стандартного модифицирующего акцессора, можно, при помощи атрибута поля mutators
, создать дополнительные мутаторы, при помощи которых можно выполнять разные атомарные операции, например inc и dec. Но тут надо иметь в виду, что:
- значение атрибута в перле хотя и изменяется сразу после вызова мутатора, но при этом может не совпадать с результатом обновления поля в коробке;
- значение в коробке обновится только после вызова метода
update
.
Удаляет запись из коробки по первичному ключу. Не принимает параметров.