Using transient objects to specify ids for saving ManyToOne relations

378 Views Asked by At

I want to save a lot of Spring JPA entities by importing them in batches to the database. Some entities have ManyToOne relations to other Entities. As there is a large amount of data, I'd prefer not to keep track of all of the related entities in memory to be set as ManyToOne relations for other entities. I just have the id of the relation, not a persisted entity.

I've encountered the following suggestion a few times as a solution for setting the relation anyway:

{
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
...
    public void setAuthorId(Long authorId) {
    Author author = new Author();
    author.setId(authorId);
    this.author = author;
}

So using a transient object as a placeholder for relating one entity to another. (assuming the related object gets saved as one of the other objects passed to the saveAll() call)

I see no reference of this approach at all in the official Spring documentation.

Is this considered a supported way to save relations based on just the id or would you say it's just a dirty hack?

1

There are 1 best solutions below

0
v.ladynev On

Using a transient object as a placeholder

My team was using such approach in production a lot working with Hibernate (without Spring Data). Didn't have any issues with it.

Drawbacks

Spring-data uses merge() inside repository save() method. Hibernate will generate additional (unnecessary) database SELECT query, to update a transient object state.

You can use a custom repository with persist()/update() methods to avoid an additional SELECT query. Described here:

The best Spring Data JpaRepository

Using getReferenceById() (since 2.7) / getReference() methods

We are using this approach a lot. It doesn't cause any additional database queries, if you get a reference and save an entity using the same persistent context (@Transactional scope).

Drawbacks

We need a repository to use getReferenceById() method. For example, you can't use this approach in a transient utility method of entity, like setAuthorId().

Using stateless session

Hibernate’s StatelessSession – What it is and how to use it

Using a custom repository with JdbcTemplate

To insert a lot of records better to use low level batch with SQL. We are using this approach a lot in production. Don't forget to add rewriteBatchedStatements=true to the connection URL, if you use MySQL.

@Repository
class NumbersRepositoryImpl implements NumbersRepositoryCustom {

    private static final int BATCH_SIZE = 1000;

    @PersistenceContext
    private EntityManager entityManager;

    private final JdbcTemplate template;

    @Autowired
    public NumbersRepositoryImpl(JdbcTemplate template) {
        this.template = template;
    }

    @Override
    public void batchSave(List<Number> numbers) {
        final String sql = "INSERT INTO NUMBERS " +
                "(number) " +
                "VALUES (?)";

        template.execute(sql, (PreparedStatementCallback<Void>) ps -> {
            int recordsCount = 0;

            for (Number number : numbers) {
                ps.setString(1, number.getValue());
                ps.addBatch();

                recordsCount++;
                if (recordsCount % BATCH_SIZE == 0) {
                    ps.executeBatch();
                }
            }

            ps.executeBatch();
            return null;
        });
    }

}