9802
СОГЛАСЕН С ОБРАБОТКОЙ ЛИЧНЫХ ДАННЫХ
© Сергей Сычёв, Александр Сычёв, TRIZ-RI Group
ИДЕАЛИЗАЦИЯ СТРУКТУРЫ ДАННЫХ. ИДЕАЛЬНАЯ ФУНКЦИЯ

ТРИЗ-технологии. Приложение ТРИЗ к IT и программированию (TRIZ-RI Skills)

РАЗДЕЛ I. ПРО ДАННЫЕ

В одном из наших проектов есть следующее тестовое задание, которое используется при приёме на работу программистов:

"Есть 3 группы организаций (юридических лиц):

  • наши компании (несколько),
  • некоторое количество фирм Клиентов,
  • несколько компаний наших поставщиков.

Причем иногда одна и та же компания может входить в разные группы (т.е., некоторые Клиенты иногда бывают нашими поставщиками). Есть люди, которые:

  • работают в указанных выше фирмах:
    • в наших компаниях,
    • в фирмах Клиентов,
    • в компаниях наших поставщиков,
  • не принадлежат ни одной компании - физические лица, купившие наш товар.

Постройте модель организации базы данных".

Рассмотрим три уровня ответов: слабый, средний и эталонный (который мы попытаемся построить, используя, в том числе, ТРИЗ-технологии).


Способ первый ("Слабый")

Решатель создал 2 следующие таблицы:

1) Таблица "Firms"

Где поле Firm_id первичный ключ. Тип фирмы задается в текстовом виде.

Тип фирмы задается в текстовом виде

2) Таблица "People"

Где поле People_id – первичный ключ, а поле Firm_id – внешний ключ для связи с таблицей Firms.

Слабый вариант

Очевидно, что данный вариант слаб. Например, здесь нельзя зарегистрировать частное лицо в качестве Клиента, да и многое иное сделать не получится. Как принято говорить: "Структура базы данных не нормализована".

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


Способ второй ("Средний")

Решатель подумал и создал 5 следующих таблиц:

1) Таблица "Agents"

Здесь поле Agent_id является первичным ключом. Количество полей свойств может быть любым (вся необходимая информация: адрес, телефон, форма собственности и т.д.).

2) Таблица "People"

Здесь поле People_id является первичным ключом. Количество полей свойств может быть любым (вся необходимая информация: адрес, телефон, email и т.д.)

3) Таблица "Agent_People"

Здесь оба поля - Agent_id, People_id – являются первичными ключами. При этом поле Agent_id является внешним ключом для связи с таблицей Agents, а поле People_id является внешним ключом для связи с таблицей People.

4) Таблица "Agent_types"

Здесь поле Type_id является первичным ключом.

5) Таблица "Agent_agent_types"

Здесь оба поля – Agent_id,Type_id - являются первичными ключами. При этом, поле Agent_id является внешним ключом для связи с таблицей Agents, а поле Type_id является внешним ключом для связи с таблицей Agent_types.

При таком варианте все сущности ("фирмы", "люди") и их типы описываются отдельно от взаимосвязей, и потому он хорош тем, что, при возникновении неучтенной ситуации, мы просто регистрируем новые данные в справочниках, а не реорганизуем старые и не создаём новых таблиц.

Например, если нам надо зарегистрировать частное лицо - хоть в качестве Клиента, хоть в качестве Поставщика, хоть в качестве собственного сотрудника, - мы лишь регистрируем новый тип агента "Частое лицо" и заполняем соответствующие таблицы, не создавая ничего нового. Тем более, что в данном варианте термин "Firm" разумно заменён на более абстрактный термин "Agent", что позволяет "фирму" того или иного типа считать частным случаем, наряду с иными типами агентов.

Это, разумеется, плюс. И если посмотреть на проект шире, станет понятно, что добавление сущностей данным способом не потребует внесения изменений и в программный код. И это тоже, разумеется, плюс. Как принято говорить: "Структура нормализована".

Во время собеседования разработчику решения был задан вопрос: зачем ему, в таком случае, таблица "People" и соответствующие взаимосвязи. Ведь если частное лицо заведено как агент, то можно добавить, наряду с типами "Наша фирма" и т.д., типы агентов: "Наш сотрудник", "Сотрудник Клиента", "Сотрудник Поставщика" и т.д. И обойтись 2-мя таблицами: "Агенты" и "Типы агентов" - вместо 5-ти.

На что разработчик резонно возразил, что у "людей как людей", у "людей как агентов" и у "фирм как агентов" могут быть разные свойства, которые тоже надо хранить. Предположим, у "Агентов" есть специфическое свойство "Дата последней сделки" или "Форма собственности", а у "Нашего сотрудника" таких свойств нет - эти поля для описания данных о сотруднике нам не требуются. Конечно, в частном случае можно как-то унифицировать свойства разных сущностей, но в общем случае следует исходить из того, что они именно разные.

Сделаем "зарубку" в этом месте и двинемся дальше.

О плюсах решения 2 мы поговорили, теперь о минусах.

Если мы, действуя в логике данного решения, захотим добавить в систему не новый тип уже имеющихся сущностей ("люди", "агенты"), а новую сущность (со своими свойствами), например, "Документы", нам придется создать новую таблицу "Documents":

а также таблицу с типами документов:

Затем создать интерфейсные таблицы, описывающие взаимодействие с людьми и агентами, то есть таблицы:

Число интерфейсных таблиц M для N сущностей вычисляется по формуле:

Соответственно, при добавлении K новых сущностей, число интерфейсных таблиц будет прирастать по формуле:

Так, если изначально в базе было 2 сущности, то при добавлении третьей, появится 2 интерфейсных таблицы, при добавлении еще одной - еще 3, еще одной - еще 4, затем еще 5 и т.д. Итого: n-1.

И это не считая числа таблиц, которые описывают саму сущность. В настоящем примере таких таблиц 2. Так что для этого примера число таблиц в предельном случае составит:

.

И, например, уже при 12 сущностях может быть до 90 таблиц, что радикально усложнит понимаемость и затруднит сопровождение.

Это очевидный минус. Поэтому способ 2 можем назвать "компромиссным" - он по замыслу явно лучше первого, но замысел, очевидно, не доведён до конца. А причиной тому (вернёмся к "зарубке") стало нерешённое противоречие:

  • "Свойства сущностей должны быть разными, чтобы именно с ними можно было работать сообразно их особенностям, и должны быть НЕ-разными, чтобы таблицы в проекте не размножались".

Это противоречие автор способа 2 решать не стал. C этого момента пробуем применить ТРИЗ.


Способ третий. Строим идеальную модель.

Имя, которое может быть названо, не есть истинное имя.
Лао Цзы

Попробуем устранить противоречие, описанное в предыдущем пункте, и создать действительно идеальную структуру для хранения любых данных. Чуть изменим для нашего случая классическую формулировку ТРИЗ и напишем: "Объекта нет, только записи о нём сохраняются".

Но что такое запись об объекте? Строго говоря, в информационной системе никаких фактических объектов и не может быть, там есть только их описания - мы ведь имеем дело с информацией. А что такое "описания объектов"? - Это их "ключи" и "свойства". А имена объектов? Смотри эпиграф. Для информационных систем эту фразу Лао Цзы следует понимать не как метафору, а буквально. Имени объекта в базе нет, есть только ключ и свойства.

Может быть, так и сделаем?

Entities (object’s keys) - Сущности

Properties (of any entities) - Свойства

Собственно, таблица Entities не нужна, поскольку она служит только для целей заведения и распределения уникального ключа Id. Если мы сможем самостоятельно обеспечить генерацию такого ключа, то мы избавимся от этой таблицы и останется только таблица Properties. Завести его несложно, к тому же, во многих существующих СУБД метод создания Id уже и так существует.

Оставим только таблицу Properties.

Properties (of any entities) - Свойства

Этот вариант хорошо обеспечивает взаимоотношение один ко многим. Любые данные можно получить с помощью одного запроса, за счет чего достигается быстрота. Это плюс. Однако, если попадутся сущности с одинаковыми свойствами, в таблице Properties мы получим идентичные записи. Это минус, и существенный.

Попробуем, к примеру, внести документы, которыми мы оформляем сделки с нашими контрагентами. Пусть тип документа (свойство "Document_Type") всегда будет выбираться из некоторого фиксированного набора (пусть это будут "счета", "накладные", "договора", "акты" и т.д.), и пусть поставщики часто будут одними и теми же (свойство "Provider").

Тогда, если мы заключим, предположим, три тысячи сделок, это повлечет за собой создание трех тысяч новых записей, у которых будут идентичными свойства "Document_Type" и "Provider", итого 6000 идентичных записей, и это при условии, что повторяются только два свойства. В общем же случае, если мы добавим в эту таблицу Properties m сущностей с n свойствами, k из которых совпадают (k<=n), это породит m*n новых записей (k*m из которых будут одинаковыми). База очень разрастётся, хотя при этом будет шустро работать.

Поэтому продолжим решение и, следуя общей логике нормализации, превратим все поля таблицы Properties (кроме поля с Id) в отдельные таблицы. То есть создадим таблицы Names и Values. В них будем хранить только уникальные имена и значения свойств соответственно, которые будут "собираться в наборы" свойств в таблице Sets.

Ключи в таблице Sets не уникальны. Они представляют из себя связи номера набора свойств с самими свойствами.

Sets (of propertie’s names and values) - Наборы имён свойств и их значений

Names (of properties) - Имена свойств

Values (of properties) - Значения свойств

Любые данные можно получить с помощью одного запроса, следовательно достигается быстрота. Размножения записей тоже не произойдёт. Если мы добавим m наборов с n именами/значениями свойств, k из которых совпадают (k<=n), это породит n-k новых записей в таблице Names/Values и m*n в Sets. (Если k=n, новых записей в Names/Values не создастся вовсе.)

Таким образом, достигается экономия при добавлении наборов с повторяющимися свойствами, и размер базы будет возрастать намного медленней, поскольку при добавлении повторяющегося имени/свойства увеличивается только таблица Sets.

И тут мы должны сделать необходимую методическую ремарку: как мы видим, теперь в базе не только нет "объектов" и их имён, но и намёков на них ("Нет никакой ложки") - есть только свойства (названия свойств, значения свойств и наборы свойств). Вы можете собрать из этих свойств что угодно. Можете навесить на собранный "набор свойств" "ярлык" (дать соответствующее имя), но Вам, как разработчику, не следует понимать и называть эту "пачку" свойств "объектом", поскольку это влечет за собой инерцию мышления. Не у "ложки (вилки, суслика) есть свойства", а "вот на эту "пачку свойств" мы (если нам взбрело это излишество) повесили ярлык "ложка", на эту - "вилка", на эту - "суслик".

Очевидно, Вы уже обратили внимание, что исходное противоречие: "Свойства разных сущностей должны быть разными, чтобы именно с ними можно было работать сообразно их особенностям, и должны быть НЕ-разными, чтобы таблицы в проекте не размножались" - устранено.

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

Но продолжим свёртывание:

Таблица "Names" содержит 2 столбца: "Id" и "Names". В каждом из столбцов хранятся уникальные значения.

Names (of properties)

Но второй столбец этой таблицы используется только для понимания программистом (не Пользователем, ни даже программой) того, с чем он работает, и ни для чего другого. К тому же, в таблице Names хранится соотношение "уникальный к уникальному". Значит, можно использовать только одно поле.

Поскольку поиск по численным идентификаторам осуществляется быстрее, то можно просто выбросить столбец Names, оставив только столбец Id. (Как сделать, чтобы при этом программист не потерял удобств, написано ниже.)

Тогда, поскольку остаётся только один столбец Id, можно выбросить всю таблицу Names и следить за идентификаторами, которые мы кладем в таблицу Sets в поле Name_id.

Таким образом, в базе остаются 2 таблицы: таблица Values (такая же, как была) и таблица Sets, у которой теперь два основных ключа: Set_id и Name_id, а также ключ Value_id, ссылающийся на соответствующее значение в таблице Values.

Sets (of propertie’s names and values)

Values (of properties)

А таблица Names исчезла (вновь вспомним Лао-Цзы :) ), точнее, стала идеальной. Теперь её нет, но функция её полностью выполняется. Вновь вспомним ТРИЗ.

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

Вернёмся к нашему сквозному примеру и заполним данными нашу универсальнo - специфичную базу:

Sets

Values

Могут возразить, что ситуация стала менее удобной для чтения. Например, мы не видим в поле имён (name_iD) первой таблицы читаемой человеком информации, о каких сущностях идёт речь, а видим только их id (4,3,2,1). Машина всё понимает и быстро работает, а человеку неудобно.

Да и запросы получаются не очень читаемыми, например: get(2,1,null);

(Примечание: здесь и ниже примеры на php, могут быть приведены тождественные примеры на других языках.)

Получается противоречие:

  • "Имя в явном виде надо назвать, чтобы было удобно программисту, и не надо его называть, чтобы машине не приходилось перебирать дополнительную таблицу, да ещё со строковыми данными". Как быть?

Решим противоречие вынесением списка имён за пределы "зоны поиска", ведь именно в этой зоне он становится противоречивым. Определим, какой id соответствует какому читаемому для программиста представлению за пределами базы - на уровне программы.

Для этого за пределами базы заведём справочник (можно в подключаемом файле), в котором хранится список констант вида:

$Document_name = 1;
$Provider_name = 2;
$Agent_name    = 3;
$People_name   = 4;

После этого наши запросы примут читаемый вид (например): get(2,$Document_name,null)

А лишней таблицы в базе всё равно нет, и скорость большая.


Врезка о предшественниках: Здесь нам надо сослаться должным образом на достаточно известную модель организации данных EAV (Entity - Attribute - Value), которая по замыслу практически тождественна описываемой структуре и появилась значительно раньше. Что мы с уважением и делаем.

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

Разница описана выше буквально в предыдущих абзацах: не следует хранить "человеческие описания атрибутов" в таблицах. В последнем "свёрнутом" варианте, который приведён здесь, мы имеем не 3 таблицы, а 2. Таблицу, которая в EAV называется Attri
bute, а здесь названа Names, мы убрали, заменив справочником констант в подключаемом файле, что даёт, как сказано выше, очень значительный выигрыш в скорости без ущерба для удобства программиста.

Другое отличие в реализации тоже заметно: все Values находятся в отдельной таблице, состоящей из 2-х полей ID и Values - то есть, "значения" отделены от "наборов", а не смешаны с ними, что, во-первых, позволяет очень существенно уменьшить размер базы, а, во-вторых, также даёт выигрыш в скорости.

Отдельный разговор про быстродействие есть ниже в разделе 3. Мы надеемся, что, с учётом изложенного в данном материале (выше и в разделе 3), разные утверждения о "медленности" баз с идеологией "Key - Value" уйдут в область "мифов". Правда, при этом, мы бы настаивали именно на той реализации, что описана в настоящей публикации. Обозначим её "EAV as Owl's" (по-Сычёвски).


РАЗДЕЛ 2. ПРО КОД

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

Предположим, нам надо сделать выборку (get). Теперь мы можем писать универсальные операции в таком стиле:

$searches = array();
$names = array (перечисляем здесь, что требуется)
for($names as $index => $name){
      $searches[$index] = get($set_id,$name,$value); // "ген" структуры данных
} 

Например, вот такая операция найдет всех людей, всех агентов, все документы и все товары:

$searches = array();
$names = array($people,$agents,$documents,$goods)
for($names as $index => $name){
       $searches[$index] = get(null,$name,null);
} 

Где null - указатель на пустоту, поскольку переменные $set и $value в этом запросе не используются.

(Примечание: Для упрощения и унификации кода данное правило мы будем стараться применять регулярно: структура $setID,$name,$value внутри соответствующей функции есть всегда, но когда та или иная переменная не нужна, вместо неё пишем "указатель на пустоту").

А если у нас в системе появилось что-либо ещё (например, "Cделки") и нам надо расширить выборку, получив также и "все сделки", нам не потребуется писать никаких дополнительных функций, мы только внесём соответствующие записи в таблицы Values и Sets базы данных (если там таковой сущности раньше не было) и укажем параметр "deals" в функции "get":

$searches = array();
$names = array($people,$agents,$documents,$goods,$deals)
for($names as $index => $name){
      $searches[$index] = get(null,$name,null);
} 

Согласитесь, что при иной структуре данных нам потребовалось бы сделать что-то вроде:

getAllpeople(); 
getAllAgents();
getAllDocuments();
getAllGoods();
getAllDeals();

И это нехорошо.

Но двинемся дальше. Вернёмся к контекстному противоречию: свойства должны быть разными и не должны быть разными. Только рассмотрим его теперь не на уровне структуры данных, а на уровне программного кода.

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

Разумеется, мы не будем писать для каждого свойства своей функции:

$people = getpeople(); 
foreach($people as $people){
echo get_people_name();
echo get_surname();
echo get_sex();
echo get_phone();
echo get_email();
echo get_weight();
echo get_height();
echo get_eye_colour();
}

Но в духе вышеизложенного сначала напишем, например, так:

$people = get(null,$surname,null);//предполагается, что свойство "фамилия" есть только у человека
$properties = array($people_name, $surname, $sex, $phone, $email, $weight, $height, $eye_colour);
foreach($people as $person){
     foreach($properties as $property){
          echo get($person,$property,null);
     }
}

Теперь представим другой контекст и выберем всех "людей как агентов" - пусть нас заинтересуют иные их свойства - не цвет глаз, рост, вес и т.п., а, например, кроме контактных данных, заключённые с нами сделки и/или соответствующие товары и документы.

Мы не пишем новых функций, но лишь меняем параметры в отмеченной строке:

$people = get(null,$surname,null); 
$properties = array ($people_name, $surname, $contracts, $goods, $documents...перечисляем необходимые параметры);
foreach($people as $person){
     foreach($properties as $property){
          echo get($person,$property,null);
     }
}

И понятно, что, если нам постоянно требуются разные "наборы" параметров (люди в разных контекстах: "как люди", "как наши сотрудники", "как агенты", "как сотрудники фирмы поставщика", "как сотрудники фирмы Клиента", "как любовники" и т.д., и т.п.), мы также оставим всего одну функцию:

$people = get(null,$surname,null); 
$properties = array (вот тут перечислим необходимые свойства);
foreach($people as $person){
     foreach($properties as $property){
          echo get($person,$property,null);
     }
}

Мы получили функцию, близкую к идеальной, в которой не указана в явном виде ни одна сущность (соответственно, код её не меняется вовсе при любой реорганизации данных), но, тем не менее, в зависимости от переданных параметров (обращения к функции), она вернёт то, что требуется.

Стандартизируем теперь требование к параметрам "идеальной функции": пусть всегда получает лишь 3 параметра на вход: "Id набора свойств", "имя свойства", "значение свойства", которые представляют собой "копию" ("ген") универсальной структуры данных, описанную выше (Set_id, Name, Value):

  • any_function ($Set_id, $Name, $Value) или просто any($Set_id, $Name, $Value)

где:

  • Set_id - Id набора свойств
  • Name - Имя свойства
  • Value - Значение свойства

Вновь методическое отступление: как видно, данные и код вместе не хранятся, поэтому, хотя (судите сами) абстрагирование проведено достаточно серьёзное, а нормализация настолько сильная, что вообще достигла своего диалектического отрицания, но инкапсуляции в ООП-шном смысле здесь вовсе нет. Мы не соединяем вместе данные и код, с ними работающий, то есть объектов не создаём - как в общем смысле, так и в ООП-шном, в частности. Код (здесь) универсальный и работает с любыми данными, а данные не имеют "личного кода". Мы не делаем "умных объектов", а делаем "тупые наборы данных" и отдельно от них "тупые функции". Разница с ООП очевидна.


Продолжим оптимизацию. В любом хорошем учебнике по проектированию баз данных будет сказано про CRUD (Create,Read,Update,Delete) и что всякая работа с базами данных сводится к четырем операциям: "Создать", "Прочитать", "Обновить", "Удалить".

Реализуем этот набор здесь. То есть, c учётом описанной выше идеализации кода, реализуем "общение" программы с базой данных через универсальный интерфейс ("ядро") - так, чтобы программа (как и положено по всем канонам) не знала ничего об устройстве базы данных, а базе данных было всё равно, какая там программа. Здесь назовём лишь ключевые "ядерные функции" (статья есть статья), а если будут многочисленные запросы на публикацию ядра целиком, то опубликуем отдельно.

Ключевые функции ядра: "Get", "Add", "Delete".

  • Функция "Получить": get ($SetId,$Name,$Value) - Read
  • Функция "Добавить": add ($SetId,$Name,$Value) - Create & Update
  • Функция "Удалить": delete ($SetId,$Name,$Value) - Delete

Рассмотрим их подробнее, поскольку они, выполняя своё предназначение, избавляют программиста как от необходимости знать устройство базы, так и от необходимости знать язык SQL. Во-первых, потому что, в силу вышеизложенного, весь язык SQL выродился здесь в предельно примитивные строки, а во-вторых, "ядерные функции" сами их создают, а ядро само отправляет данные куда надо.

Get ($Set_id, $Name, $Value) // Получает запись из базы данных в зависимости от передаваемых параметров:

1) Get (null, $name, $value) // Возвр. идентификатор наборов (Set_id), имеющих свойство Name со значением Value.

2) Get (null, $name, null)// Возвращает идентификатор наборов (Set_id), имеющих свойство Name.

3) Get ($Set_id, $name, null) // Возвращает значение свойства Name набора (Set) с идентификатором Set_id.

4) Get ($Set_id, null, $value) // Возвр. имя свойства со значением Value у набора (Set) с идентификатором Set_id.

5) Get ($Set_id, null, null) // Возвр. все свойства набора с идентификатором Set_id.

Как сказано выше, функция Get также ответственна за автоматическое создание SQL-запроса к базе данных.

Предположим, необходимо получить некоторое свойство набора 1.

Тогда заполняем функцию Get ( $Set_id, $Name, $Value) так:

get (1, $name, null)

и она сама создаст и сама выполнит вот такие SQL-запросы:

SELECT VALUE_ID FROM SETS WHERE SET_ID = 1 AND NAME_ID = 2;
SELECT VAL FROM VALUES_STRING WHERE VALUE_ID = 3;

Меняем параметры на входе. Например, так:

get (1, null, null) - сама создаст запросы:
SELECT NAME_ID,VALUE_ID FROM SETS WHERE SET_ID = 1;
SELECT VAL FROM VALUES_STRING WHERE VALUE_ID = 1;
SELECT VAL FROM VALUES_STRING WHERE VALUE_ID = 2;
SELECT VAL FROM VALUES_STRING WHERE VALUE_ID = 3;
SELECT VAL FROM VALUES_STRING WHERE VALUE_ID = 4;
SELECT VAL FROM VALUES_INT WHERE VALUE_ID = 6;

Или так:

get (1, null, 23 ) - сама создаст и выполнит:
SELECT VALUE_ID FROM VALUES_INT WHERE VAL = 23;
SELECT NAME_ID FROM SETS WHERE SET_ID = 1 AND VALUE_ID = 6;

Или так:

get (null, $email, "admin@triz-ri.com") - сама создаст и выполнит:
SELECT VALUE_ID FROM VALUES_STRING WHERE VAL = 'admin@triz-ri.com';
SELECT SET_ID FROM SETS WHERE NAME_ID = 3 AND VALUE_ID = 9;

Или так:

get (null, $login, null) - сама создаст и выполнит:
SELECT SET_ID FROM SETS WHERE NAME_ID = 0;

Или так:

get (null,null,32) - сама создаст и выполнит:
SELECT VALUE_ID FROM VALUES_INT WHERE VAL = 32;
SELECT SET_ID FROM SETS WHERE VALUE_ID = 10;

Теперь рассмотрим функцию Add:

Add ($Set_id, $Name, $Value) // Добавляет (обновляет) запись в базе данных в зависимости от передаваемых параметров:


1)
Add (null, $Name, $Value)
// Добавляет новый набор (Set) со свойством Name и значением Value в базу данных

add (null, $login, "admin3") - сама создаст и выполнит:
SELECT VALUE_ID FROM VALUES_STRING WHERE VAL = 'admin3';
UPDATE OR INSERT INTO VALUES_STRING (VALUE_ID, VAL) VALUES (1, 'admin3') matching (VALUE_ID);
INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (1, 0, 1);

2) Add ( $Set_id, $Name, $Value) // Добавляет набору (Set) с идентификатором id свойство Name со значением Value или обновляет его, если такое свойство уже есть.

add (null, $password,"123456")- сама создаст и выполнит:
SELECT VALUE_ID FROM VALUES_STRING WHERE VAL = '777';
UPDATE OR INSERT INTO VALUES_STRING (VALUE_ID, VAL) VALUES (12, '777') matching (VALUE_ID);
SELECT VALUE_ID FROM SETS WHERE SET_ID = 1 AND NAME_ID = 1;
SELECT SET_ID FROM SETS WHERE VALUE_ID = 2;
UPDATE OR INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (1, 1, 12) matching (SET_ID, NAME_ID);

Возможна ситуация:

SELECT VALUE_ID FROM VALUES_STRING WHERE VAL = 'admin6';
UPDATE OR INSERT INTO VALUES_STRING (VALUE_ID, VAL) VALUES (11, 'admin6') matching (VALUE_ID);
SELECT VALUE_ID FROM SETS WHERE SET_ID = 1 AND NAME_ID = 0;
SELECT SET_ID FROM SETS WHERE VALUE_ID = 1;
DELETE FROM VALUES_STRING WHERE VALUE_ID = 1;
UPDATE OR INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (1, 0, 11) matching (SET_ID, NAME_ID);

Здесь функция ADD "подчищает" базу, то есть удаляет значение, которое больше никто не использует.

Можно передать функции ADD сразу несколько имен и значений:

$names = array ($login,$password,$name,$email,$age);
$vals  = array("admin","123456","Петр","admin@triz-ri.com",32)
$obj_id = add(null,$names ,$vals);

Тогда она сама сразу создаст набор (Set) с указаными свойствами:

SELECT VALUE_ID FROM VALUES_STRING WHERE VAL = 'admin';
UPDATE OR INSERT INTO VALUES_STRING (VALUE_ID, VAL) VALUES (7, 'admin') matching (VALUE_ID);
INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (2, 0, 7);

SELECT
VALUE_ID FROM VALUES_STRING WHERE VAL = '123456'; INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (2, 1, 2);
SELECT
VALUE_ID FROM VALUES_STRING WHERE VAL = 'Петр'; UPDATE OR INSERT INTO VALUES_STRING (VALUE_ID, VAL) VALUES (8, 'Петр') matching (VALUE_ID); INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (2, 2, 8);
SELECT
VALUE_ID FROM VALUES_STRING WHERE VAL = 'admin@triz-ri.com'; UPDATE OR INSERT INTO VALUES_STRING (VALUE_ID, VAL) VALUES (9, 'admin@triz-ri.com') matching (VALUE_ID; INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (2, 3, 9); SELECT VALUE_ID FROM VALUES_INT WHERE VAL = 32; UPDATE OR INSERT INTO VALUES_INT (VALUE_ID, VAL) VALUES (10, 32) matching (VALUE_ID); INSERT INTO SETS (SET_ID, NAME_ID, VALUE_ID) VALUES (2, 5, 10);

И рассмотрим функцию Delete:

1) Delete ($Set_id, $Name) // Удаляет свойство Name у набора с идентификатором (Set_id).

delete (1,$login)- сама создаст и выполнит:
SELECT NAME_ID,VALUE_ID FROM SETS WHERE SET_ID = 1 AND NAME_ID = 0;
SELECT SET_ID FROM SETS WHERE VALUE_ID = 11;
DELETE FROM VALUES_STRING WHERE VALUE_ID = 11; (Может отсутствовать, здесь функция Delete "подчищает" 
базу, то есть удаляет значение, которое больше никто не используется) DELETE FROM SETS WHERE SET_ID = 1 AND NAME_ID = 0;

2) Delete ($Set_id, null)// Удаляет набор с соответствующим идентификатором.

delete (1,null)- сама создаст и выполнит:
SELECT NAME_ID,VALUE_ID FROM SETS WHERE SET_ID = 1;
SELECT SET_ID FROM SETS WHERE VALUE_ID = 1;
DELETE FROM VALUES_STRING WHERE VALUE_ID = 1;
DELETE FROM SETS WHERE SET_ID = 1 AND NAME_ID = 0;
 
SELECT SET_ID FROM SETS WHERE VALUE_ID = 2;
DELETE FROM SETS WHERE SET_ID = 1 AND NAME_ID = 1;
 
SELECT SET_ID FROM SETS WHERE VALUE_ID = 3;
DELETE FROM VALUES_STRING WHERE VALUE_ID = 3;
DELETE FROM SETS WHERE SET_ID = 1 AND NAME_ID = 2;
 
SELECT SET_ID FROM SETS WHERE VALUE_ID = 4;
DELETE FROM VALUES_STRING WHERE VALUE_ID = 4;
DELETE FROM SETS WHERE SET_ID = 1 AND NAME_ID = 3;
 
SELECT SET_ID FROM SETS WHERE VALUE_ID = 6;
DELETE FROM VALUES_INT WHERE VALUE_ID = 6;
DELETE FROM SETS WHERE SET_ID = 1 AND NAME_ID = 5;

РАЗДЕЛ 3. ПРО БЫСТРОДЕЙСТВИЕ

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

get ($SetId, $Name, $Value)

Определим как "быстрые" те поисковые операции, у которых параметр Value равен null, независимо от того, известны или неизвестны параметры SetID и Name. Эти операции "быстрые" потому, что сравниваются целые числа (поля Set_ID и Name_ID), а в поле Value мы не ищем.

Определим как "потенциально медленные" те операции, для реализации которых нам нужно провести сопоставление значения Value его идентификатору ValueID в таблице Values, потому что надо сравнивать строки, которых, по условию задачи, пусть будет много. Причём это сопоставление будет медленным лишь тогда, когда тип данных в таблице Value именно строковый (string).

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

Тогда рассмотрим подробнее ситуацию поиска по очень большой таблице Values со строковыми данными. Обычно их как раз и структурируют с тем, чтобы можно было обратиться к таблице с меньшим числом данных. То есть конечным желаемым результатом является меньшее число данных в той таблице, где мы ищем, а не сама структура. То есть нам надо - в идеале - иметь несколько таблиц Values и простое сопровождение, а не типизацию как таковую.

Ну, так и разрежем таблицы Values на несколько. Они простые, во всех таблицах одни и те же 2 поля: Id и Value. Для каждого значения Value автоматически определяется свой уникальный идентификатор, независимо от того, в какой из таблиц Values это значение хранится.

Любое количество таких простых одинаковых и примитивных таблиц совсем не усложняет сопровождения программы. Более того, делает его проще, чем в "стандартном нормализованном случае", поскольку программисту вовсе не надо изучать (понимать, помнить, описывать, объяснять, передавать по наследству и т.д.) структуру базы.

Функцию же диспетчирования (в какую именно из таблиц Values записать очередное значение, равно как и в какой из таблиц Values произвести поиск) поместим в тот же справочник, в котором хранится список имён. То есть, например, сделаем так:

// Список имён:

$Document_name = 1;
$Provider_name = 2;
$Agent_name    = 3;
$People_name   = 4;
 
 

//Диспетчер:

function table_for_name($name){
 
$values1 = "VALUES_1";
$values2 = "VALUES_2"
 
$t1 = array ($Document_name,$Provider_name);
$t2 = array ($Agent_name,$People_name);
 
          if(in_array($name, $t1))      return $values1;
          if(in_array($name, $t2))      return $values2;
 
          return null;
}

Функция диспетчирования до обращения к базе определяет таблицу, в которой надо искать. Поиск по нескольким тысячам записей или по десятку тысяч осуществляется быстро. Таким образом, уже при вводе данных для запроса, ядро само определит ту конкретную таблицу Values, к которой надо обратиться. И поскольку число записей там не будет большим, поиск произойдёт быстро.

При этом Вы можете изменять код Диспетчера, как Вам удобно (если Вы будете резать таблицы по Вашему методу). Код ядра (соответственно, и функций add, get и delete) при этом не изменится.

Каким же образом нарезаются одинаковые таблицы? Если у Вас разные типы данных, то разделите их на разные таблицы Values (на строковые "String Values" и на численные "Integer Values" и, пожалуй, всё, поскольку хранение записей булевского типа в данной структуре представляет собой таблицу "Bool Values", состоящую из 2-х строк "true" и "null"). Таблицы String Values далее разделите по одинаковым же таблицам - отдельно для данных, которые добавляются редко, и отдельно для данных, которые добавляются часто. Строковые же данные, которые добавляются часто, оставьте в отдельной таблице. И лишь когда их будет очень много (многие сотни тысяч или больше), разделите эту таблицу на несколько, по Вашему усмотрению, - в зависимости от задач Вашего проекта.

В общем случае задача выбора метода нарезки одной длинной таблицы на несколько - достаточно типовая. Множество крупных интернет-сервисов не только "режет" таблицы, но и хранит их на разных серверах. Собственно, их структура в этом случае не важна, ибо конкретная таблица разделяется на несколько. А в нашем случае таблица, к тому же, и примитивная. Для общего же случая посмотрите, например, https://msdn.microsoft.com/en-us/library/dn589797.aspx или погуглите по теме "Database Sharding".

Теперь, когда мы увидели, что поисковые операции, а следовательно, функция get выполняются оптимально, рассмотрим, что получается при выполнении оставшихся ядерных функций add и delete (функция get уже рассмотрена выше).

Случаи Add:

add ($SetId, $Name, $Value) - имеем всего 2 актуальных случая:

Случай 1:

add ($SetId, $Name, $Value) - обновляем в существующем Set свойства Name со значением Value.

Здесь функция add проверяет (поисковая операция), есть ли в таблице Values значение Value. Если нет, то создает (операция добавления). Затем она создает запись в таблице Sets, представляющую из себя указатель на это значение (операция добавления).

Также проводится проверка, есть ли другие наборы с таким же свойством (с тем же самым Name и тем же самым Value_id) - это быстрая поисковая операция.

Если других наборов нет, удаляет значение Value из таблицы Values.

Обе операции добавления быстрые. Поисковая же операция рассмотрена выше - таким образом, с учётом изложенного выше, тоже выполняется быстро.

Случай 2:

add (null, $Name, $Value) - создаём новый Set с известным свойством.

В этом втором случае функция add сразу же сгенерирует новый SetId и отработает для него так же, как в случае 1, поскольку теперь SetId известен. То есть второй случай сводится к первому.

Иные ситуации лишены смысла: либо мы добавляем не пойми какое свойство (Name = null), либо добавляем не пойми какое значение (Value = null), что нелепо (и соответствующее ограничение отражено в описании таблицы Sets).

Таким образом, быстродействие функции add высокое.

Случаи Delete:

delete ($id,$name)

Случай первый:

delete ($id,$name) - удаляет свойство с именем Name у набора Set с идентификатором id.

Здесь функция delete проводит поиск набора Set с идентификатором id (поисковая операция). Затем проводит поиск значения Value свойства с именем Name и значением (эта поисковая операция рассмотрена выше, и мы теперь знаем, что она быстрая).

Далее она проверяет, есть ли другие наборы с таким же свойством (с тем же самым Name и тем же самым Value_id) - это быстрая поисковая операция.

Если других наборов нет, удаляет значение Value из таблицы Values, затем удаляет запись из таблицы Sets, хранящую указатель на это свойство. Все эти стандартные операции удаления быстрые.

Случай второй:

delete ($id,null)- удаляет набор (Set) c идентификатором id со всеми его свойствами.

Здесь функция delete проводит поиск всех свойств, связанных с набором (быстрая операция = сравнивается 2 числа), а затем удаляет их так же, как описано в случае 1.

Таким образом, быстродействие функции delete высокое.


Резюме:

Вот это и есть контрольное решение задачи, с которой мы начали эту статью. Надеемся, Вам понравилось.

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

Это решение создавалась из утилитарного стремления к идеалу - как в управленческом, так и в тризовском смысле. Формулировки идеальности (которые соответствуют по духу и замыслу тем, что сформулировал Генрих Альтшуллер, создавая ТРИЗ-технологии, и которые продвигали эту разработку) следующие:

1) Идеально - это когда программу сможет в отсутствие Автора не только сопровождать, но и развивать программист с меньшей квалификацией, чем Автор программы.

2) Идеально - когда добавление новой функциональности в программу не потребует внесения изменений и/или добавлений в программный код (более того, в совершенно идеальной программе увеличение функциональности приводит к сокращению кода); причём всё это без ущерба для любых параметров (например, быстродействия и т.д.).

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

Но об этом, может быть, в других публикациях.


Авторы благодарят Романа Лушова (TRIZ-RI Group) и Серафима Сухенького (NiceCode) за профессиональное сотрудничество в данном проекте, Михаила Куликова (Axiosoft) за профессиональное обсуждение материала, а также Игоря Беспальчука (Custis) за продуктивное обсуждение EAV-моделей.



Материал опубликован на сайте "Открытые бизнес-методики и технологии TRIZ-RI" 21 октября 2015 г.

Контакты:

Сычёв С.В.
sch@triz-ri.com
Сычёв А.С.
admin3@triz-ri.com
skype:
triz-ri

Российская Федерация:
тел./факс: + 7 (499) 322-37-27, + 7 (863) 2-699-123
Чешская Республика:
тел. моб.: + 420 723 394 451, + 420 601 527 171
Яндекс.Метрика