Как сжать несколько значений в одно чтобы сохранить в одном поле таблицы

I. Нормализованная структура.

Представим, что в нашем проекте игроку доступны для покупки разные предметы, например, броня. Все предметы уникальны. Заранее неизвестно конечное число вещей, т.к. есть вероятность их добавления в игру с течением времени.

Вариант 1.

Представим, что в нашей игре есть следующие предметы:

  • шлем
  • корпус
  • броня для ног
  • броня для рук

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

  • userId
  • itemId

в которую добавляем записи в тот момент, когда предмет появляется и удаляем, если предмет теряется.

Вариант 2.

Предположим, что в игру добавили следующие расходные материалы:

  • метательные ножи
  • стрелы для лука
  • зелье восстановления здоровья
  • зелья восстановления маны

У каждого игрока может быть несколько предметов одного вида, т.е. у каждого предмета игрока добавляется количество. Таблица users_items приобретает следующий вид:

  • userId
  • itemId
  • itemAmount

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

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

Есть разные варианты сериализации, рассмотрим несколько.

II. Сериализация битовыми операциями

Этот способ подойдет для первого варианта, описанного выше.
Предположим, что в нашей игре заведен конфиг предметов брони такого вида:

var armor = [
{ id: 1, name: 'head' },
{ id: 2, name: 'body' },
{ id: 3, name: 'legs' },
{ id: 4, name: 'arms' }
];

Id надетых на игрока предметов записываются в массив:

var playerArmor = [2,4];

Что означает, что у игрока есть броня для корпуса и для рук.

Нам необходимо привести данный массив к одному значению, которое можно сохранить в одном поле таблицы БД. Для этого создадим новый класс ConvertIdsByBits:


var ConvertIdsByBits = class {
    constructor(items) {
        this.items = items;
    }

    addId(result, id) {
        return result | (1 << id);
    }

    hasId(value, id) {
        return value & (1 << id);
    }

    encode(items) {
        var result = 0;

        _.each(items, function(id){
            result = this.addId(result, id);
        }, this);

        return result;
    }

    decode(value) {
        var result = [];

        _.each(this.items, function(item){
            if (this.hasId(value, item.id)) {
                result.push(item.id);
            }
        }, this);

        return result;
    }
};

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

Методы класса:

  • addId— возвращает значение, из которого можно получить сохраненный id
  • hasId— проверяет наличие id в зашифрованном значении
  • decode— преобразует массив в число
  • encode— преобразует зашифрованное значение в массив

 

Создаем объект и вызываем методы:

var armorConverter = new ConvertIdsByArmor(armor);
var playerArmorEncoded = armorConverter.encode(playerArmor); //20
var playerArmorDecoded = armorConverter.decode(playerArmorEncoded); //[2, 4]

Минусы такого подхода:

  • можно сохранять только состояния «есть предмет или нет», но не количество предметов
  • нет решения из коробки — нужно разрабатывать собственные методы сериализации/десериализации

III. Сериализация математическими операциями

Данный подход примени ко второму варианту, описанному в начале статьи. Допустим, в игре заведен конфиг расходников такого вида:

var weapon = [
{ id: 1, name: 'bullets' },
{ id: 2, name: 'bombs' },
{ id: 3, name: 'health' }
];

У игрока может находиться сразу несколько однотипных расходников, поэтому в данном случае для хранения данных используем массив объектов:

var playerWeapon = [
{ id: 1, amount: 10 },
{ id: 3, amount: 5 }
];

Создадим новый класс ConvertIdsByMath с аналогичными методами encode  и decode:


var ConvertIdsByMath = class {
    constructor(items) {
        this.items = items;
        this.last = _.max(items, function(item){
            return item.id;
        });
        this.multiplier = 100;
    }
    encode(items) {
        var id, item, amount, coef = 1, result = 0;

        for (id = 1; id <= this.last.id; id++ ) {
            item = _.findWhere(items, {id: id});
            amount = item ? item.amount : 0;
            result += amount * coef;
            coef *= this.multiplier;
        }

        return result;
    }

    decode(value) {
        var id, amount, result = [];

        for (id = 1; id <= this.last.id; id++ ) {
            amount = value % this.multiplier;
            value = (value - amount) / this.multiplier;
            result.push({id: id, amount: amount});
        }

        return result;
    }
};

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

В данном подходе при сериализации:

  1. в цикле проходим подряд все возможные id предметов
  2. определяем для каждого id количество данного предмета у игрока
  3. для генерации итогового значения получаем произведение количеств каждого id на коэффициент
  4. увеличиваем коэффициент при каждой итерации умножая его на мультипликатор

Чтобы обратно получить массив объектов из сгенерированного числа:

  1. для каждого id из конфига получаем остаток от деления сгенерированного значения на 100
  2. в каждой итерации вычитаем получившийся результат из сгенерированного числа и делим на мультипликатор

Минусы:

  • при наличии большого числа разных предметов итоговое значение станет достаточно большим числом, которое может не поместиться в размеры поля в базе
  • все значения количеств предметов будут ограничены максимально возможным значением мультипликатора: в нашем примере — мультипликатор равен 100, это означает, что у нельзя будет сохранить 101 количество для каждого предмета

Создадим новый класс ConvertIds, являющийся фабрикой для созданных ранее классов


var ConvertIds = class {
    constructor(items, type) {
        switch (type) {
            case ConvertIds.MATH:
                this.adapter = new ConvertIdsByMath(items);
                break;
            case ConvertIds.BITS:
                this.adapter = new ConvertIdsByBits(items);
                break;
            default:
                break;
        } 
    }
    encode(items) {
        return this.adapter.encode(items);
    }

    decode(value) {
        return this.adapter.decode(value);
    }
};

ConvertIds.MATH = 1;
ConvertIds.BITS = 2;

Создаем объект и вызываем методы в коде:

var weaponConverter = new ConvertIds(weapon, ConvertIds.MATH);
var playerWeaponEncoded = weaponConverter.encode(playerWeapon); //50010
var playerWeaponDecoded = weaponConverter.decode(playerWeaponEncoded); //{id: 1, amount: 10}1: {id: 2, amount: 0}2: {id: 3, amount: 5}

Полный код класса я выложил на github. Как видно из реализации классов, библиотека требуется подключенного в проект underscore.

III. Обратимое хеширование

Еще один способ заключается в преобразовании массива чисел в хеш. Для этого я использую библиотеку HashIds. На сайте есть реализация для множества языков программирования. Использование максимально простое и аналогично примерам описанным выше:

var hashids = new Hashids();

console.log(hashids.encode(1, 2, 3)); // o2fXhV
console.log(hashids.decode('o2fXhV')); // [1, 2, 3]

Используя hashids вы можете шифровать одно значение или массив чисел. Также можно указать требуемую длину хеша при необходимости.

IV: Другие способы

Есть и другие способы сериализации данных, например стандартные:

  • JSON.stringify и JSON.parse
  • toString для приведения массива в строку и split(',') для обратного преобразования
  • другие возможные способы

Будьте внимательны, преобразуя строку в массив использую split значениями массива будут строки.

Но не рекомендую хранить сериализованные таким образом значения в реляционных базах по соображениям надежности. Возможно эти способы пригодятся для записи значений в localStorage.

 

P.S. Описанные выше способы подходят не только для сохранения массива значений в одно поле таблицы базы данных, но и для шифрования данных, сохраняемых в localStorage. Как мы знаем HTML5 легче поддаются взлому. Игроки могут через утилиты браузера сами открыть localStorage и проверить данные в нем. И если вы будете сохранять в локальное хранилище значения в открытом виде, то игрок без особого труда сможет считерить подменив их. Подменить зашифрованное число намного сложнее, потому что для этого потребуется изучение логики приложения.

Leave a reply:

Your email address will not be published.

Site Footer