JPA: добавление новой записи в группу "многие ко многим" занимает много времени

У меня проблема при добавлении новой записи в отношении «многие ко многим», потому что список огромен. Бывший:

Item item = new Item(1);
Category cat = dao.find(1, Category.class);
List<Category> list = new ArrayList<>();
list.add(cat);
item.setCategoryList(list);
cat.getItemList().add(item);

Проблема в том, что список Category Itens огромен, с большим количеством элементов, поэтому выполнение cat.getItemList () занимает очень много времени. Везде, где я ищу правильный способ добавить запись "многие ко многим", говорится, что это необходимо. Может кто поможет?

Редактировать: Небольшой контекст: я организовываю свои itens с помощью тегов, так что 1 элемент может иметь несколько тегов, а 1 тег может иметь несколько itens, время прошло, и теперь у меня есть теги с большим количеством itens (> 5.000), и теперь, когда я сохраняю новый элемент с одним из этих тегов занимает много времени, я отладил свой код и обнаружил, что большая часть задержки находится в строке cat.getItensList (), что имеет смысл, поскольку у нее есть обширный список o itens. Я много искал, как это сделать, и все говорят, что правильный способ сохранить запись в случае «многие ко многим» - это добавить в список с обеих сторон отношения, но если одна сторона огромна, это займет много времени, поскольку вызов getItensList () загружает их в контекст. Я ищу способ сохранить мой элемент, ссылающийся на тег, без загрузки всех элементов этого тега.

Изменить 2: Мои занятия: Пункт:

@Entity
@Table(name = "transacao")
@XmlRootElement
 public class Transacao implements Serializable {
    @ManyToMany(mappedBy = "transacaoList")
    private List<Tagtransacao> tagtransacaoList;
    ...(other stuff)
}

Тег:

@Entity
@Table(name = "tagtransacao")
@XmlRootElement
public class Tagtransacao implements Serializable {
  @JoinTable(name = "transacao_has_tagtransacao", joinColumns = {
        @JoinColumn(name = "tagtransacao_idTagTransacao", referencedColumnName = "idTagTransacao")}, inverseJoinColumns = {
        @JoinColumn(name = "transacao_idTransacao", referencedColumnName = "idTransacao")})
    @ManyToMany
    private List<Transacao> transacaoList;
...(other stuff)
}

Изменить 3: ЧТО Я СДЕЛАЛ РЕШИТЬ: Как ответил Ариэль Кохан, я попытался выполнить NativeQuery, чтобы вставить отношения:

  Query query = queryDAO.criarNativeQuery("INSERT INTO " + config.getNomeBanco() + ".`transacao_has_tagtransacao` "
            + "(`transacao_idTransacao`, `tagtransacao_idTagTransacao`) VALUES (:idTransacao, :idTag);");
    query.setParameter("idTransacao", transacao.getIdTransacao());
    query.setParameter("idTag", tag.getIdTagTransacao()); 

Мне удалось сократить время запроса очереди с 10 секунд до 300 миллионов, что впечатляет. В конце концов, для моего проекта лучше, если он уже запущен для этого, вместо того, чтобы создавать новый класс, который представляет отношение "многие ко многим". Спасибо всем, кто пытался помочь \ o /

5000 - это не очень много, и я думаю, что его следует загружать только один раз, но если вы хотите вставить отношение напрямую, вы можете специально создать объект соединения (требуется создание составного ключа, множество примеров) и вставить новое отношение в эта сущность. Это должно быть в состоянии сделать это без необходимости сначала читать что-либо.

K.Nicholas 10.12.2018 16:00

Но если я использую составной ключевой класс для отношений «многие ко многим», не нужно ли мне получать список этого класса с обеих сторон и добавлять этот класс, как я должен это сделать сейчас?

Bem Hur Ganem 10.12.2018 18:42
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
2
1 100
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

Для этого я могу подумать о двух вариантах:

Использование запроса @Modyfing для вставки элементов непосредственно в БД.

[Рекомендуется для случаев, когда вы не хотите менять свою модель]

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

@Query(value = "insert into ...", nativeQuery = true)
void addItemToCategory(@Param("param1") Long param1, ...);

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

[Update] As you mentioned in a comment, doing this improved your performance from 10s to 300milis.


Измените свои Сущности, чтобы заменить @ManyToMany отношениями @OneToManys

Идея этого решения состоит в том, чтобы заменить связь ManyToMany между объектами A и B на промежуточный объект RelationAB. Думаю, это можно сделать двумя способами:

  1. Сохраните только идентификаторы из A и B в RelationAB в качестве составного ключа (конечно, вы можете добавить другие поля, такие как Date или что угодно).
  2. Добавьте автоматически сгенерированный идентификатор в RelationAB и добавьте A и B в качестве других полей в сущности RelationAB.

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

Entities A and B:

@Entity
class EntityA  {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    public EntityA() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

}

@Entity
class EntityB {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;


    public EntityB() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

}

RelationABEntity и RelationABId:

@Embeddable
class RelationABId implements Serializable {
    private Long entityAId;
    private Long entityBId;

    public RelationABId() {
    }

    public RelationABId(Long entityAId, Long entityBId) {
        this.entityAId = entityAId;
        this.entityBId = entityBId;
    }

    public Long getEntityAId() {
        return entityAId;
    }

    public void setEntityAId(Long entityAId) {
        this.entityAId = entityAId;
    }

    public Long getEntityBId() {
        return entityBId;
    }

    public void setEntityBId(Long entityBId) {
        this.entityBId = entityBId;
    }
}

@Entity
class RelationABEntity {

    @EmbeddedId
    private RelationABId id;

    public RelationABEntity() {
    }

    public RelationABEntity(Long entityAId, Long entityBId) {
        this.id = new RelationABId(entityAId, entityBId);
    }

    public RelationABId getId() {
        return id;
    }

    public void setId(RelationABId id) {
        this.id = id;
    }
}

Мои репозитории:

@Repository
interface RelationABEntityRepository extends JpaRepository<RelationABEntity, RelationABId> {

}

@Repository
interface ARepository extends JpaRepository<EntityA, Long> {


}

@Repository
interface BRepository extends JpaRepository<EntityB, Long> {

}

Тест:

@RunWith(SpringRunner.class)
@DataJpaTest
public class DemoApplicationTest {

    @Autowired RelationABEntityRepository relationABEntityRepository;
    @Autowired ARepository aRepository;
    @Autowired BRepository bRepository;


    @Test
    public void test(){
        EntityA a = new EntityA();
        a = aRepository.save(a);
        EntityB b = new EntityB();
        b = bRepository.save(b);

        //Entities A and B in the DB at this point

        RelationABId relationABID = new RelationABId(a.getId(), b.getId());

        final boolean relationshipExist = relationABEntityRepository.existsById(relationABID);
        assertFalse(relationshipExist);

        if (! relationshipExist){
            RelationABEntity relation = new RelationABEntity(a.getId(), b.getId());
            relationABEntityRepository.save(relation);
        }

        final boolean relationshipExitNow = relationABEntityRepository.existsById(relationABID);
        assertTrue(relationshipExitNow);
        /**
         * As you can see, modifying your model you can create relationships without loading big list and without complex queries. 
         */
    }    
}

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

Надеюсь это поможет :)

Я отредактировал вопрос, добавив больше контекста. Написание запроса, вероятно, решит мою проблему, но это сложный объект, и я бы предпочел использовать EntityManager и Pojos JPA. В последнем случае напишу запрос, спасибо за ответ

Bem Hur Ganem 10.12.2018 15:33

Хорошо, теперь я понимаю. Эти отношения двунаправленные? Пожалуйста, попробуйте добавить только категорию к элементу и не добавляйте элемент в cat.getItemList(), и посмотрите, что произошло, когда вы его сохраните. Если это не сработает, добавьте часть своей модели (особенно строки, в которых вы определяете отношение @ManyToMany)

Ariel Kohan 10.12.2018 15:52

Они двунаправленные, поэтому добавить только одну сторону не получится. Я снова редактировал с классами). Я написал запрос и сократил время с 10 до 300 миль, но мне кажется неправильным, что что-то такое простое и использованное может вызвать такую ​​большую проблему (извините за мой английский)

Bem Hur Ganem 10.12.2018 18:39

Рад слышать это, невероятная оптимизация. Однако я понимаю, и вы правы, мы можем сделать что-нибудь более чистое. Я собираюсь отредактировать ответ, предоставив вам другой вариант, аналогичный тому, что предложил @ K.Nicholas.

Ariel Kohan 10.12.2018 19:23

Это в основном скопировано из аналогичного ответа, который я дал ранее, но также на аналогичный вопрос. Приведенный ниже код запускался, когда я впервые его писал, но я изменил имена, чтобы они соответствовали этому вопросу, поэтому могут быть некоторые опечатки. Spring-data-jpa - это слой поверх JPA. У каждой сущности есть свой репозиторий, и вам придется с этим иметь дело. Для работы с отношениями «многие ко многим», особенно в spring-data-jpa, вы можете создать отдельный репозиторий для таблицы ссылок, если считаете это хорошей идеей.

@Entity
public class Item {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "item", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ItemCategory> categories;

@Entity
public class Category {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ItemCategory> items;

@Entity
public class ItemCategory {
    @EmbeddedId
    private ItemcategoryId id = new ItemcategoryId();

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("itemId")
    private Item Item;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("categoryId")
    private Category category;

    public ItemCategory() {}
    public ItemCategory(Item Item, Category category) {
        this.item = item;
        this.category = category;
    }

@SuppressWarnings("serial")
@Embeddable
public class ItemCategoryId implements Serializable {
    private Long itemId;
    private Long categoryId;
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        ItemCategoryId that = (ItemCategoryId) o;
        return Objects.equals(itemId, that.itemId) && Objects.equals(categoryId, that.categoryId);
    }
    @Override
    public int hashCode() {
        return Objects.hash(itemId, categoryId);
    }

И использовать это. Шаг 3 показывает, как вы это делаете в настоящее время, и создает чтение существующих объединений перед выполнением обновления. Шаг 4 просто вставляет отношение непосредственно в таблицу соединений и не вызывает предварительного чтения существующих соединений.

@Transactional
private void update() {
    System.out.println("Step 1");
    Category category1 = new Category();
    Item item1 = new Item();
    ItemCategory i1c1 = new ItemCategory(Item1, Category1);
    categoryRepo.save(Category1);
    ItemRepo.save(Item1);
    ItemCategoryRepo.save(p1t1);

    System.out.println("Step 2");
    Category category2 = new Category();
    Item item2 = new Item();
    ItemCategory p2t2 = new ItemCategory(item2, category2);
    ItemRepo.save(item2);
    categoryRepo.save(category2);
    ItemCategoryRepo.save(p2t2);

    System.out.println("Step 3");
    category2 = CategoryRepo.getOneWithitems(2L);
    category2.getitems().add(new ItemCategory(item1, category2));
    categoryRepo.save(Category2);

    System.out.println("Step 4 -- better");
    ItemCategory i2c1 = new ItemCategory(item2, category1);
    itemCategoryRepo.save(i2c1);
}

Я не устанавливаю явно идентификаторы ItemCategoryId. Они обрабатываются уровнем сохраняемости (в данном случае переходом в спящий режим).

Также обратите внимание, что вы можете обновлять записи ItemCategory либо явно, используя собственное репо, либо добавляя и удаляя их из списка, поскольку CascadeType.ALL установлен, как показано. Проблема с использованием CascadeType.ALL для spring-data-jpa заключается в том, что даже если вы предварительно выбираете объекты таблицы соединений, spring-data-jpa все равно сделает это снова. Попытка обновить отношения через CascadeType.ALL для новых сущностей проблематична.

Без CascadeType ни списки items, ни categories (которые должны быть наборами) не являются владельцами отношений, поэтому добавление к ним ничего не даст с точки зрения постоянства и будет только для результатов запроса.

При чтении отношений ItemCategory вам нужно специально их получить, поскольку у вас нет FetchType.EAGER. Проблема с FetchType.EAGER заключается в накладных расходах, если вам не нужны объединения, а также, если вы поместите его как на Category, так и на Item, тогда вы создадите рекурсивную выборку, которая получит все categories и items для любого запроса.

@Query("select c from Category c left outer join fetch c.items is left outer join fetch is.Item where t.id = :id")
Category getOneWithItems(@Param("id") Long id);

Другие вопросы по теме