MS Lesson 9: JPA, Hikari Connection Pool

ORM

ORM nədir?

ORM (Object–Relational Mapping) — verilənlər bazasındakı cədvəl sətirlərini proqramda obyektlərlə uyğunlaşdırmaq üçün üsuldur.

  • Verilənlər bazası: cədvəllər, sətirlər, xarici açarlar.
  • Java kodu: class-lar, obyektlər, əlaqələr.

ORM deyir ki: “Bu User class-ı users cədvəlinə uyğun gəlsin; sətir oxuyanda obyekt əldə edim, obyekti saxlayanda INSERT/UPDATE edim.” Yəni SQL-i əl ilə yazmaq əvəzinə çox vaxt obyektlərlə işləyirsən (amma ORM altında yenə SQL işləyir).


JPA

JPA nədir?

JPA (Jakarta Persistence API) — Java üçün standart spesifikasiyadır (interfeyslər, annotasiyalar, qaydalar). O, “obyekti necə saxlamaq, tapmaq, silməyi” tərif edir, amma özü konkret motor deyil.

  • @Entity@Id@OneToMany və s. — JPA dünyasından gəlir.
  • JPA deyir: “Belə API olmalıdır”, amma işləməsi üçün təminatçı (provider) lazımdır.

Qısa: JPA = qayda kitabı / interfeys, tək başına verilənlər bazasına qoşulmur.


Hibernate

Hibernate nədir?

Hibernate — JPA spesifikasiyasını həyata keçirən ən məşhur təminatçılardan biridir (ORM framework / implementasiya).

  • Sənin kodunda JPA annotasiyaları var.
  • Runtime-da Hibernate bu annotasiyaları oxuyur, SQL yaradır, connection idarə edir və s.

Qısa: Hibernate = JPA-nı icra edən konkret alət (bundan əlavə özünün də API-ləri və xüsusiyyətləri var).



Spring default olaraq SimpleJpaRepository ishledir:


/*
* Copyright 2008-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Tuple;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.stream.Stream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.query.*;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.QueryCreationListener;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor;
import org.springframework.data.repository.query.CachingValueExpressionDelegate;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import com.querydsl.core.types.EntityPath;

/**
* JPA specific generic repository factory.
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
* @author Jens Schauder
* @author Stefan Fussenegger
* @author Réda Housni Alaoui
* @author Gabriel Basilio
* @author Greg Turnquist
*/
public class JpaRepositoryFactory extends RepositoryFactorySupport {

private final EntityManager entityManager;
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor;
private final CrudMethodMetadata crudMethodMetadata;

private EntityPathResolver entityPathResolver;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;
private JpaRepositoryFragmentsContributor fragmentsContributor = JpaRepositoryFragmentsContributor.DEFAULT;
private QueryEnhancerSelector queryEnhancerSelector = QueryEnhancerSelector.DEFAULT_SELECTOR;
private JpaQueryMethodFactory queryMethodFactory;
private QueryRewriterProvider queryRewriterProvider;

/**
* Creates a new {@link JpaRepositoryFactory}.
*
* @param entityManager must not be {@literal null}
*/
public JpaRepositoryFactory(EntityManager entityManager) {

Assert.notNull(entityManager, "EntityManager must not be null");

this.entityManager = entityManager;
PersistenceProvider extractor = PersistenceProvider.fromEntityManager(entityManager);
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
this.entityPathResolver = SimpleEntityPathResolver.INSTANCE;
this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);
this.queryRewriterProvider = QueryRewriterProvider.simple();

addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
addRepositoryProxyPostProcessor((factory, repositoryInformation) -> {

if (isTransactionNeeded(repositoryInformation.getRepositoryInterface())) {
factory.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE);
}
});

if (extractor.equals(PersistenceProvider.ECLIPSELINK)) {
addQueryCreationListener(new EclipseLinkProjectionQueryCreationListener(entityManager));
}

this.crudMethodMetadata = crudMethodMetadataPostProcessor.getCrudMethodMetadata();
}

@Override
public void setBeanClassLoader(@Nullable ClassLoader classLoader) {

super.setBeanClassLoader(classLoader);
this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader);
}

/**
* If a {@link BeanFactory} is being set, this is clearly in a Spring context, and so we can capture the
* {@link QueryRewriterProvider} being a {@link BeanFactoryQueryRewriterProvider}.
*
* @param beanFactory
* @throws BeansException
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {

super.setBeanFactory(beanFactory);

Assert.notNull(beanFactory, "BeanFactory must not be null");

setQueryRewriterProvider(new BeanFactoryQueryRewriterProvider(beanFactory));
}

/**
* Configures the {@link EntityPathResolver} to be used. Defaults to {@link SimpleEntityPathResolver#INSTANCE}.
*
* @param entityPathResolver must not be {@literal null}.
*/
public void setEntityPathResolver(EntityPathResolver entityPathResolver) {

Assert.notNull(entityPathResolver, "EntityPathResolver must not be null");

this.entityPathResolver = entityPathResolver;
}

/**
* Configures the escape character to be used for like-expressions created for derived queries.
*
* @param escapeCharacter a character used for escaping in certain like expressions.
*/
public void setEscapeCharacter(EscapeCharacter escapeCharacter) {
this.escapeCharacter = escapeCharacter;
}

/**
* Configures the {@link JpaRepositoryFragmentsContributor} to be used. Defaults to
* {@link JpaRepositoryFragmentsContributor#DEFAULT}.
*
* @param fragmentsContributor
* @since 4.0
*/
public void setFragmentsContributor(JpaRepositoryFragmentsContributor fragmentsContributor) {
this.fragmentsContributor = fragmentsContributor;
}

/**
* Configures the {@link JpaQueryMethodFactory} to be used. Defaults to {@link DefaultJpaQueryMethodFactory}.
*
* @param queryMethodFactory must not be {@literal null}.
*/
public void setQueryMethodFactory(JpaQueryMethodFactory queryMethodFactory) {

Assert.notNull(queryMethodFactory, "QueryMethodFactory must not be null");

this.queryMethodFactory = queryMethodFactory;
}

/**
* Configures the {@link QueryEnhancerSelector} to be used. Defaults to
* {@link QueryEnhancerSelector#DEFAULT_SELECTOR}.
*
* @param queryEnhancerSelector must not be {@literal null}.
*/
public void setQueryEnhancerSelector(QueryEnhancerSelector queryEnhancerSelector) {

Assert.notNull(queryEnhancerSelector, "QueryEnhancerSelector must not be null");

this.queryEnhancerSelector = queryEnhancerSelector;
}

/**
* Configures the {@link QueryRewriterProvider} to be used. Defaults to instantiate query rewriters through
* {@link BeanUtils#instantiateClass(Class)}.
*
* @param queryRewriterProvider must not be {@literal null}.
* @since 3.0
*/
public void setQueryRewriterProvider(QueryRewriterProvider queryRewriterProvider) {

Assert.notNull(queryRewriterProvider, "QueryRewriterProvider must not be null");
this.queryRewriterProvider = queryRewriterProvider;
}

@Override
protected final JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information) {

JpaRepositoryImplementation<?, ?> repository = getTargetRepository(information, entityManager);

invokeAwareMethods(repository);

return repository;
}

/**
* Callback to create a {@link JpaRepository} instance with the given {@link EntityManager}
*
* @param information will never be {@literal null}.
* @param entityManager will never be {@literal null}.
* @return
*/
protected JpaRepositoryImplementation<?, ?> getTargetRepository(RepositoryInformation information,
EntityManager entityManager) {

JpaEntityInformation<?, Serializable> entityInformation = getEntityInformation(information.getDomainType());
Object repository = getTargetRepositoryViaReflection(information, entityInformation, entityManager);

Assert.isInstanceOf(JpaRepositoryImplementation.class, repository);

return (JpaRepositoryImplementation<?, ?>) repository;
}

@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return SimpleJpaRepository.class;
}

@Override
protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader,
@Nullable BeanFactory beanFactory) {

CollectionAwareProjectionFactory factory = new CollectionAwareProjectionFactory();
if (classLoader != null) {
factory.setBeanClassLoader(classLoader);
}
if (beanFactory != null) {
factory.setBeanFactory(beanFactory);
}

return factory;
}

@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
ValueExpressionDelegate valueExpressionDelegate) {

JpaQueryConfiguration queryConfiguration = new JpaQueryConfiguration(queryRewriterProvider, queryEnhancerSelector,
new CachingValueExpressionDelegate(valueExpressionDelegate), escapeCharacter);

return Optional.of(JpaQueryLookupStrategy.create(entityManager, queryMethodFactory, key, queryConfiguration));
}

@Override
@SuppressWarnings("unchecked")
public <T, ID> JpaEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return (JpaEntityInformation<T, ID>) JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
}

@Override
public EntityInformation<?, ?> getEntityInformation(RepositoryMetadata metadata) {
return JpaEntityInformationSupport.getEntityInformation(metadata.getDomainType(), entityManager);
}

@Override
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
return getRepositoryFragments(metadata, entityManager, entityPathResolver, this.crudMethodMetadata);
}

/**
* Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add JPA-specific extensions. Typically,
* adds a {@link QuerydslJpaPredicateExecutor} if the repository interface uses Querydsl.
* <p>
* Built-in fragment contribution can be customized by configuring {@link JpaRepositoryFragmentsContributor}.
*
* @param metadata repository metadata.
* @param entityManager the entity manager.
* @param resolver resolver to translate a plain domain class into a {@link EntityPath}.
* @param crudMethodMetadata metadata about the invoked CRUD methods.
* @return {@link RepositoryFragments} to be added to the repository.
* @since 2.5.1
*/
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, EntityManager entityManager,
EntityPathResolver resolver, CrudMethodMetadata crudMethodMetadata) {

RepositoryFragments fragments = this.fragmentsContributor.contribute(metadata,
getEntityInformation(metadata.getDomainType()), entityManager, resolver);

for (RepositoryFragment<?> fragment : fragments) {
fragment.getImplementation().filter(JpaRepositoryConfigurationAware.class::isInstance)
.ifPresent(it -> invokeAwareMethods((JpaRepositoryConfigurationAware) it));
}

return fragments;
}

private void invokeAwareMethods(JpaRepositoryConfigurationAware repository) {

repository.setRepositoryMethodMetadata(crudMethodMetadata);
repository.setEscapeCharacter(escapeCharacter);
repository.setProjectionFactory(getProjectionFactory());
}

private static boolean isTransactionNeeded(Class<?> repositoryClass) {

Method[] methods = ReflectionUtils.getAllDeclaredMethods(repositoryClass);

for (Method method : methods) {
if (Stream.class.isAssignableFrom(method.getReturnType()) || method.isAnnotationPresent(Procedure.class)) {
return true;
}
}

return false;
}

/**
* Query creation listener that informs EclipseLink users that they have to be extra careful when defining repository
* query methods using projections as we have to rely on the declaration order of the accessors in projection
* interfaces matching the order in columns. Alias-based mapping doesn't work with EclipseLink as it doesn't support
* {@link Tuple} based queries yet.
*
* @author Oliver Gierke
* @since 2.0.5
* @see <a href=
* "https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141">https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141</a>
*/
private static class EclipseLinkProjectionQueryCreationListener implements QueryCreationListener<AbstractJpaQuery> {

private static final String ECLIPSELINK_PROJECTIONS = "Usage of Spring Data projections detected on persistence provider EclipseLink; Make sure the following query methods declare result columns in exactly the order the accessors are declared in the projecting interface or the order of parameters for DTOs:";

private static final Log log = LogFactory.getLog(EclipseLinkProjectionQueryCreationListener.class);

private final JpaMetamodel metamodel;

private boolean warningLogged = false;

/**
* Creates a new {@link EclipseLinkProjectionQueryCreationListener} for the given {@link EntityManager}.
*
* @param em must not be {@literal null}.
*/
public EclipseLinkProjectionQueryCreationListener(EntityManager em) {

Assert.notNull(em, "EntityManager must not be null");

this.metamodel = JpaMetamodel.of(em.getMetamodel());
}

@Override
public void onCreation(AbstractJpaQuery query) {

JpaQueryMethod queryMethod = query.getQueryMethod();
ReturnedType type = queryMethod.getResultProcessor().getReturnedType();

if (type.isProjecting() && !metamodel.isJpaManaged(type.getReturnedType())) {

if (!warningLogged) {
log.info(ECLIPSELINK_PROJECTIONS);
this.warningLogged = true;
}

log.info(String.format(" - %s", queryMethod));
}
}
}
}


SimpleJpaRepository EntityManager interface muraciet edir, layihede Hibernate JPA provider-dirse bu implementasiya adeten Hibernate terefinden verilir.



JpaRepository-de butun metodlar @Transactional-dir.

/*
* Copyright 2008-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.support;

import static org.springframework.data.jpa.repository.query.QueryUtils.*;

import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Selection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.jspecify.annotations.Nullable;

import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.core.PropertyPath;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
import org.springframework.data.jpa.domain.DeleteSpecification;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.domain.UpdateSpecification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.KeysetScrollDelegate;
import org.springframework.data.jpa.repository.query.KeysetScrollSpecification;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.support.FetchableFluentQueryBySpecification.SpecificationScrollDelegate;
import org.springframework.data.jpa.repository.support.FluentQuerySupport.ScrollQueryFactory;
import org.springframework.data.jpa.repository.support.QueryHints.NoHints;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ProxyUtils;
import org.springframework.data.util.Streamable;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

/**
* Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer
* you a more sophisticated interface than the plain {@link EntityManager} .
*
* @param <T> the type of the entity to handle
* @param <ID> the type of the entity's identifier
* @author Oliver Gierke
* @author Eberhard Wolff
* @author Thomas Darimont
* @author Mark Paluch
* @author Christoph Strobl
* @author Stefan Fussenegger
* @author Jens Schauder
* @author David Madden
* @author Moritz Becker
* @author Sander Krabbenborg
* @author Jesse Wouters
* @author Greg Turnquist
* @author Yanming Zhou
* @author Ernst-Jan van der Laan
* @author Diego Krupitza
* @author Seol-JY
* @author Joshua Chen
* @author Giheon Do
*/
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null";
private static final String IDS_MUST_NOT_BE_NULL = "Ids must not be null";
private static final String ENTITY_MUST_NOT_BE_NULL = "Entity must not be null";
private static final String ENTITIES_MUST_NOT_BE_NULL = "Entities must not be null";
private static final String EXAMPLE_MUST_NOT_BE_NULL = "Example must not be null";
private static final String SPECIFICATION_MUST_NOT_BE_NULL = "Specification must not be null";
private static final String QUERY_FUNCTION_MUST_NOT_BE_NULL = "Query function must not be null";

private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager entityManager;
private final PersistenceProvider provider;

private final Lazy<String> deleteAllQueryString;
private final Lazy<String> countQueryString;

private @Nullable CrudMethodMetadata metadata;
private ProjectionFactory projectionFactory;
private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;

/**
* Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}.
*
* @param entityInformation must not be {@literal null}.
* @param entityManager must not be {@literal null}.
*/
public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {

Assert.notNull(entityInformation, "JpaEntityInformation must not be null");
Assert.notNull(entityManager, "EntityManager must not be null");

this.entityInformation = entityInformation;
this.entityManager = entityManager;
this.provider = PersistenceProvider.fromEntityManager(entityManager);
this.projectionFactory = new SpelAwareProxyProjectionFactory();

this.deleteAllQueryString = Lazy
.of(() -> getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()));
this.countQueryString = Lazy
.of(() -> getQueryString(String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s"),
entityInformation.getEntityName()));
}

/**
* Creates a new {@link SimpleJpaRepository} to manage objects of the given domain type.
*
* @param domainClass must not be {@literal null}.
* @param entityManager must not be {@literal null}.
*/
public SimpleJpaRepository(Class<T> domainClass, EntityManager entityManager) {
this(JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager), entityManager);
}

/**
* Configures a custom {@link CrudMethodMetadata} to be used to detect {@link LockModeType}s and query hints to be
* applied to queries.
*
* @param metadata custom {@link CrudMethodMetadata} to be used, can be {@literal null}.
*/
@Override
public void setRepositoryMethodMetadata(CrudMethodMetadata metadata) {
this.metadata = metadata;
}

@Override
public void setEscapeCharacter(EscapeCharacter escapeCharacter) {
this.escapeCharacter = escapeCharacter;
}

@Override
public void setProjectionFactory(ProjectionFactory projectionFactory) {
this.projectionFactory = projectionFactory;
}

protected @Nullable CrudMethodMetadata getRepositoryMethodMetadata() {
return metadata;
}

protected Class<T> getDomainClass() {
return entityInformation.getJavaType();
}

@Override
@Transactional
public void deleteById(ID id) {

Assert.notNull(id, ID_MUST_NOT_BE_NULL);

findById(id).ifPresent(this::delete);
}

@Override
@Transactional
public void delete(T entity) {

Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);

doDelete(entityManager, entityInformation, entity);
}

static <T> boolean doDelete(EntityManager entityManager, JpaEntityInformation<T, ?> entityInformation, T entity) {

if (entityInformation.isNew(entity)) {
return false;
}

if (entityManager.contains(entity)) {
entityManager.remove(entity);
return true;
}

Class<?> type = ProxyUtils.getUserClass(entity);

// if the entity to be deleted doesn't exist, delete is a NOOP
T existing = (T) entityManager.find(type, entityInformation.getId(entity));
if (existing != null) {
entityManager.remove(entityManager.merge(entity));

return true;
}

return false;
}

@Override
@Transactional
public void deleteAllById(Iterable<? extends ID> ids) {

Assert.notNull(ids, IDS_MUST_NOT_BE_NULL);

for (ID id : ids) {
deleteById(id);
}
}

@Override
@Transactional
public void deleteAllByIdInBatch(Iterable<ID> ids) {

Assert.notNull(ids, IDS_MUST_NOT_BE_NULL);

if (!ids.iterator().hasNext()) {
return;
}

if (entityInformation.hasCompositeId()) {

List<T> entities = new ArrayList<>();
// generate entity (proxies) without accessing the database.
ids.forEach(id -> entities.add(getReferenceById(id)));
deleteAllInBatch(entities);
} else {

String queryString = String.format(DELETE_ALL_QUERY_BY_ID_STRING, entityInformation.getEntityName(),
entityInformation.getRequiredIdAttribute().getName());

Query query = entityManager.createQuery(queryString);

/*
* Some JPA providers require {@code ids} to be a {@link Collection} so we must convert if it's not already.
*/
Collection<ID> idCollection = toCollection(ids);
query.setParameter("ids", idCollection);

applyQueryHints(query);

query.executeUpdate();
}
}

@Override
@Transactional
public void deleteAll(Iterable<? extends T> entities) {

Assert.notNull(entities, ENTITIES_MUST_NOT_BE_NULL);

for (T entity : entities) {
delete(entity);
}
}

@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {

Assert.notNull(entities, ENTITIES_MUST_NOT_BE_NULL);

if (!entities.iterator().hasNext()) {
return;
}

applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, entityManager)
.executeUpdate();
}

@Override
@Transactional
public void deleteAll() {

for (T element : findAll()) {
delete(element);
}
}

@Override
@Transactional
public void deleteAllInBatch() {

Query query = entityManager.createQuery(deleteAllQueryString.get());

applyQueryHints(query);

query.executeUpdate();
}

@Override
public Optional<T> findById(ID id) {

Assert.notNull(id, ID_MUST_NOT_BE_NULL);

Class<T> domainType = getDomainClass();

if (metadata == null) {
return Optional.ofNullable(entityManager.find(domainType, id));
}

LockModeType type = metadata.getLockModeType();
Map<String, Object> hints = getHints();

return Optional.ofNullable(
type == null ? entityManager.find(domainType, id, hints) : entityManager.find(domainType, id, type, hints));
}

@Deprecated
@Override
public T getOne(ID id) {
return getReferenceById(id);
}

@Deprecated
@Override
public T getById(ID id) {
return getReferenceById(id);
}

/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.JpaRepository#getReferenceById(java.io.Serializable)
*/
@Override
public T getReferenceById(ID id) {

Assert.notNull(id, ID_MUST_NOT_BE_NULL);
return entityManager.getReference(getDomainClass(), id);
}

@Override
public boolean existsById(ID id) {

Assert.notNull(id, ID_MUST_NOT_BE_NULL);

if (entityInformation.getIdAttribute() == null) {
return findById(id).isPresent();
}

String placeholder = provider.getCountQueryPlaceholder();
String entityName = entityInformation.getEntityName();
Iterable<String> idAttributeNames = entityInformation.getIdAttributeNames();
String existsQuery = QueryUtils.getExistsQueryString(entityName, placeholder, idAttributeNames);

TypedQuery<Long> query = entityManager.createQuery(existsQuery, Long.class);

applyQueryHints(query);

if (!entityInformation.hasCompositeId()) {
query.setParameter(idAttributeNames.iterator().next(), id);
return query.getSingleResult() == 1L;
}

for (String idAttributeName : idAttributeNames) {

Object idAttributeValue = entityInformation.getCompositeIdAttributeValue(id, idAttributeName);

boolean complexIdParameterValueDiscovered = idAttributeValue != null
&& !query.getParameter(idAttributeName).getParameterType().isAssignableFrom(idAttributeValue.getClass());

if (complexIdParameterValueDiscovered) {

// fall-back to findById(id) which does the proper mapping for the parameter.
return findById(id).isPresent();
}

query.setParameter(idAttributeName, idAttributeValue);
}

return query.getSingleResult() == 1L;
}

@Override
public List<T> findAll() {
return getQuery(Specification.unrestricted(), Sort.unsorted()).getResultList();
}

@Override
public List<T> findAllById(Iterable<ID> ids) {

Assert.notNull(ids, IDS_MUST_NOT_BE_NULL);

if (!ids.iterator().hasNext()) {
return Collections.emptyList();
}

if (entityInformation.hasCompositeId()) {

List<T> results = new ArrayList<>();

for (ID id : ids) {
findById(id).ifPresent(results::add);
}

return results;
}

Collection<ID> idCollection = toCollection(ids);

TypedQuery<T> query = getQuery((root, q, criteriaBuilder) -> {

Path<?> path = root.get(entityInformation.getIdAttribute());
return path.in(idCollection);

}, Sort.unsorted());

return query.getResultList();
}

@Override
public List<T> findAll(Sort sort) {
return getQuery(Specification.unrestricted(), sort).getResultList();
}

@Override
public Page<T> findAll(Pageable pageable) {
return findAll(Specification.unrestricted(), pageable);
}

@Override
public Optional<T> findOne(Specification<T> spec) {
return Optional.ofNullable(getQuery(spec, Sort.unsorted()).setMaxResults(2).getSingleResultOrNull());
}

@Override
public List<T> findAll(Specification<T> spec) {
return getQuery(spec, Sort.unsorted()).getResultList();
}

@Override
public Page<T> findAll(Specification<T> spec, Pageable pageable) {
return findAll(spec, spec, pageable);
}

@Override
public Page<T> findAll(Specification<T> spec, Specification<T> countSpec, Pageable pageable) {

Specification<T> specToUse = spec == null ? Specification.unrestricted() : spec;
Specification<T> countSpecToUse = countSpec == null ? Specification.unrestricted() : countSpec;

TypedQuery<T> query = getQuery(specToUse, pageable);
return pageable.isUnpaged() ? new PageImpl<>(query.getResultList())
: readPage(query, getDomainClass(), pageable, countSpecToUse);
}

@Override
public List<T> findAll(Specification<T> spec, Sort sort) {
return getQuery(spec, sort).getResultList();
}

@Override
public boolean exists(Specification<T> spec) {

Assert.notNull(spec, "Specification must not be null");

CriteriaQuery<Integer> cq = this.entityManager.getCriteriaBuilder() //
.createQuery(Integer.class) //
.select(this.entityManager.getCriteriaBuilder().literal(1));

applySpecificationToCriteria(spec, getDomainClass(), cq);

TypedQuery<Integer> query = applyRepositoryMethodMetadata(this.entityManager.createQuery(cq));
return query.setMaxResults(1).getResultList().size() == 1;
}

@Override
@Transactional
public long update(UpdateSpecification<T> spec) {

Assert.notNull(spec, "Specification must not be null");

return getUpdate(spec, getDomainClass()).executeUpdate();
}

@Override
@Transactional
public long delete(DeleteSpecification<T> spec) {

Assert.notNull(spec, "Specification must not be null");

return getDelete(spec, getDomainClass()).executeUpdate();
}

@Override
public <S extends T, R extends @Nullable Object> R findBy(Specification<T> spec,
Function<? super SpecificationFluentQuery<S>, R> queryFunction) {

Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);

return doFindBy(spec, getDomainClass(), queryFunction);
}

@SuppressWarnings("unchecked")
private <S extends T, R> R doFindBy(Specification<T> spec, Class<T> domainClass,
Function<? super SpecificationFluentQuery<S>, R> queryFunction) {

Assert.notNull(spec, SPECIFICATION_MUST_NOT_BE_NULL);
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);

ScrollQueryFactory<TypedQuery<T>> scrollFunction = (q, scrollPosition) -> {

Specification<T> specToUse = spec;
Sort sort = q.sort;

if (scrollPosition instanceof KeysetScrollPosition keyset) {
KeysetScrollSpecification<T> keysetSpec = new KeysetScrollSpecification<>(keyset, sort, entityInformation);
sort = keysetSpec.sort();
specToUse = specToUse.and(keysetSpec);
}

TypedQuery<T> query = getQuery(q.returnedType, specToUse, domainClass, sort, q.properties, scrollPosition);

if (scrollPosition instanceof OffsetScrollPosition offset) {
if (!offset.isInitial()) {
query.setFirstResult(Math.toIntExact(offset.getOffset()) + 1);
}
}

return query;
};

Function<FluentQuerySupport<?, ?>, TypedQuery<T>> finder = (q) -> getQuery(q.returnedType, spec, domainClass,
q.sort, q.properties, null);

SpecificationScrollDelegate<T> scrollDelegate = new SpecificationScrollDelegate<>(scrollFunction,
entityInformation);
FetchableFluentQueryBySpecification<?, T> fluentQuery = new FetchableFluentQueryBySpecification<>(spec, domainClass,
finder, scrollDelegate, this::count, this::exists, this.entityManager, getProjectionFactory());

R result = queryFunction.apply((SpecificationFluentQuery<S>) fluentQuery);

if (result instanceof FluentQuery<?>) {
throw new InvalidDataAccessApiUsageException(
"findBy(…) queries must result a query result and not the FluentQuery object to ensure that queries are executed within the scope of the findBy(…) method");
}

return result;
}

@Override
public <S extends T> Optional<S> findOne(Example<S> example) {

TypedQuery<S> query = getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(),
Sort.unsorted()).setMaxResults(2);

return Optional.ofNullable(query.getSingleResultOrNull());
}

@Override
public <S extends T> long count(Example<S> example) {
return executeCountQuery(
getCountQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType()));
}

@Override
public <S extends T> boolean exists(Example<S> example) {

Specification<S> spec = new ExampleSpecification<>(example, this.escapeCharacter);
CriteriaQuery<Integer> cq = this.entityManager.getCriteriaBuilder() //
.createQuery(Integer.class) //
.select(this.entityManager.getCriteriaBuilder().literal(1));

applySpecificationToCriteria(spec, example.getProbeType(), cq);

TypedQuery<Integer> query = applyRepositoryMethodMetadata(this.entityManager.createQuery(cq));
return query.setMaxResults(1).getResultList().size() == 1;
}

@Override
public <S extends T> List<S> findAll(Example<S> example) {
return getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), Sort.unsorted())
.getResultList();
}

@Override
public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
return getQuery(new ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), sort).getResultList();
}

@Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {

ExampleSpecification<S> spec = new ExampleSpecification<>(example, escapeCharacter);
Class<S> probeType = example.getProbeType();
TypedQuery<S> query = getQuery(new ExampleSpecification<>(example, escapeCharacter), probeType, pageable);

return pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) : readPage(query, probeType, pageable, spec);
}

@Override
@SuppressWarnings("unchecked")
public <S extends T, R extends @Nullable Object> R findBy(Example<S> example,
Function<FetchableFluentQuery<S>, R> queryFunction) {

Assert.notNull(example, EXAMPLE_MUST_NOT_BE_NULL);
Assert.notNull(queryFunction, QUERY_FUNCTION_MUST_NOT_BE_NULL);

ExampleSpecification<S> spec = new ExampleSpecification<>(example, escapeCharacter);
Class<S> probeType = example.getProbeType();

return doFindBy((Specification<T>) spec, (Class<T>) probeType, queryFunction);
}

@Override
public long count() {

TypedQuery<Long> query = entityManager.createQuery(countQueryString.get(), Long.class);

applyQueryHintsForCount(query);

return query.getSingleResult();
}

@Override
public long count(Specification<T> spec) {
return executeCountQuery(getCountQuery(spec, getDomainClass()));
}

@Override
@Transactional
public <S extends T> S save(S entity) {

Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);

if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}

@Override
@Transactional
public <S extends T> S saveAndFlush(S entity) {

S result = save(entity);
flush();

return result;
}

@Override
@Transactional
public <S extends T> List<S> saveAll(Iterable<S> entities) {

Assert.notNull(entities, ENTITIES_MUST_NOT_BE_NULL);

List<S> result = new ArrayList<>();

for (S entity : entities) {
result.add(save(entity));
}

return result;
}

@Override
@Transactional
public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {

List<S> result = saveAll(entities);
flush();

return result;
}

@Override
@Transactional
public void flush() {
entityManager.flush();
}

/**
* Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable} and
* {@link Specification}.
*
* @param query must not be {@literal null}.
* @param spec must not be {@literal null}.
* @param pageable must not be {@literal null}.
* @deprecated use {@link #readPage(TypedQuery, Class, Pageable, Specification)} instead
*/
@Deprecated
protected Page<T> readPage(TypedQuery<T> query, Pageable pageable, Specification<T> spec) {
return readPage(query, getDomainClass(), pageable, spec);
}

/**
* Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable} and
* {@link Specification}.
*
* @param query must not be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param spec must not be {@literal null}.
* @param pageable can be {@link Pageable#unpaged()}.
*/
protected <S extends T> Page<S> readPage(TypedQuery<S> query, Class<S> domainClass, Pageable pageable,
Specification<S> spec) {

Assert.notNull(spec, "Specification must not be null");

if (pageable.isPaged()) {
query.setFirstResult(PageableUtils.getOffsetAsInteger(pageable));
query.setMaxResults(pageable.getPageSize());
}

return PageableExecutionUtils.getPage(query.getResultList(), pageable,
() -> executeCountQuery(getCountQuery(spec, domainClass)));
}

/**
* Creates a new {@link TypedQuery} from the given {@link Specification}.
*
* @param spec must not be {@literal null}.
* @param pageable must not be {@literal null}.
*/
protected TypedQuery<T> getQuery(Specification<T> spec, Pageable pageable) {
return getQuery(spec, getDomainClass(), pageable.getSort());
}

/**
* Creates a new {@link TypedQuery} from the given {@link Specification}.
*
* @param spec must not be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param pageable must not be {@literal null}.
*/
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Pageable pageable) {
return getQuery(spec, domainClass, pageable.getSort());
}

/**
* Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
*
* @param spec must not be {@literal null}.
* @param sort must not be {@literal null}.
*/
protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {
return getQuery(spec, getDomainClass(), sort);
}

/**
* Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
*
* @param spec must not be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param sort must not be {@literal null}.
*/
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
return getQuery(ReturnedType.of(domainClass, domainClass, projectionFactory), spec, domainClass, sort,
Collections.emptySet(), null);
}

/**
* Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
*
* @param returnedType must not be {@literal null}.
* @param spec must not be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param sort must not be {@literal null}.
* @param inputProperties must not be {@literal null}.
* @param scrollPosition must not be {@literal null}.
*/
private <S extends T> TypedQuery<S> getQuery(ReturnedType returnedType, Specification<S> spec, Class<S> domainClass,
Sort sort, Collection<String> inputProperties, @Nullable ScrollPosition scrollPosition) {

Assert.notNull(spec, "Specification must not be null");

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<S> query;

boolean interfaceProjection = returnedType.isInterfaceProjection();
boolean inputPropertiesPresent = !inputProperties.isEmpty();

if (returnedType.needsCustomConstruction()) {
query = (CriteriaQuery) (interfaceProjection ? builder.createTupleQuery()
: builder.createQuery(returnedType.getReturnedType()));
} else {
query = builder.createQuery(domainClass);
}

Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

if (returnedType.needsCustomConstruction()) {

Collection<String> requiredSelection;
if (inputProperties.isEmpty()) {
inputProperties = returnedType.getInputProperties();
}

if (scrollPosition instanceof KeysetScrollPosition && interfaceProjection) {
requiredSelection = KeysetScrollDelegate.getProjectionInputProperties(entityInformation, inputProperties, sort);
} else {
requiredSelection = inputProperties;
}

List<Selection<?>> selections = new ArrayList<>();
Set<String> topLevelProperties = new HashSet<>();
for (String property : requiredSelection) {

if (inputPropertiesPresent && !interfaceProjection) {
PropertyPath path = PropertyPath.from(property, returnedType.getDomainType());
selections.add(QueryUtils.toExpressionRecursively(root, path, true));
} else {

int separator = property.indexOf('.');
String topLevelProperty = separator == -1 ? property : property.substring(0, separator);

if (!topLevelProperties.add(topLevelProperty)) {
continue;
}

PropertyPath path = PropertyPath.from(topLevelProperty, returnedType.getDomainType());
selections.add(QueryUtils.toExpressionRecursively(root, path, true).alias(topLevelProperty));
}
}

Class<?> typeToRead = returnedType.getReturnedType();

query = typeToRead.isInterface() //
? query.multiselect(selections) //
: query.select((Selection) builder.construct(typeToRead, //
selections.toArray(new Selection[0])));
} else {
query.select(root);
}

if (sort.isSorted()) {
query.orderBy(toOrders(sort, root, builder));
}

return applyRepositoryMethodMetadata(entityManager.createQuery(query));
}

/**
* Creates a {@link Query} for the given {@link UpdateSpecification}.
*
* @param spec must not be {@literal null}.
* @param domainClass must not be {@literal null}.
*/
protected <S> Query getUpdate(UpdateSpecification<S> spec, Class<S> domainClass) {

Assert.notNull(spec, "Specification must not be null");

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaUpdate<S> query = builder.createCriteriaUpdate(domainClass);

applySpecificationToCriteria(spec, domainClass, query);

return applyRepositoryMethodMetadata(entityManager.createQuery(query));
}

/**
* Creates a {@link Query} for the given {@link DeleteSpecification}.
*
* @param spec must not be {@literal null}.
* @param domainClass must not be {@literal null}.
*/
protected <S> Query getDelete(DeleteSpecification<S> spec, Class<S> domainClass) {

Assert.notNull(spec, "Specification must not be null");

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaDelete<S> query = builder.createCriteriaDelete(domainClass);

applySpecificationToCriteria(spec, domainClass, query);

return applyRepositoryMethodMetadata(entityManager.createQuery(query));
}

/**
* Creates a new count query for the given {@link Specification}.
*
* @param spec must not be {@literal null}.
* @deprecated override {@link #getCountQuery(Specification, Class)} instead
*/
@Deprecated
protected TypedQuery<Long> getCountQuery(Specification<T> spec) {
return getCountQuery(spec, getDomainClass());
}

/**
* Creates a new count query for the given {@link Specification}.
*
* @param spec must not be {@literal null}.
* @param domainClass must not be {@literal null}.
*/
protected <S extends T> TypedQuery<Long> getCountQuery(Specification<S> spec, Class<S> domainClass) {

Assert.notNull(spec, "Specification must not be null");

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);

Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

if (query.isDistinct()) {
query.select(builder.countDistinct(root));
} else {
query.select(builder.count(root));
}

// Remove all Orders the Specifications might have applied
query.orderBy(Collections.emptyList());

return applyRepositoryMethodMetadataForCount(entityManager.createQuery(query));
}

/**
* Returns {@link QueryHints} with the query hints based on the current {@link CrudMethodMetadata} and potential
* {@link EntityGraph} information.
*/
protected QueryHints getQueryHints() {
return metadata == null ? NoHints.INSTANCE : DefaultQueryHints.of(entityInformation, metadata);
}

/**
* Returns {@link QueryHints} with the query hints on the current {@link CrudMethodMetadata} for count queries.
*/
protected QueryHints getQueryHintsForCount() {
return metadata == null ? NoHints.INSTANCE : DefaultQueryHints.of(entityInformation, metadata).forCounts();
}

private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
CriteriaQuery<S> query) {

Root<U> root = query.from(domainClass);

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);

if (predicate != null) {
query.where(predicate);
}

return root;
}

private <S> void applySpecificationToCriteria(UpdateSpecification<S> spec, Class<S> domainClass,
CriteriaUpdate<S> query) {

Root<S> root = query.from(domainClass);

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);

if (predicate != null) {
query.where(predicate);
}
}

private <S> void applySpecificationToCriteria(DeleteSpecification<S> spec, Class<S> domainClass,
CriteriaDelete<S> query) {

Root<S> root = query.from(domainClass);

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);

if (predicate != null) {
query.where(predicate);
}
}

private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {

if (metadata == null) {
return query;
}

LockModeType type = metadata.getLockModeType();
TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);

applyQueryHints(toReturn);

return toReturn;
}

private Query applyRepositoryMethodMetadata(Query query) {

if (metadata == null) {
return query;
}

LockModeType type = metadata.getLockModeType();
Query toReturn = type == null ? query : query.setLockMode(type);

applyQueryHints(toReturn);

return toReturn;
}

private void applyQueryHints(Query query) {

if (metadata == null) {
return;
}

getQueryHints().withFetchGraphs(entityManager).forEach(query::setHint);
applyComment(metadata, query::setHint);
}

private <S> TypedQuery<S> applyRepositoryMethodMetadataForCount(TypedQuery<S> query) {

if (metadata == null) {
return query;
}

applyQueryHintsForCount(query);

return query;
}

private void applyQueryHintsForCount(Query query) {

if (metadata == null) {
return;
}

getQueryHintsForCount().forEach(query::setHint);
applyComment(metadata, query::setHint);
}

private Map<String, Object> getHints() {

Map<String, Object> hints = new HashMap<>();

getQueryHints().withFetchGraphs(entityManager).forEach(hints::put);

if (metadata != null) {
applyComment(metadata, hints::put);
}

return hints;
}

private void applyComment(CrudMethodMetadata metadata, BiConsumer<String, Object> consumer) {

if (metadata.getComment() != null && provider.getCommentHintKey() != null) {
consumer.accept(provider.getCommentHintKey(), provider.getCommentHintValue(metadata.getComment()));
}
}

private ProjectionFactory getProjectionFactory() {
return projectionFactory;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private static <T> Collection<T> toCollection(Iterable<T> ids) {
return ids instanceof Collection c ? c : Streamable.of(ids).toList();
}

/**
* Executes a count query and transparently sums up all values returned.
*
* @param query must not be {@literal null}.
*/
private static long executeCountQuery(TypedQuery<Long> query) {

Assert.notNull(query, "TypedQuery must not be null");

List<Long> totals = query.getResultList();
long total = 0L;

for (Long element : totals) {
total += element == null ? 0 : element;
}

return total;
}

/**
* {@link Specification} that gives access to the {@link Predicate} instance representing the values contained in the
* {@link Example}.
*
* @param <T>
* @author Christoph Strobl
* @since 1.10
*/
private record ExampleSpecification<T>(Example<T> example,
EscapeCharacter escapeCharacter) implements Specification<T> {

/**
* Creates new {@link ExampleSpecification}.
*
* @param example the example to base the specification of. Must not be {@literal null}.
* @param escapeCharacter the escape character to use for like expressions. Must not be {@literal null}.
*/
private ExampleSpecification {

Assert.notNull(example, EXAMPLE_MUST_NOT_BE_NULL);
Assert.notNull(escapeCharacter, "EscapeCharacter must not be null");

}

@Override
public @Nullable Predicate toPredicate(Root<T> root, @Nullable CriteriaQuery<?> query, CriteriaBuilder cb) {
return QueryByExamplePredicateBuilder.getPredicate(root, cb, example, escapeCharacter);
}
}

} 










Sinif səviyyəsində

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

Sinifin özündə @Transactional(readOnly = true) var — yəni default olaraq bütün metodlar read-only transaction-dadırfindAll()findById()count() — bunların heç birinin üstündə ayrıca annotasiya yoxdur, çünki sinif səviyyəsindəki readOnly = true onlara şamil olur.

Yazma metodlarında isə override edilir

@Override
@Transactional // <-- readOnly = true YOXDUR, yəni readOnly = false (default)
public <S extends T> S save(S entity) { ... }
@Override
@Transactional // <-- eyni şəkildə
public void deleteById(ID id) { ... }
@Override
@Transactional // <-- eyni şəkildə
public void deleteAll() { ... }

Bu metodların üstündəki @Transactional (readOnly olmadan) sinif səviyyəsindəki readOnly = true-nu override edir. Yəni save()delete()flush() metodları yazma icazəsi olan transaction-da işləyir.



Mes: burada @Transactional veya @Modifying annotasiyasini ishletmesek xeta atacaq:

@Modifying
@Transactional
@Query("DELETE FROM Student s WHERE s.age < :age")
void deleteByAgeLessThan(@Param("age") int age);


BEGIN (transaction başlayır)
├── SELECT ...
├── INSERT ...
├── UPDATE ...
├── DELETE ...
│ (nə etmək istəyirsinizsə — hamısı burada)
COMMIT (hamısı təsdiqlənir, dəyişikliklər qalıcı olur)


BEGIN
├── ...
├── XƏTA
ROLLBACK (hamısı ləğv olunur, heç nə yazılmır)


Bəli. JpaRepository-nin default implementasiyası olan SimpleJpaRepository-də bütün metodlar bir şəkildə transactional-dır.


Lakin bu ishleyecek:

@Query(value = "select * from student", nativeQuery = true)
List<Student> foo();

SELECT yalnız oxuyur, heç nə dəyişdirmir. Baza bunu transaction olmadan da (implicit transaction / auto-commit ilə) icra edə bilir. Ona görə JPA da tələb etmir.

Siz BEGIN / COMMIT yazmırsınız. Amma baza daxilən belə edir:

BEGIN; ← baza özü açır (siz görmürsünüz)
SELECT * FROM student;
COMMIT; ← baza dərhal bağlayır



*** open-in-view: false izahi










Connection nədir?

Tətbiqiniz verilənlər bazası ilə danışmaq istəyəndə, aralarında bir bağlantı (connection) açılmalıdır. Bu, telefon zəngi kimi bir şeydir:

  1. Tətbiq bazaya "zəng edir" (TCP bağlantısı açılır)
  2. Əl sıxma olur — kim olduğunu təsdiqləyir (authentication — istifadəçi adı, şifrə)
  3. Kanal açılır — indi sorğu göndərə bilərsən (SELECT, INSERT...)
  4. İş bitir — bağlantı bağlanır

Bu proses hər dəfə 20-50ms çəkir. Bir sorğu üçün az görünür, amma saniyədə 1000 sorğu gələndə bu, böyük problem olur.

Poolsuz həyat (problem)

İstifadəçi 1 → connection aç → sorğu → connection bağla (50ms boşa)
İstifadəçi 2 → connection aç → sorğu → connection bağla (50ms boşa)
İstifadəçi 3 → connection aç → sorğu → connection bağla (50ms boşa)
...
1000 istifadəçi → 1000 dəfə aç/bağla → 50 saniyə boşa gedir

Hər dəfə connection açıb-bağlamaq resurs israfıdır.



Connection Pool nədir? (Hikari-nin həll etdiyi problem)

Pool — hovuz deməkdir. Connection pool — əvvəlcədən açılmış connection-ların saxlandığı hovuzdur.

Tətbiq başlayanda:
HikariCP → 5 connection açır → hovuzda saxlayır
İstifadəçi 1 → hovuzdan connection götürür → sorğu → hovuza qaytarır
İstifadəçi 2 → hovuzdan connection götürür → sorğu → hovuza qaytarır
İstifadəçi 3 → hovuzdan connection götürür → sorğu → hovuza qaytarır

Heç vaxt yeni connection açılmır, heç vaxt bağlanmır — təkrar istifadə olunur.



Analoji — kitabxana

Poolsuz: Hər dəfə kitab lazım olanda mağazaya gedirsən, kitab alırsan, oxuyursan, atırsan. Növbəti dəfə yenə mağazaya gedirsən → bahalı, yavaş.

Pool ilə (Hikari): Kitabxanada 10 kitab var. Birini götürürsən, oxuyursan, qaytarırsan. Başqası eyni kitabı götürür → sürətli, səmərəli, heç kim gözləmir.

Niyə məhz Hikari?

Java-da başqa connection pool-lar da var (C3P0, DBCP2, Tomcat Pool), amma Hikari:

  • Ən sürətli olanıdır
  • Ən yüngül olanıdır (az yaddaş istifadə edir)
  • Ona görə Spring Boot default olaraq onu seçib

Tələbələrinizə qısaca belə deyə bilərsiniz: "Hikari — verilənlər bazası ilə bağlantıları əvvəlcədən açıb hovuzda saxlayan mexanizmdir. Bu sayədə hər sorğuda yeni bağlantı açmaq əvəzinə, hazır bağlantıdan istifadə olunur və tətbiq qat-qat sürətli işləyir."




Connection açılanda arxa planda nə olur?

Siz repository.findAll() çağıranda, arxa planda təxminən belə bir proses gedir:

1. TCP bağlantısı açılır (şəbəkə səviyyəsi)

Tətbiqiniz PostgreSQL-ə şəbəkə üzərindən qoşulur. Hətta localhost olsa belə, bu bir TCP bağlantısıdır — əməliyyat sistemi soket açır, port ayırır.

2. Authentication (kimlik doğrulama)

Baza soruşur: "Sən kimsən?" — tətbiq istifadəçi adı və şifrəni göndərir. Baza yoxlayır, təsdiqləyir.

3. Session yaradılır

PostgreSQL bu connection üçün ayrıca proses ayırır, yaddaş ayırır, session konfiqurasiyasını qurur (encoding, timezone və s.).

4. Hazırdır — indi sorğu göndərə bilərsiniz

Bu prosesin hamısı JDBC driver-in kodu tərəfindən idarə olunur. Poolsuz dünyada bu kod təxminən belə görünür:

// 1. Driver yüklənir
// 2. Connection açılır (bahalı hissə — TCP + auth + session)
Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/test", "user", "password"
);
// 3. Sorğu göndərilir
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM student");
ResultSet rs = stmt.executeQuery();
// 4. Connection bağlanır (resurslar azad edilir)
conn.close();

Hər getConnection() çağırışı — TCP bağlantısı + authentication + session yaradılması. Hər close() — bütün bunların dağıdılması.

Niyə bahalıdır?

AddımNə baş verir?Təxmini vaxt
TCP bağlantısıƏməliyyat sistemi soket açır, 3-way handshake1-5 ms
Authenticationİstifadəçi/şifrə yoxlanılır5-10 ms
Session yaradılmasıBaza yaddaş ayırır, proses yaradır10-30 ms
Cəmi20-50 ms

Sorğunun özü isə bəzən 1 ms çəkir. Yəni connection açmaq, sorğunun özündən 20-50 dəfə bahalıdır.

Hikari ilə nə dəyişir?

// Hikari hovuzdan HAZIR connection verir (TCP artıq açıqdır, auth artıq olub)
Connection conn = hikariDataSource.getConnection(); // ~0.5 ms
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM student");
ResultSet rs = stmt.executeQuery();
// close() connection-u BAĞLAMIR, hovuza QAYTARIR
conn.close(); // ~0.5 ms

getConnection() — artıq açıq olan connection-u hovuzdan götürür. close() — bağlamır, hovuza geri qoyur. Həmin TCP bağlantısı, həmin session — təkrar istifadə olunur.



Spring Boot + HikariCP-nin default konfiqurasiyası belədir:

ParametrDefault dəyərMənası
maximum-pool-size10Hovuzda maksimum 10 connection ola bilər
minimum-idle10 (maximum ilə eyni)Hovuzda minimum boş connection sayı
connection-timeout30000 ms (30 san)Connection gözləmə limiti
idle-timeout600000 ms (10 dəq)Boş connection-un hovuzda qalma müddəti
max-lifetime1800000 ms (30 dəq)Bir connection-un maksimum ömrü



Bu nə deməkdir?

  • Tətbiq başlayanda Hikari 10 connection açır PostgreSQL-ə
  • Eyni anda maksimum 10 sorğu paralel icra oluna bilər
  • 11-ci sorğu gəlsə, 30 saniyə gözləyir (boş connection olana qədər)
  • 30 saniyə ərzində boş connection olmazsa — xəta atılır




Tətbiq ayağa qalxanda Hikari 10 ayrı TCP bağlantısı açır PostgreSQL-ə.

Loglarınızdan da bunu izləmək olar:


HikariPool-1 - Starting... ← hovuz yaranmağa başlayır
HikariPool-1 - Added connection ...@2b6b487c ← ilk TCP connection açıldı
HikariPool-1 - Start completed. ← hovuz hazırdır

Logda yalnız bir connection görünür, çünki Hikari default olaraq connection-ları lazy şəkildə əlavə edir — ilk birini açır, qalanlarını lazım olduqca əlavə edir (amma minimum-idle sayına çatana qədər).

Spring Boot başlayır
HikariCP başlayır
├── TCP connection #1 → PostgreSQL:5432 (auth + session)
├── TCP connection #2 → PostgreSQL:5432 (auth + session)
├── TCP connection #3 → PostgreSQL:5432 (auth + session)
├── ...
└── TCP connection #10 → PostgreSQL:5432 (auth + session)
Hovuz hazırdır, sorğu gözləyir


Hər biri ayrıca:

  • TCP soket açır
  • PostgreSQL-ə authentication edir
  • PostgreSQL tərəfdə ayrıca proses/session yaranır

Bunu PostgreSQL tərəfdən də görmək olar

Əgər tətbiq işləyərkən PostgreSQL-də bu sorğunu çalışdırsanız:



Hikari sizin əvəzinizə nələri idarə edir?


ProblemSizin əl ilə yazdığınızHikariCP
Connection açmaqÖzünüz yazırsınızAvtomatik
Hansı connection boşdur?Özünüz izləməlisinizAvtomatik izləyir
İki thread eyni connection istəsə?Race condition, bugThread-safe idarə edir
Connection ölübsə (baza restart)?Xəta alırsınızAvtomatik aşkarlayıb yenisini açır
Connection çox boş qalıbsa?Həmişə açıq qalır, resurs israfıidle-timeout ilə bağlayır
Connection çox köhnəlibsə?Heç vaxt yenilənmirmax-lifetime ilə yeniləyir
Hovuz dolubsa, yeni sorğu gəlsə?Xəta və ya sonsuz gözləməTimeout ilə idarə edir



getConnection() çağırılır
Hovuzda boş connection var?
Bəli ──────────────────── Xeyr
│ │
▼ ▼
Connection-u Hovuz max-a çatıb?
ver, istifadə et │
Xeyr ────── Bəli
│ │
▼ ▼
Yeni connection connection-timeout
yarat, ver qədər gözlə
Boşalan oldu? ── Xeyr → XƏTA atılır
Bəli
Connection-u ver


HouseKeeper (hər 30 saniyədə):
├── Bu connection 10 dəq boş dayanıb? → Bağla
├── Bu connection 30 dəq-dən çox yaşayır? → Bağla, yenisini aç
└── Hovuzda minimum-idle qədər boş connection var? → Yoxdursa, yarat



SELECT pid, usename, application_name, state
FROM pg_stat_activity
WHERE datname = 'test';


















Комментарии

Популярные сообщения из этого блога

Interview questions

Lesson1: JDK, JVM, JRE

Lesson_2: Operations in Java