Первинний ключ у UB (генерування значень первинних ключів) #

Теоретична частина #

Щоб зрозуміти поняття «первинний ключ», рекомендуємо ознайомитися зі статтею у Вікіпедії.

У платформі використовуються сурогатні первинні ключі, що дає змогу скористатися такими їхніми перевагами, як:

  • Незмінність
  • Гарантована унікальність
  • Гнучкість
  • Ефективність

Водночас усі недоліки сурогатних ключів усуває сервер застосунків платформи.

Неявний атрибут ID #

Кожна сутність платформи містить атрибут ID типу BigInt. Якщо атрибут не описаний явно в метаданих сутності (це рекомендується), платформа додасть його самостійно. Назву поля в БД можна змінити з ID на іншу, використовуючи мапінг на рівні атрибута — у такому разі атрибут ID має бути описаний. У прикладі нижче показано опис атрибута ID із мапінгом на поле БД C_ID:

"attributes: {
  "ID": {
    "caption": "Ідентифікатор запису",
    "dataType": "BigInt",
    "allowNull": false,
    "mapping": {
        "AnsiSql": { "expression": "C_ID" }
    }
  },.....

Генерування ідентифікаторів #

У платформі реалізовано наскрізний механізм генерування значень первинних ключів — для ВСІХ таблиць бази даних створюється ОДНА послідовність, її значення використовується як значення поля ID для всіх сутностей (для всіх нових записів у таблицях). Тобто навіть у різних таблицях значення ідентифікаторів не повторюються, що відкриває низку корисних можливостей, наприклад:

  • [міксин unity]{@link tutorial unity} може зберігати в батьківській таблиці значення з різних сутностей,
  • стовпець entityinfo_id сутності аудиту uba_auditTrail може містити ідентифікатори різних сутностей тощо.

Крім того, платформа дає змогу використовувати ідентифікатори, що не перетинаються між різними інсталяціями, завдяки чому досить просто організувати реплікацію даних між різними клієнтами. Щоб забезпечити унікальність ідентифікаторів для кожної інсталяції, потрібно вказати унікальний CLIENT_NUM у командному файлі createNewApp.cmd під час розгортання застосунку. Навіть якщо ви не плануєте використовувати реплікацію, призначення унікального номера кожному клієнту дасть змогу легко визначити за значеннями поля ID, чиї дані ви переглядаєте.

Щоб зменшити навантаження на СУБД, платформа використовує кеш значень ідентифікаторів; для цього весь діапазон ідентифікаторів умовно поділено на дві частини. Припустімо, ми генеруємо БД для клієнта № X=7. Система створить дві послідовності (SEQUENCE), що не перетинаються: SEQ_UBMAIN, з якої можна отримати одразу 100 ідентифікаторів, та SEQ_UBMAIN_BY1 — для отримання ідентифікаторів по одному (приклад для Oracle):

CREATE SEQUENCE SEQ_UBMAIN     START WITH   70000000000 MAXVALUE   74999999999 MINVALUE   70000000000 NOCYCLE CACHE 10 ORDER;
CREATE SEQUENCE SEQ_UBMAIN_BY1 START WITH 7500000000000 MAXVALUE 7999999999999 MINVALUE 7500000000000 NOCYCLE ORDER'; 

Під час виклику методу {@link TubDataStore#generateID generateID} один раз на 100 викликів відбуватиметься звернення до БД: val=select SEQ_UBMAIN.nextVal from dual, а 99 значень буде отримано за формулою ID = val * 100 + i, де i — номер виклику. Важливо: не використовуйте послідовність SEQ_UBMAIN для генерування окремих ID.

Послідовність SEQ_UBMAIN_BY1 призначена для генерування великої кількості окремих ідентифікаторів під час безпосередніх вставок у БД. Рекомендується використовувати її лише у виняткових випадках, наприклад під час вставки 1000 і більше записів — приклад тут.

Використання сторонніх правил генерування ID #

Іноді під час мапінгу на наявну схему БД потрібно підтримати сторонні правила формування ідентифікаторів. Часто це окрема послідовність для кожної таблиці, але можливі й складніші варіанти. Для цього у властивості мапінгу pkGenerator можна задати правила генерування ID.

Мапінг pkGenerator на послідовність #

Приклад мапінгу на послідовність: у цьому разі в БД у тій самій схемі, що й сутність, має бути послідовність з іменем, зазначеним у pkGenerator. За такого мапінгу генератор DDL може створити послідовність:

{
    "caption": "ID із мапінгом",
    "description": "Тест для ID із мапінгом [UB-1219]",
    "connectionName": "",
    "descriptionAttribute": "code",
    "documentation": "",
    "attributes": {
        "ID": {
            "caption": "Ідентифікатор запису",
            "dataType": "BigInt",
            "allowNull": false,
            "mapping": {
                "AnsiSql": { "expression": "C_ID" }
            }
        },
        "code": {
            "dataType": "String",
            "size": 32,
            "caption": "Код",
            "description": "Внутрішній код",
            "allowNull": false,
            "mapping": [
                {"name": "AnsiSQL", "expressionType": "Field", "expression": "C_CODE" }
            ]
        }
    },
    "mapping": {
        "AnsiSql": { 
            "pkGenerator": "SEQ_UBMAIN_BY1" 
        }
    },
    "mixins": {
        "mStorage": { "simpleAudit":false, "safeDelete":false }
    }
}

Мапінг pkGenerator на довільний вираз #

Якщо в цільовій БД ID генеруються НЕ за допомогою послідовностей, можна налаштувати мапінг на вираз. У такому разі останній вираз у блоці має бути запитом на вибірку (обов'язково містити ключове слово SELECT), що повертає один запис з одним полем. Приклад для MSSQL, коли для генерування ID використовується поле типу identity:

// У SQL Server:
CREATE TABLE MD_IDGEN(
  ID_column INT IDENTITY PRIMARY KEY,
  ...
);

У файлі метаданих:

"mapping": {
    "MSSQL": { 
        "pkGenerator": "insert into MD_IDGEN(ID_column) values(0); select IDENT_CURRENT('MD_IDGEN')" 
    }
},   

Зазначення з'єднання для pkGenerator #

Починаючи з версії сервера 5.26.12 (2026-09-14) pkGenerator можна задати в форматі [statement]@connection щоб явно вказати з'єднання в якому виконувати statement:

  • прив'язка до sequence в іншому з'єднанні (з'єднання повинно бути оголошене в ubConfig.application.connections)
"mapping": {
  "AnsiSql": {
    "pkGenerator": "SEQ_UBMAIN_BY1@otherConnection"
  }
}
  • прив'язка до основного генератора зі з'єднання main, навіть коли сутність знаходиться в іншій БД
"mapping": {
  "AnsiSql": {
    "pkGenerator": "@main"
  }
}

Практичне використання ідентифікаторів #

Розгляньмо окремо приклади для клієнта й сервера.

Робота з ID на клієнті #

Клієнт (наприклад, веббраузер) ніколи не запускає генерування ID безпосередньо. Він або викликає метод addnew, щоб отримати «порожній» запис із заповненими типовими значеннями полів, зокрема ID:

const result = await $App.connection.query({
    entity: 'tst_IDMapping', 
    method: 'addnew', 
    fieldList: ['ID', 'code']
}) 
const objArray = UB.LocalDataStore.selectResultToArrayOfObjects(result) // перетворити результат із масиву масивів на масив об'єктів 
console.log(objArray) // [{ID: newID: int64, code: null}]

а потім викликає insert з ідентифікатором, отриманим на етапі addnew:

const inserted = await $App.connection.query({
  entity: 'tst_IDMapping', 
  method: 'insert', 
  fieldList: ['ID', 'code'],
  execParams: {ID: idFromPrevRequest, code: 'codeToInsert'} 
})

або викликає метод insert без передавання параметра ID — у такому разі сервер сам згенерує ідентифікатор:

const result = $App.connection.insert({
   entity: 'tst_IDMapping',
   fieldList: ['ID', 'code'],
   execParams: {code: 'codeToInsert'} 
}) 
const objArray = UB.LocalDataStore.selectResultToArrayOfObjects(result);
console.log(objArray) // [{ID: newID: int64, code: 'codeToInsert'}] 

Робота з ID на сервері #

Під час написання серверної логіки ID генерується або автоматично під час виклику addNew чи insert без параметра ID:

var store = new TubDataStore('tst_IDMapping')
store.run('insert', {
  execParams: { // ID не передано, тому його буде згенеровано під час вставки
    code: 'aaa'
  }
})

або за допомогою виклику методу сховища {@link TubDataStore#generateID}:

var store = new TubDataStore('tst_IDMapping')
var newID = store.generateID();
store.run('insert', {
  execParams: { 
    ID: newID, // ID передано — використати його під час вставки
    code: 'aaa'
  }
})