Замена полного ORM (JPA/Hibernate) более легким решением: рекомендуемые шаблоны для загрузки/сохранения?

Я разрабатываю новое веб-приложение Java, и я изучаю новые способы (новые для меня!), чтобы сохранить данные. У меня в основном есть опыт работы с JPA и Hibernate, но, за исключением простых случаев, я думаю, что такой полный ORM может стать довольно сложным. Кроме того, мне не очень нравится работать с ними. Я ищу новое решение, возможно, ближе к SQL.

Решения, которые я сейчас изучаю:

  • MyBatis
  • JOOQ
  • Обычный SQL/JDBC, возможно с DbUtils или некоторыми другими базовыми библиотеками утилиты.

Но есть два варианта использования, которые я беспокоюсь по этим решениям, по сравнению с Hibernate. Я хотел бы знать, какие рекомендуемые шаблоны для этих случаев использования.


Пример использования 1 - выборка объекта и доступ к некоторым связанным с ним дочерним объектам и внукам.

  • Скажем, у меня есть объект Person.
    • Этот Person имеет связанный объект Address.
      • Этот Address имеет связанный объект City.
        • Объект City имеет свойство name.

Полный путь доступа к имени города, начиная с лица, будет:

person.address.city.name

Теперь скажем, я загружаю объект Person из PersonService с помощью этого метода:

public Person findPersonById(long id)
{
    // ...
}

Используя Hibernate, сущности, связанные с Person, могут быть лениво загружены по требованию, поэтому можно было бы получить доступ к person.address.city.name и быть уверенным, что у меня есть доступ к этому свойству (если все объекты в этом цепочка не может быть нулевой).

Но с помощью любого из трех решений, которые я изучаю, это сложнее. С этими решениями, каковы рекомендуемые шаблоны для ухода за этим вариантом использования? Наверху я вижу 3 возможных шаблона:

  • Все необходимые дочерние объекты и внуки могут быть загружены с помощью используемого SQL-запроса.

    Но проблема, которую я вижу в этом решении, заключается в том, что может быть какой-то другой код, который должен получить доступ к другим путям объектов/свойств из объекта Person. Например, возможно, какой-то код будет нуждаться в доступе к person.job.salary.currency. Если я хочу повторно использовать метод findPersonById(), который у меня уже есть, тогда SQL-запрос должен будет загрузить дополнительную информацию! Не только связанный объект address->city, но также связанный с ним объект job->salary.

    Теперь что, если есть 10 другие места, которым необходимо получить доступ к другой информации, начиная с лица? Должен ли я всегда загружать всю потенциально необходимую информацию? Или, может быть, у вас есть 12 различных методов обслуживания для загрузки лица?

    findPersonById_simple(long id)
    
    findPersonById_withAdressCity(long id)
    
    findPersonById_withJob(long id)
    
    findPersonById_withAdressCityAndJob(long id)
    
    ...
    

    Но тогда каждый раз, когда я буду использовать объект Person, я должен был бы знать, что было загружено с ним, а что нет... Это может быть довольно громоздко, правильно?

  • В методе getAddress() getter объекта Person может быть проверка, чтобы проверить, был ли уже загружен адрес и, если нет, его лениво загружать? Это часто используется в реальных приложениях?

  • Существуют ли другие шаблоны, которые можно использовать для обеспечения доступа к объектам/свойствам, которые мне нужны из загруженной модели?


Use Case 2 - Сохранение сущности и обеспечение того, чтобы ее связанные и измененные объекты также сохранялись.

Я хочу сохранить объект Person, используя этот метод PersonService:

public void savePerson(Person person)
{
    // ...
}

Если у меня есть объект Person и я меняю person.address.city.name на что-то еще, как я могу убедиться, что модификации сущностей City будут сохранены, когда я сохраню Person? Использование Hibernate может быть легко каскадировать операцию сохранения связанным объектам. Как насчет решений, которые я изучаю?

  • Должен ли я использовать какой-то грязный флаг, чтобы узнать, какие связанные объекты также должны быть сохранены при сохранении человека?

  • Есть ли другие известные шаблоны, полезные для использования в этом случае?


Обновить: обсуждение об этом вопросе на форуме JOOQ.

Ответ 1

Эта проблема типична, когда не используется реальный ORM, и нет серебряной пули. Простой подход к дизайну, который работал у меня (не очень большой) webapp с iBatis (myBatis), заключается в использовании двух уровней для сохранения:

  • Тупой низкоуровневый уровень: каждая таблица имеет свой класс Java (POJO или DTO), с полями , которые отображаются непосредственно в столбцы таблицы. Скажем, у нас есть таблица PERSON с полем ADDRESS_ID, которая указывает на таблицу ADRESS; то у нас был бы класс PersonDb, содержащий только поле addressId (integer); у нас нет метода personDb.getAdress(), просто plain personDb.getAdressId(). Таким образом, эти классы Java довольно глупы (они не знают о сохранении или о связанных классах). Соответствующий класс PersonDao знает, как загрузить/перенести этот объект. Этот слой легко создавать и поддерживать с помощью таких инструментов, как iBatis + iBator (или MyBatis + MYBatisGenerator).

  • Уровень более высокого уровня, содержащий богатые объекты домена: каждый из них обычно представляет собой график вышеуказанных POJO. Эти классы также обладают интеллектом для загрузки/сохранения графика (возможно, лениво, возможно, с некоторыми грязными флагами), вызывая соответствующие DAO. Однако важно то, что эти объекты с богатым доменом не сопоставляются друг с другом объектам POJO (или таблицам БД), а скорее с случаями использования домена. Определяется "размер" каждого графика (он не растет неограниченно) и используется извне как определенный класс. Таким образом, это не значит, что у вас есть один богатый класс PERSON (с некоторым неопределенным графиком связанных объектов), который используется несколько вариантов использования или методов обслуживания; вместо этого у вас есть несколько богатых классов, PersonWithAddreses, PersonWithAllData... каждый из них обертывает определенный хорошо ограниченный граф с собственной логикой сохранения. Это может показаться неэффективным или неуклюжим, и в некотором смысле это может быть, но часто случается, что случаи использования, когда вам нужно сохранить полный график объектов, фактически ограничены.

  • Кроме того, для таких вещей, как табличные отчеты (специальные SELECTS, которые возвращают кучу столбцов, которые будут отображаться), вы не использовали бы вышеупомянутое, но прямое и немое POJO (возможно, даже Карты)

См. мой ответ здесь

Ответ 2

Ответ на ваши многочисленные вопросы прост. У вас есть три варианта.

  • Используйте один из трех упомянутых вами SQL-ориентированных инструментов (MyBatis, jOOQ, DbUtils). Это означает, что вам следует перестать думать с точки зрения вашей модели домена OO и объектно-реляционного сопоставления (т.е. Сущностей и ленивой загрузки). SQL относится к реляционным данным, и RBDMS неплохо вычисляют планы выполнения для "нетерпеливого получения" результата нескольких объединений. Как правило, нет необходимости в досрочном кэшировании, и если вам нужно кэшировать случайный элемент данных, вы все равно можете использовать что-то вроде EhCache

  • Не используйте ни один из этих инструментов, ориентированных на SQL, и придерживайтесь Hibernate/JPA. Потому что, даже если вы сказали, что вам не нравится Hibernate, вы думаете о Hibernate. Hibernate очень хорош в сохранении графов объектов в базе данных. Ни один из этих инструментов не может быть принудительно работать как Hibernate, потому что их миссия - это что-то еще. Их задача - работать с SQL.

  • Идите совсем по-другому и не используйте реляционную модель данных. Другие модели данных (например, графики) могут вам лучше подойти. Я помещаю это как третий вариант, потому что у вас может не быть такого выбора, и у меня нет большого личного опыта с альтернативными моделями.

Обратите внимание, что ваш вопрос касался не только jOOQ. Тем не менее, с помощью jOOQ вы можете визуализировать отображение результатов плоских запросов (полученных из объединенных источников таблицы) на графические объекты с помощью внешних инструментов, таких как ModelMapper. Там есть интересный поток о такой интеграции в ModelMapper User Group.

(отказ от ответственности: я работаю в компании за jOOQ)

Ответ 3

Подходы к сохранению

Спектр решений от простого/базового до сложного/богатого:

  • SQL/JDBC - жесткий код SQL внутри объектов
  • SQL-Based Framework (например, jOOQ, MyBatis) - шаблон активной записи (отдельный общий объект представляет данные строки и обрабатывает SQL)
  • ORM-Framework (например, Hibernate, EclipseLink, DataNucleus) - шаблон карты данных (объект на единицу) плюс элемент рабочего шаблона (менеджер контекста/сущность)

Вы пытаетесь реализовать один из первых двух уровней. Это означает смещение фокуса от объектной модели к SQL. Но ваш вопрос задает вопросы использования, связанные с отображением объектной модели на SQL (то есть поведение ORM). Вы хотите добавить функциональность с третьего уровня к функциональности с одного из первых двух уровней.

Мы могли бы попытаться реализовать это поведение в Active Record. Но для каждого экземпляра Active Record необходимы большие метаданные - фактический объект, связанный с другими объектами, параметры ленивой загрузки, настройки каскадного обновления. Это позволит эффективно скрывать объект сопоставленного объекта. Кроме того, jOOQ и MyBatis не делают этого для использования случаев 1 и 2.

Как достичь ваших запросов?

Внедрите узкое поведение ORM непосредственно в свои объекты, в качестве небольшого настраиваемого уровня поверх вашей структуры или необработанного SQL/JDBC.

Использовать случай 1: хранить метаданные для каждого отношения объекта объекта: (i) должно ли отношение lzy-load (класс-класс) и (ii) происходить ли lazy-load (уровень объекта). Затем в методе getter используйте эти флаги, чтобы определить, нужно ли выполнять ленивую загрузку и на самом деле делать это.

Вариант использования 2: Как использовать пример 1 - сделайте это самостоятельно. Храните грязный флаг внутри каждого объекта. Против каждого отношения объекта объекта храните флаг, описывающий необходимость каскадирования сохранения. Затем, когда объект сохраняется, рекурсивно посещайте каждое соотношение "сохранить каскад". Напишите все обнаруженные грязные объекты.

Шаблоны

Pros

  • Вызовы в платформу SQL просты.

против

  • Ваши объекты становятся более сложными. Взгляните на код для использования случаев 1 и 2 в продукте с открытым исходным кодом. Это не тривиально.
  • Отсутствие поддержки объектной модели. Если вы используете объектную модель в java для своего домена, она будет иметь меньшую поддержку операций с данными.
  • Риск возникновения ползучести и анти-шаблонов: недостающая функциональность - это вершина айсберга. В конце концов, я смогу сделать несколько Reinvent the Wheel и Infrastructure Bloat в бизнес-логике.
  • Обучение и техническое обслуживание нестандартного решения. JPA, JDBC и SQL являются стандартами. Другие рамки или пользовательские решения не являются.

Worthwhile???

Это решение работает хорошо, если у вас достаточно простые требования к обработке данных и модель данных с меньшим количеством объектов:

  • Если так, отлично! Сделайте выше.
  • Если это не так, это решение плохо подходит и представляет собой ложную экономию в усилиях - то есть в конечном итоге займет больше времени и будет сложнее, чем использование ORM. В этом случае еще раз взгляните на JPA - это может быть проще, чем вы думаете, и поддерживает ORM для CRUD plus raw SQL для сложных запросов: -).

Ответ 4

За последние десять лет я использовал JDBC, объект EJB beans, Hibernate, GORM и, наконец, JPA (в этом порядке). Для моего текущего проекта я вернулся к использованию простого JDBC, потому что акцент делается на производительности. Поэтому я хотел

  • Полный контроль над генерацией операторов SQL: возможность передать инструкцию в DB-тюнеры производительности и вернуть оптимизированную версию в программу
  • Полный контроль над количеством операторов SQL, которые отправляются в базу данных
  • Хранимые процедуры (триггеры), хранимые функции (для сложных вычислений в SQL-запросах)
  • Чтобы иметь возможность использовать все доступные функции SQL без ограничений (рекурсивные запросы с помощью CTE, функции агрегации окна,...)

Модель данных определена в словаре данных; используя подход, основанный на модели, генератор создает вспомогательные классы, сценарии DDL и т.д. Большинство операций в базе данных доступны только для чтения; только несколько случаев использования пишут.

Вопрос 1: Получение детей

Система построена на прецедентах, и у нас есть один выделенный оператор SQL для получения всех данных для данного варианта использования/запроса. Некоторые из статусов SQL больше 20kb, они объединяются, вычисляют с использованием хранимых функций, написанных на Java/ Scala, сортируют, paginate и т.д. Таким образом, что результат непосредственно отображается в объект передачи данных, который, в свою очередь, вид (без дальнейшей обработки на прикладном уровне). Как следствие, объект передачи данных также является конкретным вариантом использования. Он содержит только данные для данного варианта использования (не более, не что иное).

Поскольку набор результатов уже "полностью соединен", нет необходимости в ленивом/нетерпеливом извлечении и т.д. Объект передачи данных завершен. Кэш не нужен (кэш базы данных в порядке); исключение: если результирующий набор является большим (около 50 000 строк), объект передачи данных используется как значение кэша.

Вопрос 2: Сохранение

После того, как контроллер сменил изменения в графическом интерфейсе, снова появляется конкретный объект, который содержит данные: в основном строки с состоянием (новые, удаленные, измененные,...) в иерархии. Это ручная итерация для сохранения данных по иерархии: новые или измененные данные сохраняются с использованием некоторых вспомогательных классов с генерированием команд SQL insert или update. Что касается удаленных элементов, это оптимизируется на каскадные удаления (PostgreSql). Если несколько строк должны быть удалены, это оптимизируется в один оператор delete ... where id in ....

Снова это конкретный вариант использования, поэтому он не имеет отношения к общему утверждению. Ему нужны больше строк кода, но это строки, содержащие оптимизации.

Опыт до сих пор

  • Не стоит недооценивать усилия по изучению Hibernate или JPA. Следует учитывать время, затрачиваемое на настройку кэшей, недействительность кэша в кластере, нетерпеливое/ленивое извлечение и настройку. Переход на другую версию Hibernate - это не просто перекомпиляция.
  • Не следует переоценивать усилия по созданию приложения без ORM.
  • Проще использовать SQL напрямую - быть близким к SQL (например, HQL, JPQL) не то же самое, особенно если вы поговорите с вашим тюнером производительности DB
  • SQL-серверы невероятно быстрые при длительных и сложных запросах, особенно если они объединены с хранимыми функциями, написанными в Scala
  • Даже с конкретными инструкциями SQL для конкретного случая размер кода меньше: "Полностью соединенные" результирующие множества сохраняют множество строк на прикладном уровне.

Дополнительная информация:

Обновление: интерфейсы

Если в модели логических данных существует объект Person, существует класс для сохранения объекта Person (для операций CRUD), как и объект JPA.

Но clou состоит в том, что нет единственного объекта Person из перспективы использования/запроса/бизнес-логики: каждый метод службы определяет свое собственное понятие Person, и он содержит только те значения, которые требуются для этого использование случай. Ни больше ни меньше. Мы используем Scala, поэтому определение и использование многих небольших классов очень эффективно (не требуется код плитки для сеттера/геттера).

Пример:

class GlobalPersonUtils {
  public X doSomeProcessingOnAPerson(
    Person person, PersonAddress personAddress, PersonJob personJob, 
    Set<Person> personFriends, ...)
}

заменяется на

class Person {
  List addresses = ...
  public X doSomeProcessingOnAPerson(...)
}

class Dto {
  List persons = ...
  public X doSomeProcessingOnAllPersons()
  public List getPersons()
}

с использованием конкретного случая использования Persons, Adresses и т.д.: В этом случае Person уже агрегирует все релевантные данные. Требуется больше классов, но нет необходимости передавать объекты JPA.

Обратите внимание, что эта обработка доступна только для чтения, результаты используются представлением. Пример. Получить отдельные экземпляры City из списка лиц.

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

Ответ 5

Предупреждение: Это еще один бесстыдный плагин от автора проекта.

Отметьте JSimpleDB и проверьте, соответствует ли он вашим критериям простоты и мощности. Это новый проект, родившийся в результате 10-летнего разочарования, который пытается справиться с сохранением Java через SQL и ORM.

Он работает поверх любой базы данных, которая может функционировать как хранилище ключей/значений, например FoundationDB (или любая база данных SQL, хотя все будет зажато в одной таблице ключей/значений).

Ответ 6

Похоже, основная проблема в этом вопросе связана с самой реляционной моделью. Из того, что было описано, база данных графиков будет очень точно отображать проблемную область. В качестве альтернативы хранилища документов - еще один способ приблизиться к проблеме, потому что, хотя импеданс все еще существует, документы в целом проще рассуждать о множестве. Конечно, любой подход будет иметь свои собственные особенности.

Ответ 7

Поскольку вам нужна простая и легкая библиотека и использовать SQL, я могу предложить взглянуть на fjorm. Это позволяет использовать операции POJO и CRUD без особых усилий.

Отказ от ответственности: я являюсь автором проекта.