Как сохранить несколько объектов в одном действии, связанном в отношениях «Многие ко многим» [Spring Boot 2, JPA, Hibernate, PostgreSQL] - PullRequest
0 голосов
/ 01 мая 2020

Я пишу здесь, чтобы иметь некоторую подсказку о решении, которое делает, короче говоря, проблема, с которой я столкнулся, такова: у меня есть две сущности в двунаправленной связи многие ко многим, например, у меня есть следующие сообщение и тег entity:

@Data
@Entity
@Table(name = "posts")
public class Post {


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

    /*...*/

    @ManyToMany( cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH} )
    @JoinTable(name = "post_tag", 
            joinColumns = @JoinColumn(name = "post_id", referencedColumnName = "id"), 
            inverseJoinColumns = @JoinColumn(name = "tag_id", referencedColumnName = "id"))
    @JsonIgnoreProperties("posts")
    private Set<Tag> tags = new HashSet<>();

}

@Data
@Entity
@Table(name = "tags")
public class Tag {

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

    @NaturalId
    private String text;

    @ManyToMany(mappedBy = "tags")//, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
    @JsonIgnoreProperties("tags")
    private Set<Post> posts = new HashSet<>();

}

Моя проблема в том, что в HTTP POST Action я получаю данные для поста и коллекцию связанных с ним тегов, и мне нужно сохранить все с условием не дублировать сущность тегов если «текст» уже присутствует в базе данных. Предполагая, что у нас есть карта с заданными данными, код выглядит следующим образом:

Post post = new Post();
String heading = (String) payload.get("heading");
String content = (String) payload.get("content");
post.setHeading(heading);
post.setContent(content);
Set<Tag> toSaveTags = new HashSet<Tag>();
List list = (List) payload.get("tags");
for (Object o : list) {
    Map map = (Map) o;
    String text = (String) map.get("text");
    Tag tag = new Tag();
    tag.setText(text);
    post.getTags().add(tag);
    tag.getPosts().add(post);
    log.info("post has {}# tag", post.getTags().size());
    toSaveTags.add(tag);
};
//method to save it all
postRepository.saveWithTags(post, toSaveTags);

Мое решение состояло в том, чтобы разработать класс репозитория с помощью метода, показанного выше, следующим образом:

@Repository
public class PostTagsRepositoryImpl implements PostTagsRepository {

    @Autowired
    private EntityManagerFactory emf;

    @Override
    public Post saveWithTags(Post post, Collection<Tag> tags) {
        EntityManager entityManager = emf.createEntityManager();
        post.getTags().clear();
        for (Tag tag : tags) {

            tag.getPosts().clear();
            Tag searchedTag = null;
            try {
                searchedTag = entityManager.createQuery(
                        "select t "
                        + "from Tag t "
                        + "join fetch t.posts "
                        + "where t.text = :text", Tag.class)
                        .setParameter("text", tag.getText())
                        .getSingleResult();
            } catch (NoResultException e) {/* DO NOTHING */}
            if (searchedTag == null) {
                post.getTags().add(tag);
                tag.getPosts().add(post);
            } else {
                entityManager.getTransaction().begin();
                entityManager.detach(searchedTag);

                post.getTags().add(searchedTag);
                searchedTag.getPosts().add(post);
                entityManager.merge(searchedTag);
                entityManager.getTransaction().commit();
            }
        }

        entityManager.getTransaction().begin();
        entityManager.merge(post);
        entityManager.getTransaction().commit();
        return post;
    }

}

Мои вопросы: могу ли я реализовать это лучше? может быть, в одном запросе / транзакции? Не могли бы вы дать мне несколько советов?

1 Ответ

1 голос
/ 02 мая 2020

Некоторые моменты:

  • вы связываете обе сущности, а затем clear отношения в хранилище. Поскольку у вас нет инвариантов между ними, первая связь бесполезна.

  • возможно в одном запросе / транзакции?

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

  • К сожалению, каскадное слияние не будет работать с naturalid, поэтому вы должны сами создать такое поведение. Поэтому для каждого тега проверьте, существует ли он:
Session session = entityManager.unwrap(Session.class);

Tag t= session.bySimpleNaturalId(Tag.class).load(“some txt”);

В зависимости от результата вы должны загрузить в объект Post существующий тег один (уже на БД и восстановить через bySimpleNaturalId) или новый. Тогда каскадное слияние на Post сделает все остальное.

  • Вы всегда создаете нового менеджера сущностей при каждом обращении к вашему хранилищу. Вы должны преодолеть это путем непосредственного внедрения общего менеджера сущностей.
@Autowired
Private EntityManager em;

Это потокобезопасно.

...