Когда использовать Entity или DTO

Spring Boot

Обратите внимание на публичный номер: Большой парень вне банка

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

JPAиHibernateпозволить вамJPQLиCriteriaиспользуется в запросеDTOиEntityкак карта. Когда я обсуждаю на своем онлайн-тренинге или семинареHibernateКогда дело доходит до производительности, меня часто спрашивают, важно ли выбирать подходящее отображение для использования? Ответ: да! Выбор правильного сопоставления для вашего варианта использования может оказать огромное влияние на производительность. Я выбираю только те данные, которые вам нужны. Очевидно, что выбор ненужной информации не даст вам преимущества в производительности.

1. Основное различие между DTO и Entity

EntityиDTOЧасто упускаемая из виду разница между -EntityУправляется контекстом постоянства. когда вы хотите обновитьEntity, просто позвониsetterспособ установки нового значения.HibernateТребуемые операторы SQL будут обработаны, и изменения будут записаны в базу данных.

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

Также нужно помнить,Hibernateи любой другойJPAРеализации хранят все управляемые объекты в кэше L1. Кажется, это хорошо. Это предотвращает выполнение повторяющихся запросов, что требуется для оптимизации записи Hibernate. Однако для управления кешем L1 требуется время, и проблемы могут возникнуть даже в том случае, если запрашиваются сотни или тысячи сущностей.

использоватьEntityбудет нести накладные расходы, в то время как вы можете использоватьDTOизбежать этих накладных расходов. Но значит ли это, что не следует использоватьEntity? Очевидно нет.

2. Напишите проекцию операции

Entity Projections применяются ко всем операциям записи.HibernateИ другиеJPAРеализация управляет состоянием объекта и создает необходимые операторы SQL для сохранения изменений в базе данных. Это делает реализацию большинства операций создания, обновления и удаления очень простой и эффективной.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Author a = em.find(Author.class, 1L);
a.setFirstName("Thorben");

em.getTransaction().commit();
em.close();

3. Прочитайте прогноз операции

Но операции только для чтения обрабатываются по-другому. Если вы хотите прочитать данные из базы данных, тоHibernateОн не управляет состоянием и не выполняет грязные проверки. Итак, теоретически для чтения данныхDTOПроекция — лучший выбор. Но действительно ли все иначе? Чтобы ответить на этот вопрос, я провел небольшой тест производительности.

3.1. Настройка теста

Я использую следующую модель предметной области для тестирования. это состоит изAuthorиBookКомпозиция сущностей с использованием ассоциаций «многие к одному». Итак, каждая книга написана одним автором.

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @Version
    private int version;

    private String firstName;

    private String lastName;

    @OneToMany(mappedBy = "author")
    private List bookList = new ArrayList();

    ...
}

для обеспеченияHibernateне получая никаких дополнительных данных, я установил@ManyToOneизFetchTypeзаLAZH. ты можешь читать Introduction to JPA FetchTypesстать другимFetchTypeи больше информации о его эффектах.

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @Version
    private int version;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fk_author")
    private Author author;

    ...
}

Я создал тестовую базу данных с 10 авторами, каждый из которых написал по 10 книг, так что всего база данных содержит 100 книг. В каждом тесте я буду использовать другую проекцию для запроса 100 книг и измерять время, необходимое для выполнения запроса и транзакции. Чтобы уменьшить влияние любых побочных эффектов, я делаю это 1000 раз и измеряю среднее время. Хорошо, давайте начнем.

3.2 Сущности запроса

В большинстве приложений Entity Projection является наиболее популярным. имеютEntity,JPAИх можно легко использовать в качестве проекций. Запустите этот небольшой тестовый пример и измерьте получение 100Bookвремя, необходимое субъекту.

long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
    EntityManager em = emf.createEntityManager();

    long startTx = System.currentTimeMillis();
    em.getTransaction().begin();

    // Execute Query
    long startQuery = System.currentTimeMillis();
    List<Book> books = em.createQuery("SELECT b FROM Book b").getResultList();
    long endQuery = System.currentTimeMillis();
    timeQuery += endQuery - startQuery;

    em.getTransaction().commit();
    long endTx = System.currentTimeMillis();

    em.close();
    timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

В среднем запрос выполняется, результаты извлекаются и сопоставляются со 100BookСущность занимает 2 мс. 2,89 мс, если включены транзакции. Неплохо для маленького и не очень нового ноутбука.

Transaction: total 2890 per iteration 2.89
Query: total 2000 per iteration 2.0

3.3 Влияние FetchType по умолчанию на ассоциацию To-One

Когда я показывал вам сущность Книга, я указал, чтоFetchTypeУстановить какLAZYчтобы избежать других запросов. по умолчанию,To-oneСвязанныйFetchtTypeдаEAGER, который говоритHibernateАссоциация инициализируется немедленно.

Для этого требуется дополнительный запрос, который может сильно повлиять на производительность, если ваш запрос выбирает несколько сущностей. давай менятьсяBookобъект для использования по умолчаниюFetchTypeи проведите тот же тест.

@Entity
public class Book {

    @ManyToOne
    @JoinColumn(name = "fk_author")
    private Author author;

    ...
}

Это небольшое изменение увеличило время выполнения теста более чем в три раза. Теперь для выполнения запроса и сопоставления результатов требовалось 7,797 мс вместо 2 мс. Время на транзакцию увеличилось до 8,681 мс вместо 2,89 мс.

Transaction: total 8681 per iteration 8.681
Query: total 7797 per iteration 7.797

Поэтому лучше всего убедиться, чтоTo-oneНастройки ассоциацииFetchTypeзаLAZY.

3.4. Выберите объект @Immutable

Жоао Шарнет посоветовал мне в комментариях добавить в тест Immutable Entity. Интересный вопрос: вернуться к использованию@ImmutableАннотированные сущности, будет ли производительность запросов лучше?

HibernateНет необходимости выполнять какие-либо грязные проверки этих сущностей, поскольку они неизменяемы. Это может привести к повышению производительности. Итак, давайте попробуем.

Я добавил следующее в свой тестImmutableBookорганизация.

@Entity
@Table(name = "book")
@Immutable
public class ImmutableBook {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;

    @Version
    private int version;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fk_author")
    private Author author;

    ...
}

этоBookКопия объекта с двумя дополнительными аннотациями.@ImmutableАннотация рассказываетHibernate, эта сущность неизменна. и@Table(name =“book”)сопоставлять объекты сbookповерхность. Таким образом, мы можем запустить тот же тест с теми же данными, что и раньше.

long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
    EntityManager em = emf.createEntityManager();

    long startTx = System.currentTimeMillis();
    em.getTransaction().begin();

    // Execute Query
    long startQuery = System.currentTimeMillis();
    List<Book> books = em.createQuery("SELECT b FROM ImmutableBook b")
            .getResultList();
    long endQuery = System.currentTimeMillis();
    timeQuery += endQuery - startQuery;

    em.getTransaction().commit();
    long endTx = System.currentTimeMillis();

    em.close();
    timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

Интересно, что для запроса не имеет значения, является ли сущность неизменной или нет. Измеренное среднее время выполнения транзакций и запросов почти такое же, как и в предыдущем тесте.

Transaction: total 2879 per iteration 2.879
Query: total 2047 per iteration 2.047

3.5 Запрос объекта с помощью QueryHints.HINT_READONLY

Эндрю Буржуа предложил включить в тесты запросы только для чтения. Итак, смотрите здесь.

В этом тесте используется то, что я показал вам в начале статьи.Bookорганизация. Но это требует модификации тестовых случаев.

JPAиHibernateПоддерживает набор подсказок запроса (соответствий), которые позволяют предоставить дополнительную информацию о запросе и о том, как он выполняется. подсказка запросаQueryHints.HINT_READONLYРассказыватьHibernateОбъекты запросов в режиме только для чтения. следовательно,HibernateНет необходимости выполнять для них какие-либо грязные проверки, и можно применять и другие оптимизации.

вы можете пройти вQueryвызов на интерфейсsetHintМетод установлен для установки этой подсказки.

long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
    EntityManager em = emf.createEntityManager();

    long startTx = System.currentTimeMillis();
    em.getTransaction().begin();

    // Execute Query
    long startQuery = System.currentTimeMillis();
    Query query = em.createQuery("SELECT b FROM Book b");
    query.setHint(QueryHints.HINT_READONLY, true);
    query.getResultList();
    long endQuery = System.currentTimeMillis();
    timeQuery += endQuery - startQuery;

    em.getTransaction().commit();
    long endTx = System.currentTimeMillis();

    em.close();
    timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

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

Но, как вы можете видеть ниже, время выполнения почти такое же, как и в предыдущем тесте. По крайней мере, в этом тестовом сценарииQueryHints.HINT_READONLYУстановить какtrueНе улучшит производительность.

Transaction: total 2842 per iteration 2.842
Query: total 2006 per iteration 2.006

3.6 Запрос DTO

Загрузка 100 объектов книги занимает около 2 мс. Давайте посмотрим наJPQLЛучше ли использовать выражения конструктора в запросах, чтобы получить те же данные.

Конечно, вы также можетеCriteriaИспользуйте выражения конструктора в запросах.

long timeTx = 0;
long timeQuery = 0;
long iterations = 1000;
// Perform 1000 iterations
for (int i = 0; i < iterations; i++) {
    EntityManager em = emf.createEntityManager();

    long startTx = System.currentTimeMillis();
    em.getTransaction().begin();

    // Execute the query
    long startQuery = System.currentTimeMillis();
    List<BookValue> books = em.createQuery("SELECT new org.thoughts.on.java.model.BookValue(b.id, b.title) FROM Book b").getResultList();
    long endQuery = System.currentTimeMillis();
    timeQuery += endQuery - startQuery;

    em.getTransaction().commit();
    long endTx = System.currentTimeMillis();

    em.close();

    timeTx += endTx - startTx;
}
System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations);
System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

Как и ожидалось,DTOПроекционное отношение实体(Entity)Проекция работает лучше.

Transaction: total 1678 per iteration 1.678
Query: total 1143 per iteration 1.143

В среднем для выполнения запроса требуется 1,143 мс, а для выполнения транзакции — 1,678 мс. Производительность запросов улучшена на 43%, а производительность транзакций улучшена примерно на 42%.

Неплохо для небольшого изменения, реализация которого заняла минуту.

В большинстве проектов,DTOПрирост производительности для проекции будет еще выше. Это позволяет вам выбирать данные, необходимые для варианта использования, а не только все свойства сопоставления объектов. Выбор меньшего количества данных почти всегда приводит к повышению производительности.

4. Резюме

Выбор правильной проекции для вашего варианта использования проще и важнее, чем вы думаете.

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

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

file

использоватьDTOПрогнозируемые запросы выполняются примерно на 40 % быстрее, чем запросы, выбирающие сущности. Так что лучше потратить дополнительные усилия на создание для ваших операций только для чтенияDTOи использовать его как проекцию.

Кроме того, убедитесь, что все ассоциации используютFetchType.LAZY. Как видно из тестов, даже нетерпеливый выборto-one, что также может утроить время выполнения запроса. Поэтому лучше использоватьFetchType.LAZYи инициализируйте то, что нужно вашему варианту использованиясвязь.

Оригинальная ссылка:мысли-на-java.org/entities- но он…

Автор: Торбен Янссен.

Переводчик: Юнооа