Java オブジェクトのフラット化ストレージ設計

Java オブジェクトのフラット化フィールドデータを保存するためには、以下の表設計を提案します。この設計は、埋め込みオブジェクトや配列を含む階層的なデータを平坦な形式で格納します。パス(Path)モデルを使用し、階層関係を文字列パスに変換し、配列インデックスの管理もサポートします。

表名: java_object_data

カラム名 データ型 必須 デフォルト値 説明
id BIGINT はい 自動生成 主キー、一意な識別子
root_object_id VARCHAR(128) はい - ルートオブジェクトの一意な識別子(ハッシュ値または業務ID)
full_path VARCHAR(1024) はい - **完全パス**(ドット区切り、配列は\[index\]で表す、例:user.addresses\[0\].city
field_name VARCHAR(255) はい - 現在のフィールド名(パスなし、例:city
data_type VARCHAR(255) はい - Javaの完全修飾型名(例:java.lang.String
is_array_item BOOLEAN はい false 配列の要素であるかどうか(trueの場合、配列内)
array_index INT いいえ NULL 配列インデックス(is\_array\_item=trueの場合のみ有効、0始まり)
parent_path VARCHAR(1024) いいえ NULL 親フィールドの完全パス(ルートフィールドの場合NULL)
depth INT はい 0 埋め込み深度(ルート=0、各階層+1)
value TEXT いいえ NULL フィールド値(基本型/Stringの場合のみ格納、複合型はNULL)
is_terminal BOOLEAN はい - 終端ノードであるかどうか(基本型/Stringの場合true)

設計のポイント

  1. パスモデル
  • full_path: フィールドの位置を完全パスで表す(例:user.addresses[0].city)。
  • parent_path: 親ノードの完全パスを記録(ルートの場合NULL)。
  • field_name: 単純なフィールド名を格納(例:city)。
  1. 配列のサポート
  • is_array_item: フィールドが配列の要素であるかどうかをマーク。
  • array_index: 配列のインデックスを格納(例:0)。
  • 配列パスフォーマット:parent_array[index].child_field
  1. 埋め込み深度
  • depth: ルート=0、各階層埋め込み毎に+1(例:ルートフィールドの検索:depth=0)。
  1. 値格納ロジック
  • value: is_terminal=trueの場合のみ格納(基本型/String)。
  • 複合型(オブジェクト、配列)は値を格納せず、子ノードで構造を復元。
  1. ルートオブジェクトの識別
  • root_object_id: 同一Javaオブジェクトの全フィールドで共有され、バッチ操作を容易にする。

データ例

Java クラスの例:

class User {
    String name = "John";
    Address[] addresses = {
        new Address("New York", "5th Ave"),
        new Address("Boston", "Main St")
    };
}

class Address {
    String city;
    String street;
    // コンストラクタ省略
}

フラット化後の格納例:

id root_object_id full_path field_name data_type is_array_item array_index parent_path depth value is_terminal
1 obj-001 name name java.lang.String false NULL NULL 0 "John" true
2 obj-001 addresses addresses com.example.Address[] true NULL NULL 0 NULL false
3 obj-001 addresses[0] [0] com.example.Address true 0 addresses 1 NULL false
4 obj-001 addresses[0].city city java.lang.String false NULL addresses[0] 2 "New York" true
5 obj-001 addresses[0].street street java.lang.String false NULL addresses[0] 2 "5th Ave" true
6 obj-001 addresses[1] [1] com.example.Address true 1 addresses 1 NULL false
7 obj-001 addresses[1].city city java.lang.String false NULL addresses[1] 2 "Boston" true
8 obj-001 addresses[1].street street java.lang.String false NULL addresses[1] 2 "Main St" true

この設計のメリット

  1. フラット化格納: 階層構造を線形データに変換し、多表結合を回避。
  2. 効率的な検索:
  • パス検索: WHERE full_path = 'addresses[0].city'
  • 親パスの子ノード検索: WHERE parent_path = 'addresses[0]'
  • 深度検索: WHERE depth = 2
  1. 柔軟な拡張性: 任意階層のオブジェクト/配列埋め込みに対応。
  2. 容易な復元: parent_pathdepthを使用し、オブジェクトツリーを再構築。

インデックスの提案: root_object_id, full_path, parent_path, depthにインデックスを付けることで、検索パフォーマンスを向上させます。

以下のJavaメソッドは、任意のオブジェクトを上記表設計に準拠したフラット化構造のリストに変換します。このメソッドは、埋め込みオブジェクトや配列を再帰的に処理し、パス、親パス、深度情報を追跡します。

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

public class ObjectFlattener {

    // フィールドノードの定義
    public static class FieldNode {
        private String rootObjectId;
        private String fullPath;
        private String fieldName;
        private String dataType;
        private boolean isArrayItem;
        private Integer arrayIndex;
        private String parentPath;
        private int depth;
        private String value;
        private boolean isTerminal;

        public FieldNode(String rootObjectId, String fullPath, String fieldName, 
                         String dataType, boolean isArrayItem, Integer arrayIndex, 
                         String parentPath, int depth, String value, boolean isTerminal) {
            this.rootObjectId = rootObjectId;
            this.fullPath = fullPath;
            this.fieldName = fieldName;
            this.dataType = dataType;
            this.isArrayItem = isArrayItem;
            this.arrayIndex = arrayIndex;
            this.parentPath = parentPath;
            this.depth = depth;
            this.value = value;
            this.isTerminal = isTerminal;
        }

        @Override
        public String toString() {
            return String.format("FieldNode{fullPath='%s', type=%s, value=%s}", 
                                fullPath, dataType, value);
        }
    }

    /**
     * Java オブジェクトをフラット化したフィールドノードリストに変換します。
     * 
     * @param obj 変換対象のJava オブジェクト
     * @param rootObjectId ルートオブジェクトID
     * @return フラット化したフィールドノードリスト
     */
    public static List<FieldNode> flattenObject(Object obj, String rootObjectId) {
        List<FieldNode> result = new ArrayList<>();
        // IdentityHashMapを使用して循環参照を管理
        flatten("", obj, rootObjectId, null, 0, result, new IdentityHashMap<>());
        return result;
    }

    private static void flatten(String path, Object obj, String rootObjectId, 
                               String parentPath, int depth, 
                               List<FieldNode> result, 
                               Map<Object, Boolean> visited) {
        if (obj == null) return;

        // 循環参照の検出
        if (visited.containsKey(obj)) return;
        visited.put(obj, true);

        Class<?> clazz = obj.getClass();

        // 配列の処理
        if (clazz.isArray()) {
            handleArray(path, obj, rootObjectId, parentPath, depth, result, visited);
            return;
        }

        // 終端ノード(基本型、String)の処理
        if (isTerminalType(clazz)) {
            handleTerminalNode(path, obj, rootObjectId, parentPath, depth, result, 
                               extractFieldName(path), false, null);
            return;
        }

        // 終端ではないフィールドの処理
        if (!path.isEmpty()) {
            String fieldName = extractFieldName(path);
            FieldNode node = new FieldNode(
                rootObjectId, path, fieldName, 
                clazz.getName(), false, null, 
                parentPath, depth, null, false
            );
            result.add(node);
        }

        // 全てのフィールドを再帰的に処理
        for (Field field : getAllFields(clazz)) {
            field.setAccessible(true);
            try {
                Object fieldValue = field.get(obj);
                String newField = field.getName();
                String newParent = path.isEmpty() ? null : path;
                String newPath = path.isEmpty() ? newField : path + "." + newField;

                flatten(newPath, fieldValue, rootObjectId, newParent, 
                       depth + 1, result, new IdentityHashMap<>(visited));
            } catch (IllegalAccessException e) {
                System.err.println("フィールドアクセスエラー: " + field.getName());
            }
        }
    }

    private static void handleArray(String path, Object array, String rootObjectId, 
                                   String parentPath, int depth, 
                                   List<FieldNode> result, 
                                   Map<Object, Boolean> visited) {
        Class<?> componentType = array.getClass().getComponentType();
        int length = Array.getLength(array);

        // 配列自体のノードを作成
        FieldNode arrayNode = new FieldNode(
            rootObjectId, path, extractFieldName(path), 
            array.getClass().getName(), false, null, 
            parentPath, depth, null, false
        );
        result.add(arrayNode);

        // 配列要素を処理
        for (int i = 0; i < length; i++) {
            Object element = Array.get(array, i);
            String elementPath = path + "[" + i + "]";
            String elementParent = path;
            
            // 配列要素ノードを作成
            FieldNode elementNode = new FieldNode(
                rootObjectId, elementPath, "[" + i + "]", 
                element != null ? element.getClass().getName() : "null", 
                true, i, elementParent, depth + 1, null, false
            );
            result.add(elementNode);

            // 配列要素を再帰的に処理
            flatten(elementPath, element, rootObjectId, elementPath, 
                   depth + 1, result, new IdentityHashMap<>(visited));
        }
    }

    private static void handleTerminalNode(String path, Object value, String rootObjectId, 
                                         String parentPath, int depth, 
                                         List<FieldNode> result, 
                                         String fieldName, 
                                         boolean isArrayItem, 
                                         Integer arrayIndex) {
        String strValue = valueToString(value);
        FieldNode node = new FieldNode(
            rootObjectId, path, fieldName, 
            value.getClass().getName(), isArrayItem, arrayIndex, 
            parentPath, depth, strValue, true
        );
        result.add(node);
    }

    // クラスとその親クラスの全フィールドを取得
    private static List<Field> getAllFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        while (clazz != null && clazz != Object.class) {
            for (Field field : clazz.getDeclaredFields()) {
                if (!field.isSynthetic()) { // シンセティックフィールドを無視
                    fields.add(field);
                }
            }
            clazz = clazz.getSuperclass();
        }
        return fields;
    }

    // 型が終端型かどうかを判断
    private static boolean isTerminalType(Class<?> clazz) {
        return clazz.isPrimitive() || 
               clazz.equals(String.class) ||
               clazz.equals(Integer.class) || 
               clazz.equals(Long.class) ||
               clazz.equals(Double.class) || 
               clazz.equals(Float.class) ||
               clazz.equals(Boolean.class) || 
               clazz.equals(Character.class) ||
               clazz.equals(Byte.class) || 
               clazz.equals(Short.class) ||
               clazz.equals(Void.class);
    }

    // パスからフィールド名を抽出
    private static String extractFieldName(String path) {
        if (path == null || path.isEmpty()) return "";
        
        // 配列インデックスの処理
        if (path.endsWith("]")) {
            int start = path.lastIndexOf('[');
            if (start != -1) {
                return path.substring(start);
            }
        }
        
        // 普通のフィールド名抽出
        int lastDot = path.lastIndexOf('.');
        return (lastDot != -1) ? path.substring(lastDot + 1) : path;
    }

    // 値を文字列に変換
    private static String valueToString(Object value) {
        if (value == null) return "null";
        
        if (value.getClass().isArray()) {
            Class<?> componentType = value.getClass().getComponentType();
            if (componentType.isPrimitive()) {
                int len = Array.getLength(value);
                StringBuilder sb = new StringBuilder("[");
                for (int i = 0; i < len; i++) {
                    if (i > 0) sb.append(", ");
                    sb.append(Array.get(value, i));
                }
                sb.append("]");
                return sb.toString();
            }
            return "Array@" + Integer.toHexString(value.hashCode());
        }
        
        return value.toString();
    }

    public static void main(String[] args) {
        // テスト例
        class Address {
            String city;
            String street;
            
            Address(String city, String street) {
                this.city = city;
                this.street = street;
            }
        }
        
        class User {
            String name = "John Doe";
            int age = 30;
            Address[] addresses = {
                new Address("New York", "5th Ave"),
                new Address("Boston", "Main St")
            };
        }
        
        User user = new User();
        List<FieldNode> nodes = flattenObject(user, "user-001");
        
        // 結果を出力
        nodes.forEach(System.out::println);
    }
}

功能の説明

  1. 階層構造の再帰処理:
  • 深さ優先探索(DFS)を用いてオブジェクトの全フィールドを探索
  • 埋め込みオブジェクトや配列要素を再帰的に処理
  1. パス管理:
  • full_path:ドット区切りのフィールドパス(例:addresses[0].city
  • parent_path:親ノードのパスを記録
  • depth:埋め込み深度を追跡
  1. 特殊型の処理:
  • 配列: 各配列要素にインデックス付きパスを付ける(例:[0]
  • 終端ノード: 基本型、Stringは値を格納
  • 循環参照: IdentityHashMapで既存オブジェクトを検出
  1. 値の変換:
  • 基本型、Stringは直接文字列に変換
  • 配列はハッシュ値を表示(無限再帰を防ぐ)
  • nullは文字列"null"として格納
  1. フィールド取得:
  • クラスとその親クラスの全フィールドを取得(プライベートフィールド含む)
  • シンセティックフィールドを無視

注意事項

  1. 循環参照の管理:
  • IdentityHashMapで既存オブジェクトを追跡
  • 既存オブジェクトを検出したら再帰を停止
  1. パフォーマンス:
  • 反射操作は遅いので、高頻度な場面には適さない
  • 大規模な埋め込み構造は最適化が必要
  1. 型のサポート:
  • 基本型、包装型、Stringに対応
  • 集合型(List、Set等)は別途処理が必要
  1. セキュリティ制限:
  • プライベートフィールドのアクセスにはfield.setAccessible(true)が必要
  • セキュリティマネージャー環境では追加権限が必要

タグ: Java オブジェクトフラット化 データベース設計 反射

6月8日 23:07 投稿