JPA предлагает аннотации @OneToMany , @ManyToOne , @OneToOne и @ManyToMany для сопоставления ассоциаций между объектами. В то время как EJB 2.x предлагает управляемые контейнерами отношения для управления этими ассоциациями, и особенно для синхронизации двунаправленных ассоциаций, JPA оставляет больше на усмотрение разработчика.
Настройка
Начнем с расширения примера Order из предыдущего блога с помощью объекта OrderLine. У него есть идентификатор, описание, цена и ссылка на содержащий его заказ:
@Entity public class OrderLine { @Id @GeneratedValue private int id; private String description; private int price; @ManyToOne private Order order; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public Order getOrder() { return order; } public void setOrder(Order order) { this.order = order; } }
Используя общий шаблон DAO, мы быстро получаем очень простой интерфейс OrderLineDao и реализацию:
public interface OrderLineDao extends Dao<Integer, OrderLine> { public List<OrderLine> findOrderLinesByOrder(Order o); } public class JpaOrderLineDao extends JpaDao<Integer, OrderLine> implements OrderLineDao { public List<OrderLine> findOrderLinesByOrder(Order o) { Query q = entityManager.createQuery("SELECT e FROM " + entityClass.getName() + " e WHERE order = :o"); q.setParameter("o", o); return (List<OrderLine>) q.getResultList(); } }
Мы можем использовать этот DAO, чтобы добавить строку заказа к заказу или найти все строки заказа для заказа:
OrderLine line = new OrderLine(); line.setDescription("Java Persistence with Hibernate"); line.setPrice(5999); line.setOrder(o); orderLineDao.persist(line); Collection<OrderLine> lines = orderLineDao.findOrderLinesByOrder(o);
Мо ассоциации, МО проблемы
Все это довольно просто, но становится интересным, когда мы делаем эту ассоциацию двунаправленной. Давайте добавим поле orderLines к нашему объекту Order и включим наивную реализацию пары getter / setter:
@OneToMany(mappedBy = "order") private Set<OrderLine> orderLines = new HashSet<OrderLine>(); public Set<OrderLine> getOrderLines() { return orderLines; }
public void setOrderLines(Set<OrderLine> orderLines) { this.orderLines = orderLines; }
Поле mappedBy в аннотации @OneToMany сообщает JPA, что это обратная сторона ассоциации, и вместо сопоставления этого поля непосредственно со столбцом базы данных оно может посмотреть на поле порядка объекта OrderLine, чтобы узнать, к какому объекту заказа он относится. ,
Таким образом, не изменяя основную базу данных, мы можем теперь получить строки заказа для заказа, подобного этому:
Collection<OrderLine> lines = o.getOrderLines();
Больше нет необходимости получать доступ к OrderLineDao.
Но есть подвох! Хотя управляемые контейнером отношения (CMR), определенные EJB 2.x, обеспечили, чтобы добавление объекта OrderLine к свойству orderLines Order также устанавливало свойство order для этой OrderLine (и наоборот), JPA (являясь каркасом POJO) выполняет нет такой магии. Это на самом деле хорошая вещь, потому что это делает наши доменные объекты пригодными для использования вне контейнера JPA, что означает, что вы можете тестировать их проще и использовать их, когда они еще не были сохранены (пока). Но это также может сбивать с толку людей, привыкших к поведению EJB 2.x CMR .
Если вы запустите приведенные выше примеры в отдельных транзакциях, вы обнаружите, что они работают правильно. Но если вы запустите их в одной транзакции, как показано в приведенном ниже коде, вы увидите, что список элементов пока пуст:
Order o = new Order(); o.setCustomerName("Mary Jackson"); o.setDate(new Date()); OrderLine line = new OrderLine(); line.setDescription("Java Persistence with Hibernate"); line.setPrice(5999); line.setOrder(o); System.out.println("Items ordered by " + o.getCustomerName() + ": "); Collection<OrderLine> lines = o.getOrderLines(); for (OrderLine each : lines) { System.out.println(each.getId() + ": " + each.getDescription() + " at $" + each.getPrice()); }
Это можно исправить, добавив следующую строку перед первым оператором System.out.println:
o.getOrderLines().add(line);
Исправление и исправление …
Это работает, но это не очень красиво. Это нарушает абстракцию и является хрупким, так как зависит от пользователя наших доменных объектов, чтобы правильно вызывать эти сеттеры и сумматоры. Мы можем исправить это, переместив этот вызов в определение OrderLine.setOrder (Order):
public void setOrder(Order order) {
this.order = order;
order.getOrderLines().add(this);
}
Когда это можно сделать еще лучше, лучше инкапсулируя свойство orderLines объекта Order:
public Set<OrderLine> getOrderLines() { return orderLines; }
public void addOrderLine(OrderLine line) { orderLines.add(line); }
И тогда мы можем переопределить OrderLine.setOrder (Order) следующим образом:
public void setOrder(Order order) { this.order = order; order.addOrderLine(this); }
Все еще со мной? Я надеюсь на это, но если нет, пожалуйста, попробуйте и убедитесь сами.
Теперь возникает другая проблема. Что если кто-то напрямую вызовет метод Order.addOrderLine (OrderLine)? OrderLine будет добавлен в коллекцию orderLines, но его свойство order не будет указывать на принадлежащий ему порядок. Модификация Order.addOrderLine (OrderLine), как показано ниже, не будет работать, потому что это приведет к бесконечному циклу с addOrderLine, вызывающим setOrder, вызывающим addOrderLine, вызывающим setOrder и т. Д .:
public void addOrderLine(OrderLine line) { orderLines.add(line); line.setOrder(this); }
Эту проблему можно решить, введя метод Order.internalAddOrderLine (OrderLine), который только добавляет строку в коллекцию, но не вызывает line.setOrder (this). Этот метод затем будет вызван из OrderLine.setOrder (Order) и не вызовет бесконечный цикл. Пользователи класса Order должны вызывать Order.addOrderLine (OrderLine).
Шаблон
Принимая эту идею к ее логическому завершению, мы получаем следующие методы для класса OrderLine:
public Order getOrder() { return order; }
public void setOrder(Order order) {
if (this.order != null) { this.order.internalRemoveOrderLine(this); }
this.order = order;
if (order != null) { order.internalAddOrderLine(this); }
}
И эти методы для класса Order:
public Set<OrderLine> getOrderLines() { return Collections.unmodifiableSet(orderLines); }
public void addOrderLine(OrderLine line) { line.setOrder(this); }
public void removeOrderLine(OrderLine line) { line.setOrder(null); }
public void internalAddOrderLine(OrderLine line) { orderLines.add(line); }
public void internalRemoveOrderLine(OrderLine line) { orderLines.remove(line); }
Эти методы обеспечивают реализацию на базе POJO логики CMR, встроенной в EJB 2.x. С типичными преимуществами POJOish — простота понимания, тестирования и обслуживания.
Конечно, есть несколько вариантов на эту тему:
- Если Order и OrderLine находятся в одном пакете, вы можете указать область действия внутренних … методов, чтобы предотвратить их случайный вызов. (Здесь пригодится концепция класса C ++ для друзей . Опять же, давайте не будем здесь. ).
- Вы можете покончить с методами removeOrderLine и internalRemoveOrderLine, если строки заказа никогда не будут удалены из заказа.
- Вы можете переместить ответственность за управление двунаправленной ассоциацией из метода OrderLine.setOrder (Order) в класс Order, в основном перевернув идею. Но это означало бы распространение логики на методы addOrderLine и removeOrderLine.
- Вместо или в дополнение к использованию Collections.singletonSet, чтобы сделать orderLine установленным только для чтения во время выполнения, вы также можете использовать универсальные типы, чтобы сделать его доступным только для чтения во время компиляции:
public Set<? extends OrderLine> getOrderLines() { return Collections.unmodifiableSet(orderLines); }
Но это усложняет насмешку над этими объектами с помощью насмешливой среды, такой как EasyMock .
Есть также несколько вещей, которые следует учитывать при использовании этого шаблона:
- Добавление OrderLine к Заказу не сохраняет его автоматически. Вам также нужно будет вызвать метод persist в его DAO (или EntityManager), чтобы сделать это. Или вы можете установить свойство каскада аннотации @OneToMany для свойства Order.orderLines в CascadeType.PERSIST (как минимум) для достижения этого. Подробнее об этом, когда мы обсудим метод EntityManager.persist .
- Двунаправленные ассоциации плохо работают с методом EntityManager.merge . Мы обсудим это, когда перейдем к вопросу об отдельных объектах.
- Когда сущность, которая является частью двунаправленной ассоциированной сущности, (собирается быть) удалена, она также должна быть удалена с другого конца ассоциации. Это также произойдет, когда мы поговорим о методе EntityManager.remove .
- Приведенный выше шаблон работает только при использовании доступа к полю (вместо доступа к свойству / методу), чтобы ваш поставщик JPA заполнил ваши объекты. Доступ к полю используется, когда аннотация @Id вашей сущности размещается в соответствующем поле, а не в соответствующем получателе. Предпочитать ли доступ к полю или доступ к свойству / методу — спорный вопрос, к которому я вернусь в следующем блоге.
- И последнее но не менее важное; хотя этот шаблон может быть технически обоснованной реализацией управляемых ассоциаций на основе POJO, вы можете поспорить, зачем вам все эти методы получения и установки. Зачем вам нужно использовать и Order.addOrderLine (OrderLine), и OrderLine.setOrder (Order) для достижения одинакового результата? Отказ от одного из них может сделать наш код проще. Посмотрите, например , статью Джеймса Холуба о методах получения и установки . С другой стороны, мы обнаружили, что этот шаблон дает разработчикам, которые используют эти доменные объекты, гибкую возможность связывать их по своему усмотрению.
Ну, это на сегодня. Мне очень интересно услышать ваши отзывы об этом и узнать, как вы управляете двунаправленными ассоциациями в ваших объектах домена JPA. И конечно же: увидимся на следующей схеме!