Версия:

Практикум / Практические задания на Lua
Практикум / Практические задания на Lua

Практические задания на Lua

Практические задания на Lua

Практические задания по использованию хранимых процедур на языке Lua в работе с Tarantool’ом:

Вставка 1 млн кортежей с помощью хранимой процедуры на языке Lua

Задание по данному практикуму: “Вставьте 1 миллион кортежей. В каждом кортеже должно быть поле, которое соответствует ключу в первичном индексе, в виде постоянно возрастающего числа, а также поле в виде буквенной строки со случайным значением из 10 символов.”

Цель данного упражнения состоит в том, чтобы показать, как выглядят Lua-функции в Tarantool’е. Необходимо будет работать с математической библиотекой Lua, библиотекой для работы со строками интерпретатора Lua, Tarantool-библиотекой box, Tarantool-библиотекой box.tuple, циклами и конкатенацией. Инструкции легко будет выполнять даже тем, кто никогда не использовал раньше Lua или Tarantool. Единственное требование — знание того, как работают другие языки программирования, и изучение первых двух глав данного руководства. Но для лучшего понимания можно следовать по комментариям и ссылкам на руководство по Lua или другим пунктам в данном руководстве по Tarantool’у. А чтобы облегчить изучение, читайте инструкции параллельно с вводом операторов в Tarantool-клиент.

Настройка

Будем использовать Tarantool-песочницу, которую создавали для упражнений раздела «Руководство для начинающих». Таким образом, у нас есть один спейс и числовой ключ первичного индекса, а также экземпляр Tarantool’а, который также выступает в виде клиента.

Разделитель

В более ранних версиях Tarantool’а многострочные функции обрамляются символами-разделителями. Сейчас в них нет необходимости, поэтому в данном практическом задании они использоваться не будут. Однако они все еще поддерживаются. Если вы хотите использовать разделители или используете более раннюю версию Tarantool’а, перед работой проверьте описание синтаксиса для объявления разделителя.

Создание функции, которая возвращает строку

Начнем с создания функции, которая возвращает заданную строку — “Hello world”.

function string_function()
       return "hello world"
     end

Слово «function» (функция) — ключевое слово в языке Lua. Рассмотрим подробно работу с языком Lua. Имя функции — string_function (строковая_функция). В функции есть один исполняемый оператор, return "hello world" (вернуть «hello world»). Строка «hello world» здесь заключена в двойные кавычки, хотя в Lua это не имеет значения, можно использовать одинарные кавычки. Слово «end» означает, что “это конец объявления Lua-функции.” Чтобы проверить работу функции, можем выполнить команду

string_function()

Отправка function-name() (имя-функции) означает команду вызова Lua-функции. В результате возвращаемая функцией строка появится на экране.

Для получения подробной информации о строках в языке Lua, см. Главу 2.4 «Строки» в руководстве по языку Lua. Для получения подробной информации о функциях см. Главу 5 «Функции» в руководстве по языку Lua (chapter 5 «Functions»).

Теперь вывод на экране выглядит следующим образом:

tarantool> function string_funciton()
              >   return "hello world"
              > end
     ---
     ...
     tarantool> string_function()
     ---
     - hello world
     ...
     tarantool>

Создание функции, которая вызывает другую функцию и определяет переменную

Теперь у нас есть функция string_function, и можно вызвать ее с помощью другой функции.

function main_function()
       local string_value
       string_value = string_function()
       return string_value
     end

Сначала объявим переменную «string_value» (значение_строки). Слово «local» (локально) означает, что string_value появится только в main_function (основная_функция). Если бы мы не использовали «local», то string_value увидели бы даже пользователи других клиентов, которые подключились к данному экземпляру! Иногда это может быть очень полезно при взаимодействии клиентов, но не в нашем случае.

Затем определим значение для string_value, а именно, результат функции string_function(). Сейчас вызовем main_function(), чтобы проверить, что значение определено.

Для получения подробной информации о переменных в языке Lua, см. Главу 4.2 «Локальные переменные и блоки» в руководстве по языку Lua (chapter 4.2 «Local Variables and Blocks»).

Теперь вывод на экране выглядит следующим образом:

tarantool> function main_function()
              >   local string_value
              >   string_value = string_function()
              >   return string_value
              > end
     ---
     ...
     tarantool> main_function()
     ---
     - hello world
     ...
     tarantool>

Изменение функции для возврата строки из одной случайной буквы

Сейчас стало понятно, как задавать переменную, поэтому можно изменить функцию string_function() так, чтобы вместо возврата заданной фразы «Hello world», она возвращала случайным образом выбранную букву от „A“ до „Z“.

function string_function()
       local random_number
       local random_string
       random_number = math.random(65, 90)
       random_string = string.char(random_number)
       return random_string
     end

Нет необходимости стирать содержание старой функции string_function(), оно просто перезаписывается. Первый оператор вызывает функцию из математической библиотеки Lua, которая возвращает случайное число; параметры означают, что число должно быть целым от 65 до 90. Второй оператор вызывает функцию из библиотеки Lua для работы со строками, которая преобразует число в символ; параметр представляет собой кодовую точку символа. К счастью, в кодировке ASCII символу „A“ соответствует значение 65, а „Z“ — 90, так что в результате всегда получим букву от A до Z.

Для получения подробной информации о функциях математической библиотеки в языке Lua, см. Практическое задание по математической библиотеке для пользователей Lua (Math Library Tutorial). Для получения подробной информации о функциях библиотеки для работы со строками в языке Lua, см.  Практическое задание по библиотеке для работы со строками для пользователей Lua (String Library Tutorial).

И снова функцию string_function() можно вызвать из main_function(), которую можно вызвать с помощью main_function().

Теперь вывод на экране выглядит следующим образом:

tarantool> function string_function()
              >   local random_number
              >   local random_string
              >   random_number = math.random(65, 90)
              >   random_string = string.char(random_number)
              >   return random_string
              > end
     ---
     ...
     tarantool> main_function()
     ---
     - C
     ...
     tarantool>

… На самом деле, вывод не всегда будет именно таким, поскольку функция math.random() вызывает случайные числа. Но для наглядности случайные значения в строке не важны.

Изменение функции для возврата строки из десяти случайных букв

Сейчас стало понятно, как вызывать строки из одной случайной буквы, поэтому можно перейти к нашей цели — возврату строки из десяти букв с помощью конкатенации десяти строк из одной случайной буквы в цикле.

function string_function()
       local random_number
       local random_string
       random_string = ""
       for x = 1,10,1 do
         random_number = math.random(65, 90)
         random_string = random_string .. string.char(random_number)
       end
       return random_string
     end

Слова «for x = 1,10,1» означают: “начать с x, равного 1, зацикливать до тех пор, пока x не будет равен 10, увеличивать x на 1 на каждом шаге цикла”. Символ «..» означает «конкатенацию», то есть добавление строки справа от знака «..» к строке слева от знака «..». Поскольку в начале определяется, что random_string (случайная_строка) представляет собой «» (пустую строку), в результате получим, что в random_string 10 случайных букв. И снова функцию string_function() можно вызвать из main_function(), которую можно вызвать с помощью main_function().

Для получения подробной информации о циклах в языке Lua, см. Главу 4.3.4 «Числовой оператор for» в руководстве по языку Lua (chapter 4.3.4 «Numeric for»).

Теперь вывод на экране выглядит следующим образом:

tarantool> function string_function()
              >   local random_number
              >   local random_string
              >   random_string = ""
              >   for x = 1,10,1 do
              >     random_number = math.random(65, 90)
              >     random_string = random_string .. string.char(random_number)
              >   end
              >   return random_string
              > end
     ---
     ...
     tarantool> main_function()
     ---
     - 'ZUDJBHKEFM'
     ...
     tarantool>

Составление кортежа из числа и строки

Сейчас стало понятно, как создать строку из 10 случайных букв, поэтому можно создать кортеж, который будет содержать число и строку из 10 случайных букв, с помощью функции в Tarantool-библиотеке Lua-функций.

function main_function()
       local string_value, t
       string_value = string_function()
       t = box.tuple.new({1, string_value})
       return t
     end

После этого, «t» будет представлять собой значение нового кортежа с двумя полями. Первое поле является числовым: «1». Второе поле представляет собой случайную строку. И снова функцию string_function() можно вызвать из main_function(), которую можно вызвать с помощью main_function().

Для получения подробной информации о кортежах в Tarantool’е, см. раздел Вложенный модуль box.tuple руководства по Tarantool’у.

Теперь вывод на экране выглядит следующим образом:

tarantool> function main_function()
              > local string_value, t
              > string_value = string_function()
              > t = box.tuple.new({1, string_value})
              > return t
              > end
     ---
     ...
     tarantool> main_function()
     ---
     - [1, 'PNPZPCOOKA']
     ...
     tarantool>

Изменение основной функции main_function для вставки кортежа в базу данных

Сейчас стало понятно, как создавать кортеж, который содержит число и строку из десяти случайных букв, поэтому осталось только поместить этот кортеж в спейс tester. Следует отметить, что tester — это первый спейс, определенный в песочнице, поэтому он представляет собой таблицу в базе данных.

function main_function()
       local string_value, t
       string_value = string_function()
       t = box.tuple.new({1,string_value})
       box.space.tester:replace(t)
     end

Здесь новая строка — box.space.tester:replace(t). Имя содержит слово „tester“, потому что вставка будет осуществляться в спейс tester. Второй параметр представляет собой значение в кортеже. Для абсолютной точности мы могли ввести команду box.space.tester:insert(t), а не box.space.tester:replace(t), но слово «replace» (заменить) означает “вставить, даже если уже существует кортеж, у которого значение первичного ключа совпадает”, и это облегчит повтор упражнения, даже если песочница не пуста. После того, как это будет выполнено, спейс tester будет содержать кортеж с двумя полями. Первое поле будет 1. Второе поле будет представлять собой строку из десяти случайных букв. И снова функцию string_function() можно вызвать из main_function(), которую можно вызвать с помощью main_function(). Но функция main_function() не может полностью отразить ситуацию, поскольку она не возвращает t, она только размещает t в базе данных. Чтобы убедиться, что произошла вставка, используем SELECT-запрос.

main_function()
     box.space.tester:select{1}

Для получения подробной информации о вызовах insert и replace в Tarantool’е, см. разделы Вложенный модуль box.space, space_object:insert() и space_object:replace() руководства по Tarantool’у.

Теперь вывод на экране выглядит следующим образом:

tarantool> function main_function()
              >   local string_value, t
              >   string_value = string_function()
              >   t = box.tuple.new({1,string_value})
              >   box.space.tester:replace(t)
              > end
     ---
     ...
     tarantool> main_function()
     ---
     ...
     tarantool> box.space.tester:select{1}
     ---
     - - [1, 'EUJYVEECIL']
     ...
     tarantool>

Изменение основной функции main_function для вставки миллиона кортежей в базу данных

Сейчас стало понятно, как вставить кортеж в базу данных, поэтому несложно догадаться, как можно увеличить масштаб: вместо того, чтобы вставлять значение 1 для первичного ключа, вставьте значение переменной от 1 до миллиона в цикле. Поскольку уже рассматривалось, как заводить цикл, это будет несложно. Мы лишь добавим небольшой штрих — функцию распределения во времени.

function main_function()
       local string_value, t
       for i = 1,1000000,1 do
         string_value = string_function()
         t = box.tuple.new({i,string_value})
         box.space.tester:replace(t)
       end
     end
     start_time = os.clock()
     main_function()
     end_time = os.clock()
     'insert done in ' .. end_time - start_time .. ' seconds'

Стандартная Lua-функция os.clock() вернет время ЦП в секундах с момента начала. Таким образом, выводя start_time = number of seconds (время_начала = секунды) прямо перед вставкой, а затем выводя end_time = number of seconds (время_окончания = секунды) сразу после вставки, можно рассчитать (время_окончания - время_начала) = затраченное время в секундах. Отобразим это значение путем ввода в запрос без операторов, что приведет к тому, что Tarantool отправит значение на клиент, который выведет это значение. (Ответ Lua на C-функцию printf(), а именно print(), также сработает.)

Для получения подробной информации о функции os.clock() см. Главу 22.1 «Дата и время» в руководстве по языку Lua (chapter 22.1 «Date and Time»). Для получения подробной информации о функции print() см. Главу 5 «Функции» в руководстве по языку Lua (chapter 5 «Functions»).

И поскольку наступает кульминация — повторно введем окончательные варианты всех необходимых запросов: запрос, который создает string_function(), запрос, который создает main_function(), и запрос, который вызывает main_function().

function string_function()
       local random_number
       local random_string
       random_string = ""
       for x = 1,10,1 do
         random_number = math.random(65, 90)
         random_string = random_string .. string.char(random_number)
       end
       return random_string
     end

     function main_function()
       local string_value, t
       for i = 1,1000000,1 do
         string_value = string_function()
         t = box.tuple.new({i,string_value})
         box.space.tester:replace(t)
       end
     end
     start_time = os.clock()
     main_function()
     end_time = os.clock()
     'insert done in ' .. end_time - start_time .. ' seconds'

Теперь вывод на экране выглядит следующим образом:

tarantool> function string_function()
              >   local random_number
              >   local random_string
              >   random_string = ""
              >   for x = 1,10,1 do
              >     random_number = math.random(65, 90)
              >     random_string = random_string .. string.char(random_number)
              >   end
              >   return random_string
              > end
     ---
     ...
     tarantool> function main_function()
              >   local string_value, t
              >   for i = 1,1000000,1 do
              >     string_value = string_function()
              >     t = box.tuple.new({i,string_value})
              >     box.space.tester:replace(t)
              >   end
              > end
     ---
     ...
     tarantool> start_time = os.clock()
     ---
     ...
     tarantool> main_function()
     ---
     ...
     tarantool> end_time = os.clock()
     ---
     ...
     tarantool> 'insert done in ' .. end_time - start_time .. ' seconds'
     ---
     - insert done in 37.62 seconds
     ...
     tarantool>

Итак, мы доказали, что возможности Lua-функций довольно многообразны (на самом деле, с помощью хранимых процедур на языке Lua в Tarantool’е можно сделать больше, чем с помощью хранимых процедур в некоторых SQL СУБД), и несложно комбинировать функции Lua-библиотек и функции Tarantool-библиотек.

Также мы показали, что вставка миллиона кортежей заняла 37 секунд. Хостом выступил ноутбук с ОС Linux. А изменив значение wal_mode на „none“ перед запуском теста, можно уменьшить затраченное время до 4 секунд.

Подсчет суммы по JSON-полям во всех кортежах

Задание по данному практикуму: “Предположим, что в каждом кортеже есть строка в формате JSON. В каждой строке есть числовое поле формата JSON. Для каждого кортежа необходимо найти значение числового поля и прибавить его к переменной „sum“ (сумма). В конце функция должна вернуть переменную „sum“.” Цель данного упражнения — получить опыт в прочтении и обработке кортежей одновременно.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
json = require('json')
     function sum_json_field(field_name)
       local v, t, sum, field_value, is_valid_json, lua_table
       sum = 0
       for v, t in box.space.tester:pairs() do
         is_valid_json, lua_table = pcall(json.decode, t[2])
         if is_valid_json then
           field_value = lua_table[field_name]
           if type(field_value) == "number" then sum = sum + field_value end
         end
       end
       return sum
     end

СТРОКА 3: ЗАЧЕМ НУЖЕН «LOCAL». Эта строка объявляет все переменные, которые будут использоваться в функции. На самом деле, нет необходимости в начале объявлять все переменные, а в длинной функции лучше объявить переменные прямо перед их использованием. Фактически объявлять переменные вообще необязательно, но необъявленная переменная будет «глобальной». Это представляется нежелательным для всех переменных, объявленных в строке 1, поскольку все они используются только в рамках функции.

СТРОКА 5: ЗАЧЕМ НУЖЕН «PAIRS()». Наша задача — пройти по всем строкам, что можно сделать двумя способами: с помощью box.space.space_object:pairs() или с помощью variable = select(...) с указанием for i, n, 1 do some-function(variable[i]) end. Для данного примера мы предпочли использовать pairs().

СТРОКА 5: НАЧАЛО ОСНОВНОГО ЦИКЛА. Всё внутри цикла «for» будет повторяться до тех пор, пока не кончатся индекс-ключи. На полученный кортеж можно сослаться с помощью переменной t.

СТРОКА 6: ЗАЧЕМ НУЖЕН «PCALL». Если бы мы просто ввели lua_table = json.decode(t[2])), то функция завершила бы работу с ошибкой, обнаружив любое несоответствие в JSON-строке, например отсутствие запятой. Заключив функцию в «pcall» (protected call — защищенный вызов), мы заявляем следующее: хотим перехватывать ошибки такого рода, поэтому в случае ошибки следует просто указать is_valid_json = false, и позднее мы решим, что с этим делать.

СТРОКА 6: ЗНАЧЕНИЕ. Функция json.decode означает декодирование JSON-строки, а параметр t[2] представляет собой ссылку на JSON-строку. Здесь есть заранее заданные значения, а мы предполагаем, что JSON-строка была вставлена во второе поле кортежа. Например, предположим, что кортеж выглядит следующим образом:

field[1]: 444
     field[2]: '{"Hello": "world", "Quantity": 15}'

что означает, что первое поле кортежа, первичное поле, представляет собой число, а второе поле кортежа, JSON-строка, является строкой. Таким образом, значение оператора будет следующим: «декодировать t[2] (второе поле кортежа) как JSON-строку; если обнаружится ошибка, то указать is_valid_json = false; если ошибок нет, указать is_valid_json = true и lua_table = Lua-таблица, в которой находится декодированная строка».

СТРОКА 8. Наконец, мы готовы получить значение JSON-поля из Lua-таблицы, взятое из JSON-строки. Значение в field_name (имя_поля), которое является параметром всей функции, должно представлять собой JSON-поле. Например, в JSON-строке '{"Hello": "world", "Quantity": 15}' есть два JSON-поля: «Hello» и «Quantity». Если вся функция вызывается с помощью sum_json_field("Quantity"), тогда field_value = lua_table[field_name] (значение_поля = Lua_таблица[имя_поля]) по сути аналогично field_value = lua_table["Quantity"] или даже field_value = lua_table.Quantity. Итак, этими тремя способами можно ввести следующую команду: получить значение поля Quantity в Lua-таблице и поместить его в переменную field_value.

СТРОКА 9: ЗАЧЕМ НУЖЕН «IF». Предположим, что JSON-строка не содержит синтаксических ошибок, но JSON-поле не является числовым или вовсе отсутствует. В таком случае выполнение функции прервется при попытке прибавить значение к сумме. Если сначала проверить, type(field_value) == "number" (тип(значение_поля) == «число»), можно избежать прерывания функции. Если вы уверены, что база данных в идеальном состоянии, этот шаг можно пропустить.

И функция готова. Пора протестировать ее. Начинаем с пустой базы данных так же, как с песочницы в упражнения в «Руководстве для начинающих»,

-- если спейс tester остался от предыдущего задания, удалите его
     box.space.tester:drop()
     box.schema.space.create('tester')
     box.space.tester:create_index('primary', {parts = {1, 'unsigned'}})

затем добавим несколько кортежей, где первое поле является числовым, а второе поле представляет собой строку.

box.space.tester:insert{444, '{"Item": "widget", "Quantity": 15}'}
     box.space.tester:insert{445, '{"Item": "widget", "Quantity": 7}'}
     box.space.tester:insert{446, '{"Item": "golf club", "Quantity": "sunshine"}'}
     box.space.tester:insert{447, '{"Item": "waffle iron", "Quantit": 3}'}

Для целей практики здесь допущены ошибки. В «golf club» и «waffle iron» поля Quantity не являются числовыми, поэтому будут игнорироваться. Таким образом, итоговая сумма для полей Quantity в JSON-строках должна быть следующей: 15 + 7 = 22.

Вызовите функцию с помощью sum_json_field("Quantity").

tarantool> sum_json_field("Quantity")
     ---
     - 22
     ...

Сработало. Для дополнительной отработки материала можно убрать заранее заданные значения, добавить проверку потенциально возможного арифметического переполнения при наличии больших значений некоторых полей, а также команду передачи управления при огромном количестве кортежей.