Как работать с иерархическими данными в Java и SQL?

Ответ

В Java иерархические структуры (деревья) обычно представляют с помощью рекурсивных классов.

class TreeNode<T> {
    T data;
    List<TreeNode<T>> children = new ArrayList<>();

    public TreeNode(T data) {
        this.data = data;
    }

    public void addChild(TreeNode<T> child) {
        children.add(child);
    }

    // Рекурсивный обход дерева (DFS)
    public void traverse() {
        System.out.println(data);
        for (TreeNode<T> child : children) {
            child.traverse();
        }
    }
}

// Использование
TreeNode<String> root = new TreeNode<>("Root");
root.addChild(new TreeNode<>("Child 1"));
root.traverse();

В SQL существует несколько паттернов для хранения иерархий:

  1. Adjacency List (Список смежности): Каждая запись хранит ссылку на родителя (parent_id). Для выборки поддерева требуются рекурсивные запросы (WITH RECURSIVE в PostgreSQL, CONNECT BY в Oracle).
  2. Nested Sets (Вложенные множества): Каждый узел имеет границы left и right. Позволяет быстро выбирать поддеревья без рекурсии, но сложен в модификации.
  3. Materialized Path (Материализованный путь): Путь от корня хранится в виде строки (например, 1.5.12). Легко читается и фильтруется по LIKE.

В JPA/Hibernate для Adjacency List используются аннотации @ManyToOne и @OneToMany.

@Entity
class Category {
    @Id
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private List<Category> children;
}

Ответ 18+ 🔞

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

Смотри, в Java всё просто, как три копейки. Берёшь класс, суёшь в него список его же детей — и поехали, рекурсия, мать её.

class TreeNode<T> {
    T data;
    List<TreeNode<T>> children = new ArrayList<>();

    public TreeNode(T data) {
        this.data = data;
    }

    public void addChild(TreeNode<T> child) {
        children.add(child);
    }

    // Рекурсивный обход дерева (DFS)
    public void traverse() {
        System.out.println(data);
        for (TreeNode<T> child : children) {
            child.traverse();
        }
    }
}

// Использование
TreeNode<String> root = new TreeNode<>("Root");
root.addChild(new TreeNode<>("Child 1"));
root.traverse();

Вот и вся магия. Создал корень, прилепил к нему дитёнышей, а потом traverse пойдёт по всем веткам, как ёбанный алкоголик по всем барам в районе, пока не упрётся в тупик. Красота, да?

А вот в SQL, ёпта, начинается настоящий цирк с конями. Тут не один, а целых три способа устроить эту иерархическую пьянку.

  1. Adjacency List (Список смежности). Это как в паспорте графа "отец" — каждая запись тычет пальцем на своего родителя через parent_id. Просто? Ага, пока не надо выбрать всех внуков и правнуков. Тут уже без рекурсивных запросов (WITH RECURSIVE или CONNECT BY) — нихуя не выйдет. Компьютер будет думать, как философ на сходняке, пока всё не вычислит.
  2. Nested Sets (Вложенные множества). Тут уже каждому узлу дают два номера, left и right, как будто это места в кинотеатре. Выбрать целое поддерево — раз плюнуть, один запрос. Но вот добавить нового "зрителя" или пересадить — это пиздец, придётся половину зала сдвигать. Оптимизация для чтения, а не для жизни, блядь.
  3. Materialized Path (Материализованный путь). Самый наглядный, для распиздяев. Путь от корня до узла просто пишется в строку, типа 1.5.12. Хочешь найти всех потомков узла 5? WHERE path LIKE '1.5.%' и готово. Читается легко, как детская книжка, но строки эти могут раздуться, как жопа после новогодних праздников.

Ну и куда же без JPA/Hibernate, эти ребята любят Adjacency List, потому что он похож на обычные связи между сущностями. Смотри, как просто:

@Entity
class Category {
    @Id
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Category parent; // Ты чей, падла?

    @OneToMany(mappedBy = "parent")
    private List<Category> children; // А вот моя орава!
}

Создал сущность, на parent повесил @ManyToOne, а на children@OneToMany. Hibernate сам всё свяжет, как умеет. Главное — не запутаться, кто кого породил, а то получится, как в той поговорке: "Иван, не помнящий родства, а также своих LAZY-коллекций", которые потом выстрелят LazyInitializationException в самый неподходящий момент. Вот тогда и будешь орать "Муму!", как тот самый Герасим, блядь.