Spring Data vs Micronaut Data vs Jakarta Data: відмінності в сучасних технологіях доступу до даних
Усім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник, хочу поділитися з вами досвідом роботи з системами доступу до даних. Я вже писав про проєкти Spring Data в цій та цiй статтях. Тут же я хочу, по-перше, узагальнити і систематизувати всю інформацію станом на
Сподіваюся, що ця стаття буде корисною для всіх, хто хоче дізнатися відмінності в сучасних технологіях доступу до даних.
Spring Data
Довгі роки Hibernate, а потім JPA, були головним способом для роботи з реляційними базами даних для тих, хто хотів використовувати Java та ORM технології. Поява, а потім бурхливий розвиток NoSQL-рішень, наштовхнув Spring-інженерів на думку, що потрібен новий, більш інноваційний підхід для доступу до будь-яких джерел даних. Вони вибрали за основу патерн Репозиторій, який інкапсулював роботу з базою даних і незалежно від фізичного пристрою та API цієї БД дозволяв працювати з нею, як зі звичайною колекцією. Коли в 2008 році проєкт Spring Data стартував, то в ньому презентували фреймворк тільки для JPA додатків — Spring Data JPA. Зараз же складніше назвати популярну базу даних, для якої немає інтеграції в Spring Data. Більше того, потім з’явилися вже не інтеграції, а самостійні проєкти:
- Spring Data REST — інтеграція із Spring MVC
- Spring Data JDBC — ORM на базі чистого JDBC
- Spring Data R2DBC — рішення на базі реактивних драйверів до БД (стандартні JDBC-драйвери пропонують синхронний блокуючий підхід)
Робота зі Spring Data починається з того, що ви, маючи готову модель даних, створюєте для кожної сутності інтерфейс, який розширює один із вбудованих (CrudRepository, наприклад)::
public interface ProductRepository extends CrudRepository<Product, Integer> {
Після цього, якщо ви використовуєте Spring Boot, він при старті автоматично знайде всі подібні інтерфейс і на льоту згенерує реалізацію за допомогою JDK проксі. Таким чином, ви можете впровадити цей інтерфейс як Spring бін і одразу виконувати:
- Будь-які CRUD операції
- Offset-based pagination, якщо в якості базового інтерфейсу вибрати PagingAndSort-ingRepository
Але це ще не все. Однією з вимог під час роботи з JPA (Hibernate) було знання JPQL (SQL). Тепер це стало опціональним, тому що у цих інтерфейсах можна писати query methods:
Streamable<Product> findByPrice(double price);
List<Product> findByNameAndPriceAndActiveTrue(String name, double price);
Spring Data при завантаженні контексту автоматично розпарсує назву методу і згенерує відповідний JPQL запит. Якщо ж це зробити складно, або назва вийде занадто довгою, то можна все-таки вказати JPQL запит окремо:
@Query("SELECT count(id) > 0 FROM Product p WHERE p.id=:id")
boolean exists(@Param("id") int id);
Але такий запит може бути статичним, тобто генеруватися при завантаженні додатку. Якщо ж у вас був динамічний пошук, то доводилося використовувати API від JPA Criteria, що було громіздким. І нарешті в Spring Data 2.x з’явилися Query By Example, коли ви на льоту створюєте так званий probe, який потім перетворюється на умови для фільтрації.:
Product product = new Product();
product.setPrice(200);
Example<Product> example = Example.of(product);
List<Product> items = productRepository.findAll(example);
Такий спосіб мав деякі обмеження (не всі операції порівняння підтримувалися), але був відчутним кроком вперед. Також було дуже зручно працювати з проекціями сутностей. Можна було створити проекцію як окремий клас/інтерфейс:
public interface ProductDetail {
Integer getId();
String getName();
}
І вказати її для пошуку, а Spring Data автоматично конвертує результати запиту не в Product, а в ProductDetail:
List<ProductDetail> find();
У Spring Data 3.x до всіх його численних можливостей додалася підтримка keyset-based pagination:
Window<Product> products = productRepository.findAllBy(
ScrollPosition.forward(Map.of("id", id,
"createdAt", LocalDateTime.now())));
І це при тому, що Spring інженерам доводилося реалізовувати все перераховане для кожної з підтримуваних БД (MongoDB, Redis, Cassandra, і т.д.). Але в роботі з NoSQL технологіями є одна важлива відмінність. Spring Data JPA заснована на JPA, тому ви в будь-який Spring бін можете впровадити EntityManager для роботи безпосередньо з JPA/JPQL:
@PersistenceContext
private EntityManager em;
Для NoSQL технологій загальної специфікації немає. Тому інженери Spring для кожної з них додали спеціальний бін-обгортку *-Template, який підтримує доступ до Java клієнта для бази даних. Наприклад, для Spring Data MongoDB це MongoTemplate:
@RequiredArgsConstructor
@Service
public class ProductService {
private final MongoTemplate mongoTemplate;
public Double maxPrice() {
GroupOperation groupOp = Aggregation.group().max("price").as("maxPrice");
ProjectionOperation projection = Aggregation.project("maxPrice").andExclude("_id");
Aggregation aggregation = Aggregation.newAggregation(groupOp, projection);
Document doc = mongoTemplate.aggregate(aggregation, "products", Document.class).getUniqueMappedResult();
return doc.getDouble("maxPrice");
}
Micronaut Data
Проєкт Micronaut Data стартував у
В цілому Micronaut Data концептуально схожий на Spring Data, але відрізняється тим самим, чим і Spring від Micronaut:
- Не використовується Reflection API або проксі
- Вся підготовка до роботи та генерація додаткового коду здійснюється під час компіляції. А це дозволяє знаходити ті помилки, які в Spring Data виявляються лише під час виконання
На цей момент Micronaut Data підтримує:
- JPA
- Hibernate Reactive
- Чистий SQL (Micronaut Data JDBC і R2DBC)
- MongoDB
- Azure Cosmos
Як ви бачите, цей список не такий значний, як у Spring Data. З іншого боку, Spring Data не має підтримки Azure Cosmos і Hibernate Reactive. В іншому, Hibernate Reactive є прямим конкурентом їх Spring Data R2DBC, і хоча Java-спільнота просить про таку інтеграцію, але поки отримує категоричні відповіді.
Для того, щоб розпочати роботу, потрібно вже мати сутності (наприклад, з JPA анотаціями) і додати для них інтерфейс-репозиторій:
@Repository
public interface ProductRepository extends CrudRepository<Product, Integer> {
}
Але анотація @Repository не просто позначає цей інтерфейс як той, що відноситься до Micronaut Data, але і дозволяє вказати джерело даних, якщо у вас, наприклад, кілька баз даних. Наприклад, якщо у вас є такий datasource в налаштуваннях:
datasources:
local:
url: jdbc:h2:mem:devDb
driverClassName: org.h2.Driver
username: sa
password: ''
schema-generate: CREATE_DROP
dialect: H2
Його ідентифікатор можна явно вказати в @Repository:
@Repository("local")
public interface ProductRepository extends CrudRepository<Product, Integer> {
}
Це приємне доповнення до основної функціональності, тому що в Spring Data JPA можна досягти того ж самого, але набагато більшою кількістю коду.
Ієрархія інтерфейсів тут приблизно така сама, як і в Spring Data, тільки замість Repository — GenericRepository. Є інтерфейси і для реактивних операцій, і навіть підтримка Kotlin із його coroutines.
Як і в Spring Data підтримуються query methods:
@Repository
public interface ProductRepository extends CrudRepository<Product, Integer> {
Optional<Product> findByName(String name);
}
Тільки JPQL запит буде згенеровано на стадії компіляції. І якщо відкрити згенерований клас $ProductRepository$Intercepted$Definition$Exec, то там в метаданих цей запит можна легко знайти:
"SELECT product_ FROM model.Product AS product_ WHERE (product_.name = :p1)")
Більш того, у Micronaut Data можна в назві методу явно вказати поле для вибірки:
Optional<String> findNameByCategory(String category);
У Spring Data при цьому доводиться створювати окремий тип-DTO. Також приємно порадувало можливість вказати прямо в репозиторії за допомогою анотації @Valid, що ваші сутності повинні перевірятися перед записом в базу даних:
@Repository
public interface ProductRepository extends CrudRepository<@Valid Product, Integer> {
Іноді потрібно додати певний фільтр до всіх запитів, які генеруються Micronaut Data, але не хочеться переробляти кожен запит. Натомість можна просто навісити інструкцію @Where, як на сутність:
@Table
@Entity
@Where("@.active=true")
public class Product {
І тоді до кожного SQL запит на товари буде додано фільтр (active = true). Якщо такий фільтр потрібен лише для певних запитів, то можна його вказати там:
@Where("@.active=true")
Optional<String> findNameByCategory(String category);
Для запису даних можна використовувати стандартні операції CRUD:
@Override
Product save(Product product);
А можна створити метод persist, в якому вказати всі поля сутності, у яких дефолтних значень:
Product persist(String name, String category);
І тоді Micronaut сам створить об’єкт Product та заповнить його полями. Цікава річ щодо транзакцій. За промовчанням Micronaut Data обертає у транзакцію всі методи з ваших репозиторіїв, і відключити це глобально неможливо. Доводиться для кожного репозиторію вказувати анотацію @Transactional і спеціально вимикати транзакції, де вони не потрібні:
@MongoRepository
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public interface PaymentRepository extends CrudRepository<Payment, ObjectId>{
На жаль, тут немає такої зручної фітчі як Query By Example, тому для динамічних запитів доведеться використовувати JPA Criteria:
@Transactional
default List<Product> findByNameOrCategory(String name, String category) {
Specification<Product> specification = (root, query, criteriaBuilder)
-> {
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.isNotEmpty(name)) {
predicates.add(criteriaBuilder.equal(root.get("name"), name));
}
if (StringUtils.isNotEmpty(category)) {
predicates.add(criteriaBuilder.equal(root.get("category"), category));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
};
return findAll(specification);
}
Jakarta Data
Enterprise Java (Java EE) і її розробників можна дорікнути в тому, що вони недостатньо уваги приділяють ORM технологіям. Коли в 2006 році з’явилася JPA 1.0, Hibernate було вже 6 років, і JPA просто виділила його основну функціональність (але не всю) у вигляді API та анотацій. Коли в 2008 році з’явився проєкт Spring Data, Java EE взагалі на це ніяк не відреагувала. І тільки в
Використовувати Jakarta Data можна як для реляційних, так і для NoSQL. Відмінність буде лише в тому, які інструкції будуть навішані для ваших сутностей.:
- Для реляційних — з JPA
- Для NoSQL — з Jakarta NoSQL (теж вийде як частина Jakarta EE 11)
Втім, розглядається варіант і annotation-free. Тоді будуть використані дефолтні настройки, а ідентифікатор — поле з назвою id або закінчується на id.
Тут також є своя ієрархія репозиторіїв, тільки замість Repository — DataRepository, а також BasicRepository — щось середнє між Repository та CrudRepository.
Для початку роботи потрібно лише створити свій інтерфейс для кожної сутності:
@Repository
public interface ProductRepository extends CrudRepository<Product, Integer> {
}
Головна відмінність від Spring Data — необхідність в анотації @Repository, яка перетворює реалізацію інтерфейсу на
@Repository(provider = "hibernate")
public interface ProductRepository extends CrudRepository<Product, Integer> {
Тут можна також писати query methods:
public interface ProductRepository extends CrudRepository<Product, Integer> {
List<Product> findByCategory(String category);
Приємне доповнення — спеціальна інструкція @OrderBy для сортування результату:
@OrderBy("price")
List<Product> findByCategory(String category);
Втім, її можна використовувати для статичного сортування. Коли поля сортування можуть змінюватись, також можна вказати аргумент Sort:
List<Product> findByCategory(String category, Sort sort);
Якщо потрібно вказати запит, то є анотація @Query:
@Query("WHERE category = :category")
List<Product> findByCategory(@Param("category") String category);
Так як для Jakarta EE 11 мінімальна версія Java — JDK 17, то вже можна помітити якісь технологічні нововведення, які були неможливі в часи створення Spring Data:
public record Limit(int maxResults, long startAt) {
Але в Jakarta Data є така функціональність, яка нам і не снилася у Spring Data. Справа в тому, що стандартний CrudRepository заточений на операції CRUD, і для збереження завжди потрібно викликати save(), а для видалення — deleteById. Це не завжди узгоджується з мовою нашої доменної моделі. І в Jakarta Data можна взагалі не використовувати базові репозиторії.:
@Repository
public interface OrderRepository {
@Insert
void apply(Order order);
@Delete
void cancel(Order order);
}
Це все, тому що тут є підтримка так званих lifecycle методів, для яких є спеціальні анотації — @ Insert, @ Update, @ Delete і @ Save. Єдине, чого поки бракує Jakarta Data — динамічного створення запитів (як Query By Example у Spring Data).
Після всього прочитаного постає питання. А чому Jakarta Data просто не включили до Jakarta Persistence (JPA)? Відповідь досить тривіальна — просто, тому що всім JPA провайдерам довелося б реалізовувати цю специфікацію, а не у всіх є можливості для цього. Крім того, Jakarta Data, як уже було сказано, може бути використана не тільки для реляційних баз даних.
Загалом ця технологія сприймається досить оптимістично. Жаль тільки, що вона запізнилася за часом років на 10.
Висновки
Проєкт Spring Data розвивається вже 15 років, і поки що у нього немає конкурентів за кількістю можливостей та підтримуваних БД. У той же час його відносні недоліки — це загальна для всіх Spring-проєктів магія — проксі, Reflection API, auto-configuration і т.д.
Проєкту Micronaut Data всього кілька років, але він вже практично наздогнав Spring Data щодо функціональності. Головне технологічне відставання — кількість підтримуваних баз даних. З іншого боку Micronaut Data, як і Micronaut, не використовує магію у своїй роботі, все підготовча частина відбувається під час компіляції. І, зрозуміло, це впливає на час завантаження додатка та ефективність роботи.
Jakarta Data — це єдина специфікація із усіх розглянутих. І, строго кажучи, вона і повинна була з’явитися першою, щоб стати фундаментом для реалізації (як JPA або JAX-RS). На жаль, цього не сталося. Але принаймні, тепер розробники, які використовують Enterprise Java, отримають технологію для більш простої роботи з джерелами даних. Єдине питання, яке досі відкрите, — наскільки швидко напишуть для неї реалізацію і які саме бази даних будуть підтримуватися. Зараз йдеться лише про реляційні дані та JPA. Поки що доступна бета-версія Jakarta Data, а це означає, що їй API і функціональність можуть змінитися до виходу Jakarta EE 11.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів