В прошлой статье мы начали разговор о реализации паттерна DAO в Java. В качестве наглядного примера использовалась ситуация с двумя сущностями: группой и студентом. В самом начале нашей беседы мы столкнулись с дилеммой: как реализовывать связь студент-группа? Сохранять ли первичный ключ группы, в которой состоит студент, или хранить объект, эту группу описывающий? Первый вариант мы уже рассмотрели. Пришла пора реализовать второй. |
Второй способ. Храним ссылку на объект.
И так, наша цель абстрагироваться от реляционного представления и максимально обобщить код получившегося решения. Мы будем рассматривать простейшую связь многие-к-одному. В выбранном примере это означает, что в одной группе может учиться несколько студентов, но каждый студент может быть зачислен только в одну группу. Кроме того, у объектов описывающих студентов будет ссылка на группу, в то время как у объектов групп ссылки на студентов не будет.
Student.java
Что мы хотим получить? TDD
Ситуация, когда один хранимый объект имеет ссылку на другой хранимый объект, не столь прозрачна, как в случае с непосредственным отражением реляционной структуры. Прежде всего, давайте определимся с тем, что мы хотим получить, а точнее с тем, как должны вести себя наши объекты в CRUD(Create, Read, Update, Delete) ситуациях. В этом нам поможет принцип разработки отталкивающийся от тестов, так называемый test-driven development. Вначале мы напишем тесты, которые описывают желаемое поведение, и в процессе статьи добьемся их выполнения.Create
Начнем с операции создания объекта. Прежде всего, объект должен создаваться без ошибок и допускать изменение ссылки на группу:
@Test
public void testCreate() throws PersistException {
Student student = (Student) factory.getDao(connection, Student.class).create();
Assert.assertNull("Group is not null.", student.getGroup());
Group group = new Group();
student.setGroup(group);
Assert.assertNotNull("Group is null.", student.getGroup());
}
Persist
При сохранении объекта в базе, мы будем сохранять и его зависимости. Напомню, что при создании записи об объекте (метод persist), мы создаем новый объект. Поэтому важно проконтролировать, что ссылка на группу не теряется. К тому же, здесь мы сталкиваемся с двоякой ситуацией: в одном случае объекту Group уже может соответствовать запись в базе данных, в другом нет. Нам необходимо проверить оба варианта:
@Test
public void testPersist() throws PersistException {
Student student = new Student();
Group group = (Group) factory.getDao(connection, Group.class).create();
student.setGroup(group);
group.setNumber(1234);
student = (Student) factory.getDao(connection, Student.class).persist(student);
Assert.assertNotNull("Group is null.", student.getGroup());
Assert.assertEquals("Wrong group number.", 1234, student.getGroup().getNumber());
}
@Test
public void testPersistAll() throws PersistException {
Student student = new Student();
student.setGroup(new Group());
student = (Student) factory.getDao(connection, Student.class).persist(student);
Assert.assertNotNull("Group is null.", student.getGroup());
Assert.assertNotNull("Group.id is null.", student.getGroup().getId());
}
Update
При обновлении записи об объекте тесты практически идентичны тем, что мы будем использовать для проверки метода persist:
@Test
public void testUpdate() throws PersistException {
Student student = (Student) factory.getDao(connection, Student.class).create();
student.setGroup(new Group());
factory.getDao(connection, Student.class).update(student);
Assert.assertNotNull("Group is null.", student.getGroup());
Assert.assertNotNull("Group.id is null.", student.getGroup().getId());
}
@Test
public void testUpdateAll() throws PersistException {
Student student = (Student) factory.getDao(connection, Student.class).create();
Group group = (Group) factory.getDao(connection, Group.class).create();
group.setNumber(1234);
student.setGroup(group);
factory.getDao(connection, Student.class).update(student);
Assert.assertNotNull("Group is null.", student.getGroup());
Assert.assertEquals("Wrong group number.", 1234, student.getGroup().getNumber());
}
Read
Пора позаботиться о том, чтобы объекты были доступны после записи их состояния в базу данных:
@Test
public void testRead() throws PersistException {
Student student = (Student) factory.getDao(connection, Student.class).create();
student.setGroup(new Group());
factory.getDao(connection, Student.class).update(student);
student = (Student) factory.getDao(connection, Student.class).getByPK(student.getId());
Assert.assertNotNull("Student is null.", student);
Assert.assertNotNull("Group is null.", student.getGroup());
}
Delete
И наконец удаление. И вот здесь кроется один очень важный нюанс. Если удаление студента логически не ведет за собой удаление группы,
@Test
public void testDelete() throws PersistException {
Student student = (Student) factory.getDao(connection, Student.class).create();
student.setGroup(new Group());
factory.getDao(connection, Student.class).update(student);
Group group = student.getGroup();
factory.getDao(connection, Student.class).delete(student);
group = (Group) factory.getDao(connection, Group.class).getByPK(group.getId());
Assert.assertNotNull("Group not found.", group);
}
RelationTest.java
Приступим
MySqlStudentDao
Прежде всего нам необходимо исправить методы parseResultSet, prepareStatementForInsert и prepareStatementForUpdate в классе MySqlStudentDao. В методах prepareStatementForInsert и prepareStatementForUpdate изменится процесс получения идентификатора группы:
@Override
protected void prepareStatementForUpdate(PreparedStatement statement, Student object) throws PersistException {
try {
int groupId = (object.getGroup() == null || object.getGroup().getId() == null) ? -1
: object.getGroup().getId();
....
statement.setInt(4, groupId);
} catch (Exception e) {
throw new PersistException(e);
}
}
@Override
protected void prepareStatementForInsert(PreparedStatement statement, Student object) throws PersistException {
try {
int groupId = (object.getGroup() == null || object.getGroup().getId() == null) ? -1
: object.getGroup().getId();
.....
statement.setInt(4, groupId);
} catch (Exception e) {
throw new PersistException(e);
}
}
И все таки без ссылки на фабрику нам не обойтись. Мы сохраним ее, но сделаем это на уровне AbstractJDBCDao в приватном поле, а для получения необходимого объекта объявим protected метод, возвращающий объект указываемого класса, по значению указанного ключа:
protected Identified getDependence(Class<? extends Identified> dtoClass, Serializable pk);
@Override
protected List<Student> parseResultSet(ResultSet rs) throws PersistException {
LinkedList<Student> result = new LinkedList<Student>();
try {
while (rs.next()) {
...
student.setGroup((Group)getDependence(Group.class, rs.getInt("group_id")));
...
}
} catch (Exception e) {
throw new PersistException(e);
}
return result;
}
AbstractJDBCDao
В нашем базовом дао-классе стало на одну зависимость больше. Теперь он требует при создании указывать создающую его фабрику (конструкторов наследующих классов это изменение тоже касается):public abstract class AbstractJDBCDao<T extends Identified<PK>, PK extends Integer> implements GenericDao<T, PK> {
private DaoFactory<Connection> parentFactory;
...
public AbstractJDBCDao(DaoFactory<Connection> parentFactory, Connection connection) {
this.parentFactory = parentFactory;
this.connection = connection;
}
protected Identified getDependence(Class<? extends Identified> dtoClass, Serializable pk) throws PersistException {
return parentFactory.getDao(connection, dtoClass).getByPK(pk);
}
...
}
А теперь самое интересное. Чтобы выполнить метод persist или update мы должны сохранить состояние всех объектов на которые ссылаемся. При чем сделать это надо до сохранения самого объекта, иначе может сложиться ситуация, когда зависимый объект окажется только созданным и не сохраненным в базу, тогда его идентификатор не будет определен, что не позволит правильно сделать запись об объекте, который на него ссылается:
@Override
public T persist(T object) throws PersistException {
if (object.getId() != null) {
throw new PersistException("Object is already persist.");
}
// Сохраняем зависимости
saveDependences(object);
...
}
@Override
public void update(T object) throws PersistException {
// Сохраняем зависимости
saveDependences(object);
...
}
Напомню, что методы persist и update реализованы в базовом абстрактном классе AbstractJDBCDao, который ничего не знает о полях объекта с зависимостями. И тем не менее, для реализации метода saveDependences нам необходимо перебрать все такие поля. Каким же образом нам лучше всего получить список таких полей? И как отличить их от прочих? Для этого мы создадим новый класс - ManyToOne, который будет отвечать за работу с полем, ссылающимся на хранимый объект. Интерфейс класса определит себя сам, в процессе реализации метода saveDependences:
private void saveDependences() throws PersistException {
for (ManyToOne m : relations) {
if (m.getDependence() == null) {
continue;
}
if (m.getDependence().getId() == null) {
Identified depend = m.persistDependence(connection);
m.setDependence(depend);
} else {
m.updateDependence(connection);
}
}
}
ManyToOne
Первая версия интерфейса класса ManyToOne:/**
* Отвечает за реализацию связи многие-к-одному.
*
* @param <Owner> класс объекта, чье поле ссылается на зависимый объект.
* @param <Dependence> класс зависимого объекта.
*/
public class ManyToOne<Owner extends Identified, Dependence extends Identified> {
public Dependence getDependence() {
return null;
}
public void setDependence(Dependence dependence) {
}
public Dependence persistDependence(Connection connection) {
return null;
}
public void updateDependence(Connection connection) {
}
}
Для реализации такого интерфейса нам явно понадобятся дополнительные зависимости. В частности реализация методов getDependence и setDependence очевидно требуют ссылки на объект класса Owner и дополнительной информации непосредственно о том поле, которое ссылается на зависимый объект. Чтобы реализовать методы persistDependence и updateDependence необходима ссылка на фабрику дао-объектов и класс зависимого объекта:
/**
* Отвечает за реализацию связи многие-к-одному.
*
* @param <Owner> класс объекта, чье поле ссылается на зависимый объект.
* @param <Dependence> класс зависимого объекта.
*/
public class ManyToOne<Owner extends Identified, Dependence extends Identified> {
private DaoFactory<Connection> factory;
private Field field;
private String name;
private int hash;
public Dependence getDependence(Owner owner) throws IllegalAccessException {
return (Dependence) field.get(owner);
}
public void setDependence(Owner owner, Dependence dependence) throws IllegalAccessException {
field.set(owner, dependence);
}
public Identified persistDependence(Owner owner, Connection connection) throws IllegalAccessException, PersistException {
return factory.getDao(connection, field.getType()).persist(getDependence(owner));
}
public void updateDependence(Owner owner, Connection connection) throws IllegalAccessException, PersistException {
factory.getDao(connection, field.getType()).update(getDependence(owner));
}
@Override
public String toString() {
return name;
}
@Override
public int hashCode() {
return hash;
}
public ManyToOne(Class<Owner> ownerClass, DaoFactory<Connection> factory, String field) throws NoSuchFieldException {
this.factory = factory;
this.field = ownerClass.getDeclaredField(field);
this.field.setAccessible(true);
name = ownerClass.getSimpleName() + " to " + this.field.getType().getSimpleName();
hash = ownerClass.hashCode() & field.hashCode();
}
}
...
private Set<ManyToOne> relations = new HashSet<ManyToOne>();
...
protected boolean addRelation(Class<? extends Identified> ownerClass, String field) {
try {
return relations.add(new ManyToOne(ownerClass, parentFactory, field));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
private void saveDependences(Identified owner) throws PersistException {
for (ManyToOne m : relations) {
try {
if (m.getDependence(owner) == null) {
continue;
}
if (m.getDependence(owner).getId() == null) {
Identified depend = m.persistDependence(owner, connection);
m.setDependence(owner, depend);
} else {
m.updateDependence(owner, connection);
}
} catch (Exception e) {
throw new PersistException("Exception on save dependence in relation " + m + ".", e);
}
}
}
Регистрация связей должна происходить в конструкторе дао-объектов:
...
public MySqlStudentDao(DaoFactory<Connection> parentFactory, Connection connection) {
super(parentFactory, connection);
addRelation(Student.class, "group");
}
...
Вот и все. Исправив мелкие ошибки компиляции, связанные с изменение интерфейсов используемых ранее классов, мы можем запускать наши тесты и радоваться их успешному выполнению!
Исходный код можно получить здесь под тегом 2.0.
Подводя итоги
Использование JDBC для реализации DAO дает наиболее полное понимание того, как именно происходит взаимодействие с базой данных. Позволяет выполнять это взаимодействие максимально гибко и оптимально.
Но давайте говорить честно, простое решение, представленное в предыдущей статье, мало пригодно для жизни, т.к. редко встречается ситуация, когда сущности не имеют связей, а при наличии этих связей идея хранения в полях объектов внешних ключей, приводит к необходимости нетривиального жонглирования сущностями и dao объектами.
Идея возвращать в полях ссылки на зависимости выглядит более привлекательной для использования, но требует достаточно трудного решения. Мы рассмотрели только одну связь и при том не самую сложную, да еще и с упрощением (допустили существование каждой сущности раздельно), но код все равно получился большим и сложным.
Вынесение кода в AbstractJDBCDao позволило серьезно упростить реализацию дао-классов для новых сущностей, но эта задача так и осталась крайне ресурсоемкой.
15 комментариев:
В методе parseResultSet(ResultSet rs) любого dao класса с зависимостями, на разбор одного ResultSet, например на 1000 строк, будет сделано как минимум 1000 запросов к БД.
Боюсь вы правы. Но этих издержек можно избежать, кешируя объекты в фабрике и в методе getDependence пытаться получить объекты вначале из кеша. Правда это приводит к нетривиальной задаче поддержания кеша в актуальном состоянии.
Также в этом же методе наблюдается косвенная рекурсия через метод T getByPK(Integer key). Допустим, в БД имеется "центральная" таблица, которая напрямую или косвенно связана со всеми таблицами БД, то вызов метода parseResultSet создаст все объекты сущностей БД, объем же БД чаще всего не ограничен.
Добрый день, подскажите пожалуйста почемы вылетают ошибки в вашем коде при добавлении студента? main -> http://pastebin.com/index/VxWc2Jda
лог:
ru.dokwork.daotalk.dao.PersistException: java.lang.ClassCastException: ru.dokwork.daotalk.domain.Student cannot be cast to ru.dokwork.daotalk.domain.Group
at ru.dokwork.daotalk.dao.AbstractJDBCDao.persist(AbstractJDBCDao.java:122)
at ru.dokwork.daotalk.mysql.MySqlDaoFactory.main(MySqlDaoFactory.java:85)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Caused by: java.lang.ClassCastException: ru.dokwork.daotalk.domain.Student cannot be cast to ru.dokwork.daotalk.domain.Group
at ru.dokwork.daotalk.mysql.MySqlGroupDao.prepareStatementForInsert(MySqlGroupDao.java:14)
at ru.dokwork.daotalk.dao.AbstractJDBCDao.persist(AbstractJDBCDao.java:116)
... 6 more
Вы создаете dao для группы, а затем пытаетесь с его помощью сохранить студента. Студенты должны сохраняться с помощью соответствующего дао.
Спасибо, помогло. Еще один вопросик: правильно ли я понимаю, что при добавлении 2х студентов в одну группу, у вас в бд создается 2 строки одной и той ж группы(number, department), но с разными id?
и подскажите пожалуйста как примерно делать, если у нас зависимость many to many ?
или может статьей поделитесь с объяснениями, если встречали.
по поводу двух студентов: сейчас уже не помню деталей, но если это действительно так, как Вы говорите, то это ошибка. По поводу many to many - это далеко не простая задача. Лучше много раз подумать, прежде чем пытаться ее унифицированно решать и взять готовое ORM решение :) Как справедливо заметили в первом комментарии к статье, предложенное решение имеет существенные недостатки, и имеет скорее фановый характер.
Здравствуйте
Всегда интересовал вопрос представления данных, после того как мы получим их через DAO. Например, мы получили коллекцию объектов Student, из этой коллекции мне нужен массив вида id -> name, чтобы создать на форме выпадающий список(select). Можно делать это по разному, получать сразу из DAO готовый массив, либо каким-то образом преобразовывать уже снаружи нашу коллекцию. Что-то посоветуете?
Если вы коллекцию уже получили, то лучше ее трансформировать. Если вам нужны только два поля, ради них вытаскивать сущности целиком смысла нет. На сколько я понимаю, ситуации, когда нужны только часть полей объектов достаточно часто встречаются и каждый ORM с ними борется по своему, но суть всех решений сводится к одному: вытаскивать из базы минимум информации
Мне интересен вопрос именно трансформации. Я сейчас создаю специальный объект-адаптер, который возвращает нужный мне массив на основе коллекции. Это мой подход не знаю на сколько это типично. Как делаете вы, интересно ваше мнение?
Скажем так, если бы мне надо было сформировать _только_ список имен студентов и их идентификаторов, целиком описание студентов из базы я бы не доставал и выполнял запрос только нужных мне полей. Возвращать массив идея спорная - просто, быстро, эффективно, но трудно поддерживать. Я бы завел отдельный объект с двумя полями.
Что до адаптера - с java 8 предпочтительнее делать фильтр коллекции с помощью stream api. Нагляднее на мой взгляд.
Отправить комментарий