0

I have written an application using Springboot 2.0 to store books catalog in MySQL 8 database. Book information is being read from the file.

Book object has a list of authors as there can be more than one author to the book. At the same time, author can have many books. Therefore, I have many-to-many relationship and created three database tables: BOOKS, AUTHORS and AUTHORS_BOOKS. In my transaction, I first check if author already exist in the database and if not, inserting the author record. Than I store book info and at the end store relationship (IDs of two previous records) in the AUTHORS_BOOKS table. By default auto-commit is set to true and everything works fine. However, if I set it to false, I am getting org.springframework.dao.DataIntegrityViolationException while inserting those last records. Apparently, it can't read book and author ID inserted within same transaction.

Here is my application.properties:

spring.datasource.hikari.transaction-isolation=TRANSACTION_READ_UNCOMMITTED
spring.datasource.hikari.auto-commit: false
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.pool-name="con-pool"
spring.datasource.hikari.minimumIdle=2
spring.datasource.hikari.maximumPoolSize=4
spring.datasource.hikari.idleTimeout=30000
spring.datasource.hikari.maxLifetime=2000000
spring.datasource.hikari.connectionTimeout=60000

And here is the code:

@Transactional
public String storeCatalogEntry(Book book, String serId, short volNum) throws SQLException {
    List<String> authorsId = storeAuthors(book.getAuthors());
    String bookId = storeBookData(book, serId, volNum);
    storeBookAuthorRefs(bookId, authorsId);
    return bookId;
}

I am getting nested exception is java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (catalog_db.authors_books, CONSTRAINT FK_BOOK_AUTHORS FOREIGN KEY (BOOK_ID) REFERENCES books (BOOK_ID) ON DELETE CASCADE ON UPDATE CASCADE) at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:251)

I did try to do @Transactional(isolation = Isolation.READ_UNCOMMITED) but result is the same. I am using Spring JdbcTemplate to perform database operations. Here is my code:

    private List<String> storeAuthors(Collection<Author> authors) throws SQLException {
    List<String> ids = new ArrayList<>(authors.size());
    for (Author author : authors) {
        String id = findAuthor(author);
        if (id == null) {
            id = storeNewAuthor(author);
        }
        ids.add(id);
    }
    return ids;
}
private String storeNewAuthor(Author author) throws SQLException {
    String id = UUID.randomUUID().toString();
    String qry = queries.get("storeAuthor");
    try {
        jdbc.update(qry, (PreparedStatement ps) -> {
            ps.setString(1, id);
            if (author.getFirstName() == null) {
                ps.setNull(2, Types.VARCHAR);
            } else {
                ps.setString(2, author.getFirstName());
            }
            ps.setString(3, author.getLastName());
        });
    } catch (DataAccessException daex) {
        LOG.error("JDBC error", daex);
        throw new SQLException(daex);
    }
    return id;
}

private String storeBookData(Book book, String serialId, short volumeNumber) throws SQLException {
    String id = UUID.randomUUID().toString();
    String qry = queries.get("storeBookData");
    try {
        jdbc.update(qry, (PreparedStatement ps) -> {
            ps.setString(1, id);
            ps.setString(2, book.getTitle());
            ps.setString(3, book.getUrl());
            ...
            ps.setString(8, book.getAnnotation());
            ps.setString(9, serialId);
            ps.setShort(10, volumeNumber);
        });
    } catch (DataAccessException daex) {
        LOG.error("JDBC error", daex);
        throw new SQLException(daex);            
    }
    return id;
}

private void storeBookAuthorRefs(String bookId, List<String> authorsId) throws SQLException {
    String qry = queries.get("storeBookAuthorRef");
    for (int i = 0; i < authorsId.size(); i++) {
        String authId = authorsId.get(i);
        short v = (short) i;
        try {
            jdbc.update(qry, (PreparedStatement ps) -> {
                ps.setString(1, bookId);
                ps.setString(2, authId);
                ps.setShort(3, v);
            });
        } catch (DataAccessException daex) {
            LOG.error("JDBC error", daex);
            throw new SQLException(daex);
        }
    }
}

Can anyone suggest what I am doing wrong and how to fix it?

Gary Greenberg
  • 468
  • 1
  • 9
  • 22
  • READ_UNCOMMITTED won't help as that is for between transactions. You can read everything inside a single transaction that has been entered into the database. So there must be an error in your code. So please show the `storeAuthors` and `storeBookData` and `storeBookAuthorRefs` implementations. The fact that auto-commit influences things makes me wonder if your `@Transactional` is even owrking. How is the `storeCatalogEntry` being called? – M. Deinum Sep 29 '22 at 08:21
  • I did add the code, however, if there would be an error in the code, it shouldn't work with auto-commit on – Gary Greenberg Sep 30 '22 at 18:41
  • As stated the fact that it works with auto-commit on is an indication there is an error in your setup as transactions aren't working. Else you would be able to read it as you can read the modified data inside the transaction . What is that `SQLException` you are throwing? – M. Deinum Sep 30 '22 at 18:50
  • I am rethrowing Spring DataAccessException just to convert unchecked exception into checked one. The underlaying exception I specified in my original post. It is java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (catalog_db.authors_books, CONSTRAINT FK_BOOK_AUTHORS FOREIGN KEY (BOOK_ID) REFERENCES books (BOOK_ID) ON DELETE CASCADE ON UPDATE CASCADE) . I do know that something is wrong in my setup, that's why I asked question in the first place. I need a tip, what is wrong. So far, you didn't tell me anything useful. – Gary Greenberg Oct 02 '22 at 03:17
  • I told you plenty of useful stuff but you are caught in your own thinking circle. As stated the fact that it works **with autocommit enabled** and **not with `@Transactional`** means something is wrong with your setup. If you are catching and rethrowing you are actually breaking tx support as only runtime exceptions are rolled back not the others (unless you configured it so, which you didn't). If `@Transactional` isn't working you ar either using the wrong tx maanger or you are calling this method internally and not externally (i.e. from within the same class instead of from another class). – M. Deinum Oct 02 '22 at 18:18
  • Now I do hear something useful. You are saying that @Transactional is not working if method is called from the same file. I didn't know that. Javadoc for the annotation does not say it. That is exactly my case. I am reading and processing XML file and preparing a list of book objects. Than I am passing it to the repository class in the method call, which in its own turn for each element in the list calls StoreCatalogEntry method. I need a separate transaction for each entry, as some entries might contain incorrect information and I need to just skip these entries (with proper logging). – Gary Greenberg Oct 03 '22 at 04:23
  • It is still not clear why it can't find just inserted record and fails integrity constraint. Can you please recommend how to fix the problem? What shall I use instead of @Transactional annotation? – Gary Greenberg Oct 03 '22 at 04:48
  • Wrap the method call in a `TransactionTemplate` or move it to a different class. `@Transactional` is applied using AOP, as Spring uses proxies (by default) AOP doesn't work when applied to the same class. This isn't an `@Transactional` thing but a shortcoming from proxy based AOP (this has been the case since the day and age of EJB1). – M. Deinum Oct 03 '22 at 05:43

0 Answers0