JPA и связи между объектами

Классы в Java могут не только наследоваться друг от друга, но и включать в себя другие классы или даже коллекции классов в качестве полей. Мы уже знаем, что в столбцах таблиц, за некоторыми исключениями, нельзя хранить сложные составные типы и коллекции таких типов, что не позволяет сохранять весь подобный объект в одну таблицу. Зато можно сохранять каждый класс в свою собственную таблицу и сохранять связи между ними.

@OneToOne

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

@Entity
public class Person extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String firstName;


    @Getter
    @Setter
    private String lastName;


    @Getter
    @Setter
    private LocalDate dob;


    @Getter
    @Setter
    @OneToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "PASSPORT_ID")
    private Passport passport;
}
@Entity
public class Passport extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String series;


    @Getter
    @Setter
    private String no;


    @Getter
    @Setter
    private LocalDate issueDate;


    @Getter
    @Setter
    private Period validity;


    @Getter
    @Setter
    @OneToOne(optional = false, mappedBy = "passport")
    private Person owner;
}

Оба класса наследованы от @MappedSuperclass, который определяет суррогатный первичный ключ.

В каждой связи есть понятие владелец и владеемый. В примере выше класс Person владеет классом Passport. Для связи один к одному в обоих классах к полю добавляется аннотация @OneToOne, параметр optional которой говорит JPA, является ли значение в этом поле обязательным или нет.

Со стороны владельца к аннотации @OneToOne добавляется так же параметр cascade,  который говорит JPA, что делать с владеемыми объектами при операциях над владельцем.

Про каскадирование стоит рассказать подробнее. Предположим, что никакого каскадирования в JPA нет и никогда не было. В это случае, если мы хотим удалить гражданина из нашей базы данных, мы должны помнить, что у него есть паспорт и должны вначале вручную удалить паспорт, затем только удалить гражданина. Если мы ничего не сделаем с паспортом, в базе останется паспорт, указывающий на несуществующего гражданина, что не хорошо. На самом деле, конечно же, в базе будет foreign key constraint, которые просто не даст сделать такое действие, что вообщем-то, не сильно лучше для конечного пользователя.

Каскадирование позволяет сказать JPA «сделай с владеемыми объектами класса тоже самое, что ты делаешь с владельцем». То есть, когда мы удаляем гражданина из базы, JPA самостоятельно увидит, что гражданин владеет паспорт и удалит вначале паспорт, потом гражданина.

Другой случай использования каскадирования характерен именно для связи один к одному. Если мы без каскадирования создадим паспорт и гражданина, мы не сможем их по раздельности сохранить, потому что паспорт будет ссылаться на гражданина, которого ещё нет в базе данных, а гражданин будет ссылаться на паспорт, которого тоже ещё нет в базе данных. Каскадирование позволяет, в данном случае, обойти эту проблему.

Размеется, CascadeType.ALL это не единственный возможный тип каскадирования, но их отличиям я посвящу отдельную статью.

Итак, кроме настроек каскадирования владелец связи один к одному добавляет к полю кроме аннотации @OneToOne  ещё и аннотацию @JoinColumn, которая задаёт имя столбца, в котором будет храниться ссылка на владеемый объект. Физически это будет столбец с заданным именем, таким же типом, как у первичного ключа владеемого объекта и содержаться в нём будут значения первичных ключей владеемых объектов.

Со стороны паспорта, то есть владеемого объекта, такой столбец не требуется, а требуется в аннотации @OneToOne  задать параметр mappedBy, который указывает, какое поле в объекте владельце (то есть классе Person) соответствует владеемому объекту (то есть passport).

@OneToMany

Кроме паспорта мы хотим хранить для граждан так же и место проживания. Очевидно, что один гражданин имеет один основной адрес проживания, но при этом по одному адресу может проживать несколько человек. Граждане и адреса образуют связь один ко многим и это самый распространённый тип связи между классами.

@Entity
public class Person extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String firstName;


    @Getter
    @Setter
    private String lastName;


    @Getter
    @Setter
    private LocalDate dob;


    @Getter
    @Setter
    @OneToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "PASSPORT_ID")
    private Passport passport;


    @Getter
    @Setter
    @ManyToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "ADDRESS_ID")
    private Address primaryAddress;
}
@Entity
public class Address extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String city;


    @Getter
    @Setter
    private String street;


    @Getter
    @Setter
    private String building;


    @Getter
    @Setter
    @OneToMany(mappedBy = "primaryAddress", fetch = FetchType.EAGER)
    private Collection<Person> tenants;
}

Владельцем в данном случае опять будет класс Person, который аннотирует поле primaryAddress аннотациями  @ManyToOne и @JoinColumn. Параметры этих аннотаций несут ту же смысловую нагрузку, что и у связи один к одному.

У владеемого объекта в этот раз всё по другому. Жильцы, которых по одному адресу может быть несколько, представлены коллекцией, которая аннотирована @OneToMany. Параметр mappedBy так же указывает на поле с владеемым в классе владельце. А вот параметр fetch = FetchType.EAGER говорит, что при загрузке владеемого объекта необходимо сразу загрузить и коллекцию владельцев.

Стратегии загрузки (fetch) бывает две: EAGER и LAZY. В первом случае объекты коллекции сразу загружаются в память, во втором случае только при обращении к ним. Оба подхода имеют достоинства и недостатки. В случае FetchType.EAGER в памяти будут находиться полностью загруженные и готовые к употреблению объекты. При этом они будут эту самую память занимать и если вам нужен только один объект из сотен (тысяч), то занимать они её будут просто так. Кроме того, при загрузке какого-нибудь корневого объекта, который связан со всеми остальными объектами и коллекциями, можно случайно попытаться загрузить в память и всю базу ????

С другой стороны, FetchType.LAZY загружает объекты только по мере обращения, но при этом требует, чтобы соединение с базой (или транзакция) сохранялись. Если быть точно, требует, чтобы объект был attached, но и про это я расскажу позднее. Поэтому для работы с lazy объектами тратится больше ресурсов на поддержку соединений.

@ManyToMany

И наконец, мы бы хотели хранить сведения о местах работы. Один гражданин может работать в нескольких компаниях, а в каждой компании работает несколько человек. Это — связь многие ко многим.

@Entity
public class Person extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String firstName;


    @Getter
    @Setter
    private String lastName;


    @Getter
    @Setter
    private LocalDate dob;


    @Getter
    @Setter
    @OneToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "PASSPORT_ID")
    private Passport passport;


    @Getter
    @Setter
    @ManyToOne(optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "ADDRESS_ID")
    private Address primaryAddress;


    @Getter
    @Setter
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(name = "PERSON_COMPANIES",
            joinColumns = @JoinColumn(name = "PERSON_ID"),
            inverseJoinColumns = @JoinColumn(name = "COMPANY_ID")
    )
    private Collection<Company> workingPlaces;
}
@Entity
public class Company extends AbstractIdentifiableObject {
    @Getter
    @Setter
    private String name;


    @Getter
    @Setter
    @ManyToMany(mappedBy = "workingPlaces")
    private Collection<Person> workers;
}

Связь многие ко многим с обоих сторон представлена коллекцией объектов. Так как напрямую в реляционных базах данных такая связь не поддерживается, JPA реализует её с помощью промежуточной таблицы, которая описывается аннотацией @JoinTable у объекта владельца. Параметр name задаёт имя промежуточной таблицы, joinColumns — имя столбца, связывающего с классом владельцем, inverseJoinColumns — имя столбца, связывающего с владеемым классом.

Во владеемом классе аннотацией @ManyToMany отмечаем поле с коллекцией объектов класса владельца. Параметр mappedBy опять указывает на поле с коллекцией владеемых объектов в классе владельце

Использовать классы со связями довольно просто — вначале наполняем их данными, как обычные классы без всякого JPA, а потом, благодаря каскадированию, сохраняем объект класса владельца и JPA делает всё остальное:

        Passport p = new Passport();
        p.setSeries("AS");
        p.setNo("123456");
        p.setIssueDate(LocalDate.now());
        p.setValidity(Period.ofYears(20));


        Address a = new Address();
        a.setCity("Kickapoo");
        a.setStreet("Main street");
        a.setBuilding("1");


        Person person = new Person();
        person.setFirstName("Test");
        person.setLastName("Testoff");
        person.setDob(LocalDate.now());
        person.setPrimaryAddress(a);
        person.setPassport(p);


        Company c = new Company();
        c.setName("Acme Ltd");


        p.setOwner(person);
        person.setWorkingPlaces(Collections.singletonList(c));


        entityManagerFactory = Persistence.createEntityManagerFactory("ru.easyjava.data.jpa.hibernate");
        EntityManager em = entityManagerFactory.createEntityManager();
        em.getTransaction().begin();
        em.merge(person);
        em.getTransaction().commit();
        em.close();

Опасность, которая ожидает нас при использовании связей в классах, это циклические связи и вытекающая из них рекурсия. Например метод toString() в классе Passport реализован без вызова toString() у класса Person

@Override
public String toString() {
    return "Passport{" +
            "series='" + series + '\'' +
            ", no='" + no + '\'' +
            ", issueDate=" + issueDate +
            ", validity=" + validity +
            ", owner=" + owner.getLastName() +
            '}';
}

Потому что, если бы он просто вызывал toString() у Person, то тот, в свою очередь, вызывал бы toString() у своего поля passport и всё закончилось бы бесконечной рекурсией и StackOverFlowException. Пример с toString() разумеется весьма банален, но в реальных приложениях вляпаться в такую рекурсию при обходе иерархии связей к сожалению не просто, а очень просто.

Скачать код примера

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *