※ 本記事は、Martin Bachによる”Using faker-js/Faker to generate test data respecting referential integrity in Oracle Database 23c“を翻訳したものです。

2024年2月7日


Oracle Databaseには、一連のサンプル・スキーマが付属しています。これを使って、きわめて手軽に現実的なデータモデルを作る方法があります。この記事では、一から作るのではなく、人事管理の表(HRスキーマ)に基づいて作成します。

仮にHR.EMPLOYEES表のサンプル・データを提供するタスクがあなたに割り当てられたとします。EMPLOYEES表は、次のように定義されています:

SQL> info+ employees
TABLE: EMPLOYEES 
     LAST ANALYZED: 
     ROWS         : 
     SAMPLE SIZE  : 
     INMEMORY     :DISABLED 
     COMMENTS     :employees table. References with departments,
                       jobs, job_history tables. Contains a self reference. 

Columns 
NAME             DATA TYPE           NULL  DEFAULT    LOW_VALUE   HIGH_VALUE   NUM_DISTINCT   HISTOGRAM  
*EMPLOYEE_ID     NUMBER(6,0)         No                                                                      
 FIRST_NAME      VARCHAR2(20 BYTE)   Yes                                                                     
 LAST_NAME       VARCHAR2(25 BYTE)   No                                                                      
 EMAIL           VARCHAR2(25 BYTE)   No                                                                      
 PHONE_NUMBER    VARCHAR2(20 BYTE)   Yes                                                                     
 HIRE_DATE       DATE                No                   .....       .....                                  
 JOB_ID          VARCHAR2(10 BYTE)   No                                                                      
 SALARY          NUMBER(8,2)         Yes                                                                     
 COMMISSION_PCT  NUMBER(2,2)         Yes                                                                     
 MANAGER_ID      NUMBER(6,0)         Yes                                                                     
 DEPARTMENT_ID   NUMBER(4,0)         Yes                                                                     

Indexes
INDEX_NAME              UNIQUENESS    STATUS    FUNCIDX_STATUS    COLUMNS                  
_______________________ _____________ _________ _________________ ________________________ 
HR.EMP_JOB_IX           NONUNIQUE     VALID                       JOB_ID                   
HR.EMP_NAME_IX          NONUNIQUE     VALID                       LAST_NAME, FIRST_NAME    
HR.EMP_EMAIL_UK         UNIQUE        VALID                       EMAIL                    
HR.EMP_EMP_ID_PK        UNIQUE        VALID                       EMPLOYEE_ID              
HR.EMP_MANAGER_IX       NONUNIQUE     VALID                       MANAGER_ID               
HR.EMP_DEPARTMENT_IX    NONUNIQUE     VALID                       DEPARTMENT_ID            


References
TABLE_NAME     CONSTRAINT_NAME    DELETE_RULE    STATUS     DEFERRABLE        VALIDATED    GENERATED    
______________ __________________ ______________ __________ _________________ ____________ ____________ 
DEPARTMENTS    DEPT_MGR_FK        NO ACTION      ENABLED    NOT DEFERRABLE    VALIDATED    USER NAME    
EMPLOYEES      EMP_MANAGER_FK     NO ACTION      ENABLED    NOT DEFERRABLE    VALIDATED    USER NAME    
JOB_HISTORY    JHIST_EMP_FK       NO ACTION      ENABLED    NOT DEFERRABLE    VALIDATED    USER NAME

(info+sqlclで使用可能なコマンドです)

EMPLOYEES表は、このようなブログ投稿には最適です。(他の表に対する多くの制約や外部キーがあり、複雑さがちょうど良いです。)

FakerJSにご挨拶

FakerJSは、よく知られたデータ・ジェネレータで、前の記事で紹介されています。あらためて取り上げて、テストデータの作成に使用してみましょう。

ライセンスおよび使用に関する詳細は、プロジェクトのGitHubプロジェクト・サイトを参照してください。この記事では、貴社の法務およびITセキュリティ部門(および他の関係者)が、コードでこのモジュールを使用することが安全であり、ライセンスに準拠していることに同意したことほ前提としています。通常、アプリケーションでサード・パーティ・コードを使用するには、特定のコンプライアンス手続きを完了する必要がありますが、その段取りはこの記事の範囲外です。

この記事では、@faker-js/faker 8.3.1をOracle Database 23cにMultilingual Engine (MLE)モジュールとしてデプロイしたことを前提としています。この方法が不明な場合は、前の記事を参照してください。対応するMLE環境FAKER_ENVを構築したものとして話を進めます。

テスト・データ生成

次のJavaScriptモジュールは、いくつかのカスタム・ルーチンで@faker-js/fakerを拡張し、要求された従業員レコードを生成します。次に示す関数の一部は、以前の記事で紹介したものですが、コードをそのままコピー/ペーストすることで実行時にトラブルが起きないようにします。

randomEmployee()によって生成された従業員レコードは、JSONとして返されます。randomEmployee()の利用者は、データをそのまま列またはコレクションに格納できます。あるいは、json_table()を使用して、JSONをリレーショナル・モデルに変換することもできます。後者の例については、この記事の後半で説明します。

create or replace mle module mle_faker language javascript as
import { simpleFaker, faker } from "fakerjs";

/**
 * Generate a random integer in the interval [minNumber, maxNumber]
 * @param {number} minNumber the lower bound of the interval
 * @param {number} maxNumber the upper bound of the interval
 * @returns number
 */
export function randomInt(minNumber, maxNumber) {

    const options = {
        max: maxNumber,
        min: minNumber
    }

    return simpleFaker.number.int(options);
}

/**
 * Generate a random, alpha-numeric string of a certain case (defaults
 * to mixed case), and a specific length
 * @param {string} casing the string's case, one of upper, lower, mixed
 * @param {*} minLength the string's minimum length
 * @param {*} maxLength the string's maximum length
 * @returns string
 */
export function randomString(casing, minLength, maxLength) {

    const options = {
        length: {
            max: maxLength,
            min: minLength
        }
    };

    switch (casing) {
        case 'upper':
            options.casing = 'upper';
            break;
        case 'lower':
            options.casing = 'lower';
            break;
        case 'mixed':
            options.casing = 'mixed';
            break;
        default:
            options.casing = 'mixed';
            break;
    }

    return simpleFaker.string.alpha(options);
}

/**
 * Generate a random date between in the interval [startDate, stopDate]
 * @param {Date} startDate the earliest possible date
 * @param {Date} stopDate the latest possible date
 * @returns Date
 */
export function randomDate(startDate, stopDate) {

    const options = {
        from: startDate,
        to: stopDate
    };

    return simpleFaker.date.between(options);
}

/**
 * Create a new employee record for insertion into HR.EMPLOYEES. The main
 * goal is to ensure referential integrity is preserved. Helper functions
 * select the relevant details from the schema. Consumers of this function
 * need to either store the record in JSON format, or use json_table() to
 * convert JSON into the relational format.
 * @param {string} gender the employee's gender
 * @returns {JSON} a JSON representation of the new employee
 */
export function randomEmployee(gender) {

    let employee = {};

    const extension = randomInt(1, 9999);

    // employee_id doesn't need to be created here
    employee.firstName = faker.person.firstName(gender);
    employee.lastName = faker.person.lastName();
    employee.email = employee.firstName.substr(0,1) + employee.lastName.toUpperCase();
    employee.phoneNumber = '1.650.555.' + faker.string.numeric(4);
    employee.hireDate = randomDate('2020-01-01T00:00:00.000Z', '2023-12-01T00:00:00.000Z');
    employee.jobId = randomJob();
    employee.salary = faker.number.int(randomSalary(employee.jobId));
    employee.commissionPct = null;
    employee.departmentId = randomDepartment();
    employee.managerId = randomManager(employee.departmentId);

    return employee;
}

/**
 * Get a random job from the HR.JOBS table
 * @returns a random JOB_ID
 */
function randomJob() {

    const result = session.execute(
        `SELECT
            job_id
        FROM
            jobs
        ORDER BY
            dbms_random.random
        FETCH FIRST 1 ROWS ONLY`
    );

    if (result.rows === undefined) {
        throw new Error(`database query went wrong: ${err}`);
    } else {
        return result.rows[0].JOB_ID;
    }
}

/**
 * Get a random department ID from HR.DEPARTMENTS. Makes sure the
 * department has a manager. The department's manager will later
 * be used as the new employee's manager as well.
 * @returns a random DEPARTMENT_ID
 */
function randomDepartment() {

    const result = session.execute(
        `SELECT
            department_id
        FROM
            departments
        WHERE
            manager_id is not null
        ORDER BY
            dbms_random.random
        FETCH FIRST 1 ROWS ONLY`
    );

    if (result.rows === undefined) {
        throw new Error(`database query went wrong: ${err}`);
    } else {
        return result.rows[0].DEPARTMENT_ID;
    }
}

/**
 * Get a specific department's manager from HR.DEPARTMENTS
 * @param {number} departmentId the department ID for which a manager must be found
 * @returns the corresponding MANAGER_ID
 */
function randomManager(departmentId) {

    const result = session.execute(
        `SELECT
            manager_id
        FROM
            departments
        WHERE
            department_id = :1`,
        [ departmentId ]
    );

    if (result.rows === undefined) {
        throw new Error(`database query went wrong: ${err}`);
    } else {
        return result.rows[0].MANAGER_ID;
    }
}

/**
 * Get a random salary in the range defined by HR.JOBS, depending
 * on the input parameter.
 * @param {string} jobId matching HR.JOBS.JOB_ID
 * @returns {JSON} a JSON object to be used in faker.number.int to calculate the appropriate salary
 */
function randomSalary(jobId) {

    const result = session.execute(
        `SELECT
            min_salary as min,
            max_salary as max
        FROM
            jobs
        WHERE
            job_id = :1`,
        [ jobId ]
    );

    if (result.rows === undefined) {
        throw new Error(`database query went wrong: ${err}`);
    } else {
        return {
            min: result.rows[0].MIN,
            max: result.rows[0].MAX
        }
    }
}

コール仕様

コール仕様により、JavaScriptコードがSQLおよびPL/SQLで使用可能になります。次のように作成できます:

create or replace package mle_faker_api as

    function random_number(
        p_min_number number,
        p_max_number number
    ) return number
        as mle module mle_faker
        env faker_env
        signature 'randomInt';

    function random_string(
        p_casing varchar2,
        p_min_length number,
        p_max_length number
    ) return varchar2
        as mle module mle_faker
        env faker_env
        signature 'randomString';

    function random_date(
        p_start_date date,
        p_stop_date date
    ) return date
        as mle module mle_faker
        env faker_env
        signature 'randomDate';

    function random_employee(
        p_gender varchar2
    ) return JSON
        as mle module mle_faker
        env faker_env
        signature 'randomEmployee';

end mle_faker_api;
/

テスト

JavaScriptコードが整い、コール仕様が利用可能になったら、いよいよテストです。

単一のレコードの作成

1人の従業員を作成するとどうなるかを見てみましょう:

SQL> select
  2     mle_faker_api.random_employee('female') employee;

問合せによって次の出力が生成されます:

EMPLOYEE
---------------------------------------------------
{
  "firstName" : "Beatrice",
  "lastName" : "Lynch",
  "email" : "BLYNCH",
  "phoneNumber" : "1.650.555.2607",
  "hireDate" : "2023-01-11T05:42:26.380000+01:00",
  "jobId" : "IT_PROG",
  "salary" : 6458,
  "commissionPct" : null,
  "departmentId" : 90,
  "managerId" : 100
}  

このJSONでは注目すべき点がいくつかあります:

  • 真にランダムなデータ・ジェネレータとは異なり、EMAIL列は表の行と一致します。Eメール・アドレスは、名の頭文字+姓で、すべて大文字で構成されます
  • 電話番号(phoneNumber)は既存の番号と同じ範囲内に表示されます
  • 6458単位の給与(salary)は、HR.JOBSで定義されている範囲にあります。プログラマの給与は4000から10000の通貨単位です
  • 部門90のマネージャが、この従業員のマネージャも務めます

HRサンプル・スキーマのコンテキストで、すべての値が理にかなっているので、良さそうです。

さらにデータを

複数の従業員レコードを作成する必要がある場合はどうなりますか? 大丈夫です! 次の例は、json_table()を使用してリレーショナル・フォーマットに変換された新しい10の従業員レコードを作成する方法を示しています:

SQL> with lots_of_employees as (
  2      select
  3          case when mod(rownum, 2) = 0 then
  4              mle_faker_api.random_employee('female')
  5          else
  6              mle_faker_api.random_employee('male')
  7          end employee
  8      from
  9          dual
 10      connect by level
 11          <= 10
 12  )
 13  select
 14      jt.*
 15  from
 16      lots_of_employees e,
 17      json_table(
 18          e.employee,
 19          '$'
 20          columns (
 21              first_name      varchar2    path '$.firstName',
 22              last_name       varchar2    path '$.lastName',
 23              email           varchar2    path '$.email',
 24              phone_number    varchar2    path '$.phoneNumber',
 25              hire_date       date        path '$.hireDate',
 26              job_id          varchar2    path '$.jobId',
 27              salary          number      path '$.salary',
 28              commission_pct  number      path '$.commissionPct',
 29              manager_id      number      path '$.managerId',
 30              department_id   number      path '$.departmentId'
 31          )
 32*     ) jt;

FIRST_NAME    LAST_NAME          EMAIL               PHONE_NUMBER      HIRE_DATE    JOB_ID        SALARY    COMMISSION_PCT    MANAGER_ID    DEPARTMENT_ID 
_____________ __________________ ___________________ _________________ ____________ __________ _________ _________________ _____________ ________________ 
Marc          Mills              MMILLS              1.650.555.4471    18-JUN-23    PU_MAN         14241                             100               90 
Marcia        Welch              MWELCH              1.650.555.6313    18-MAR-21    PU_MAN         13593                             100               90 
Mack          Quitzon            MQUITZON            1.650.555.7048    20-DEC-22    PR_REP          5533                             200               10 
Leticia       Murphy             LMURPHY             1.650.555.6040    29-AUG-22    AD_ASST         5066                             103               60 
Laurence      Schuppe            LSCHUPPE            1.650.555.8321    02-SEP-20    SA_MAN         19420                             103               60 
Phyllis       Grant              PGRANT              1.650.555.2625    14-MAY-22    AC_MGR          9475                             100               90 
Lowell        Kilback            LKILBACK            1.650.555.2167    20-DEC-21    PR_REP          4751                             121               50 
Mindy         Upton              MUPTON              1.650.555.4995    02-APR-23    ST_MAN          8356                             200               10 
Eddie         Mitchell           EMITCHELL           1.650.555.6041    26-MAR-23    SA_REP         11012                             100               90 
Gertrude      O'Connell-Hoppe    GO'CONNELL-HOPPE    1.650.555.8125    23-JAN-21    AC_MGR          9139                             100               90

この方法で、HR.EMPLOYEES表に新しいレコードを追加するのは非常に簡単です。幸いなことに、EMPLOYEE_IDを心配する必要はありません。シーケンス(EMPLOYEES_SEQ)で、この列へ追加できます:

SQL> insert into employees
  2  with lots_of_employees as (
  3      select
  4          case when mod(rownum, 2) = 0 then
  5              mle_faker_api.random_employee('female')
  6          else
  7              mle_faker_api.random_employee('male')
  8          end employee
  9      from
 10          dual
 11      connect by level
 12          <= 10
 13  )
 14  select
 15      employees_seq.nextval,
 16      jt.*
 17  from
 18      lots_of_employees e,
 19      json_table(
 20          e.employee,
 21          '$'
 22          columns (
 23              first_name      varchar2    path '$.firstName',
 24              last_name       varchar2    path '$.lastName',
 25              email           varchar2    path '$.email',
 26              phone_number    varchar2    path '$.phoneNumber',
 27              hire_date       date        path '$.hireDate',
 28              job_id          varchar2    path '$.jobId',
 29              salary          number      path '$.salary',
 30              commission_pct  number      path '$.commissionPct',
 31              manager_id      number      path '$.managerId',
 32              department_id   number      path '$.departmentId'
 33          )
 34*     ) jt;

10 rows inserted.

まとめ

ランダム・ダミー・データの作成はそれほど難しくありません。データが意味を持ち、主キーと外部キーの定義を考慮する必要がある場合、難易度が多少高くなりますが、前回の記事では、これらの要件を満たすことがそれほど難しくないことを実証できたと思います。