Search code examples
springjavafxspring-bootautowiredtransactional

JavaFX + Spring (JDBC & @SpringBootApplication & @Autowired & @Transactional)


I'd like to use JavaFX with DB access using Spring JDBC. However I'm completely new to Spring and it seems that I cannot fully understand it's features, especially transactions handling...

I've added following dependencies to my project:

compile 'org.springframework.boot:spring-boot-starter-jdbc'
runtime 'mysql:mysql-connector-java'

... and I want to use Spring transactions handling mechanism when GUI application is doing it's operations on DB. As I understand it, following code should:

  • initialize and start JavaFX application - create and show GUI wireframe
  • initialize Spring
  • configure and inject JdbcTemplate dependency
  • start transaction handling mechanism and begin transaction
  • use jdbcTemplate object to create 5 entries in DB in for loop
  • simulate error (by throwing RuntimeException)
  • revert operations on DB
  • exit

So, summing up: when RuntimeException is thrown in method annotated as @Transactional that should revert all entries already created by this method before application quits, isn't it?

However all created entries stay permanently in DB (I can see them there after application quits). So first of all - am I understanding correctly how these transactions should work? If so, then how to make them actually work as I expect?

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;


@SpringBootApplication
public class SpringTransactional extends Application {
    private Pane viewPane;

    private ConfigurableApplicationContext springContext;

    /** application.properties:
     spring.datasource.driver-class-name = com.mysql.jdbc.Driver
     spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
     spring.datasource.username = db_username
     spring.datasource.password = username123
     */
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() throws Exception {
        springContext = SpringApplication.run(SpringTransactional.class);
        springContext.getAutowireCapableBeanFactory().autowireBean(this);
    }

    @Override
    public void stop() throws Exception {
        springContext.close();
    }

    @Override
    public void start(Stage primaryStage) {
        viewPane = assembleView(primaryStage);

        try {
            db_transaction_test();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        Platform.exit();
    }

    private Pane assembleView(Stage primaryStage) {
        VBox rootPane = new VBox();
        rootPane.setSpacing(10);
        rootPane.setPadding(new Insets(10));
        rootPane.setStyle("-fx-base: #84a7ad;");
        rootPane.getChildren().add(new Label("GUI goes here."));

        primaryStage.setScene(new Scene(rootPane));
        primaryStage.setResizable(false);
        primaryStage.show();

        return rootPane;
    }

    @Transactional
    private void db_transaction_test() {
        for (int i = 0; i < 10; i++) {
            try {
                int entry_name = getEntryId("entry_" + i);
                System.out.println("Created entry id=" + entry_name);
            } catch (DaoException e) {
                e.printStackTrace();
            }

            if (i == 5) {
                throw new RuntimeException("Testing data upload procedure break.");
            }
        }
    }

    /** DB creation and schema:
     CREATE DATABASE db_name;
     CREATE USER db_username;

     USE db_name;
     GRANT ALL ON db_name.* TO db_username;

     SET PASSWORD FOR spz = PASSWORD('username123');
     FLUSH PRIVILEGES;

     CREATE TABLE Entry (
     entry_ID INT NOT NULL AUTO_INCREMENT,
     name   TEXT NOT NULL,

     PRIMARY KEY (entry_ID)
     );
     */
    private int getEntryId(String entryName) throws DaoException {
        List<DbEntry> dbEntries = retrieveEntriesFor(entryName);

        if (dbEntries.size() == 1) {
            return dbEntries.get(0).getEntry_ID();
        } else if (dbEntries.size() == 0) {
            String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
            jdbcTemplate.update(sqlInsert, entryName);
            dbEntries = retrieveEntriesFor(entryName);
            if (dbEntries.size() == 1) {
                return dbEntries.get(0).getEntry_ID();
            } else {
                throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
            }
        } else {
            throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
        }
    }

    private List<DbEntry> retrieveEntriesFor(String entryName) {
        return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
    }

    private DbEntry unMarshal(ResultSet result) throws SQLException {
        DbEntry dbEntry = new DbEntry();
        dbEntry.setEntry_ID(result.getInt("entry_ID"));
        dbEntry.setName(result.getString("name"));
        return dbEntry;
    }

    public class DbEntry {
        private int entry_ID;
        private String name;

        int getEntry_ID() { return entry_ID; }
        void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    }

    private class DaoException extends Throwable {
        DaoException(String err_msg) { super(err_msg); }
    }
}

Solution

  • After more testing it seems that creating separate Spring component EntryDao works (thanks James_D) but only if db_transaction_test annotaded with @Transactional is in that class - OPTION A in code below.

    But what I'm really interested in is OPTION B - when db_transaction_test annotaded with @Transactional is in another class. This is because DAO class does not (and should not) know about DB-unrealted issues that are the reason of reverting a bunch of previous DB operations. This information comes from other 'controllers' which failrues must not cause data integrity issues. So in the example below SpringTransactional should be the only one that can throw this particular RuntimeException("Testing data upload procedure break."); (as an example of real-life system/environment issues). However as stacktrace at the end shows - transaction is not initalized there.

    So is there a way to get it work as I need with Spring @Transactional (aka. declarative transactions) or only with manual (aka. programmatic) Spring transactions control? And if this is the only way then how to configure DataSourceTransactionManager while using @SpringBootApplication for "auto-configuration" and @Autowired for jdbcTemplate object?

    Main class:

    package tmp;
    
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Label;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.transaction.annotation.Transactional;
    import tmp.dao.EntryDao;
    
    
    @SpringBootApplication
    public class SpringTransactional extends Application {
        private Pane viewPane;
    
        private ConfigurableApplicationContext springContext;
    
        @Autowired
        private EntryDao dao;
    
        public static void main(String[] args) { launch(args); }
    
        @Override
        public void init() throws Exception {
            springContext = SpringApplication.run(SpringTransactional.class);
            springContext.getAutowireCapableBeanFactory().autowireBean(this);
        }
    
        @Override
        public void stop() throws Exception { springContext.close(); }
    
        @Override
        public void start(Stage primaryStage) {
            viewPane = assembleView(primaryStage);
    
            // OPTION A:
            try {
                dao.db_transaction_test();
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            // OPTION B:
            try {
                db_transaction_test();
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
    
            Platform.exit();
        }
    
        @Transactional
        private void db_transaction_test() {
            for (int i = 0; i < 10; i++) {
                try {
                    int entry_name = dao.getEntryId("entry_" + i);
                    System.out.println("Created entry id=" + entry_name);
                } catch (EntryDao.DaoException e) {
                    e.printStackTrace();
                }
    
                if (i == 5) {
                    throw new RuntimeException("Testing data upload procedure break.");
                }
            }
        }
    
        private Pane assembleView(Stage primaryStage) {
            VBox rootPane = new VBox();
            rootPane.setSpacing(10);
            rootPane.setPadding(new Insets(10));
            rootPane.setStyle("-fx-base: #84a7ad;");
            rootPane.getChildren().add(new Label("GUI goes here."));
    
            primaryStage.setScene(new Scene(rootPane));
            primaryStage.setResizable(false);
            primaryStage.show();
    
            return rootPane;
        }
    }
    

    EntryDao class:

    package tmp.dao;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.List;
    
    /**
     * DB creation and schema:
     * CREATE DATABASE db_name;
     * CREATE USER db_username;
     * <p>
     * USE db_name;
     * GRANT ALL ON db_name.* TO db_username;
     * <p>
     * SET PASSWORD FOR spz = PASSWORD('username123');
     * FLUSH PRIVILEGES;
     * <p>
     * CREATE TABLE Entry (
     * entry_ID INT NOT NULL AUTO_INCREMENT,
     * name   TEXT NOT NULL,
     * <p>
     * PRIMARY KEY (entry_ID)
     * );
     */
    @Component
    public class EntryDao {
        /**
         * application.properties:
         * spring.datasource.driver-class-name = com.mysql.jdbc.Driver
         * spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
         * spring.datasource.username = db_username
         * spring.datasource.password = username123
         */
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Transactional
        public void db_transaction_test() {
            for (int i = 0; i < 10; i++) {
                try {
                    int entry_name = getEntryId("entry_" + i);
                    System.out.println("Created entry id=" + entry_name);
                } catch (EntryDao.DaoException e) {
                    e.printStackTrace();
                }
    
                if (i == 5) {
                    throw new RuntimeException("Testing data upload procedure break.");
                }
            }
        }
    
        public int getEntryId(String entryName) throws DaoException {
            List<DbEntry> dbEntries = retrieveEntriesFor(entryName);
    
            if (dbEntries.size() == 1) {
                return dbEntries.get(0).getEntry_ID();
            } else if (dbEntries.size() == 0) {
                String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
                jdbcTemplate.update(sqlInsert, entryName);
                dbEntries = retrieveEntriesFor(entryName);
                if (dbEntries.size() == 1) {
                    return dbEntries.get(0).getEntry_ID();
                } else {
                    throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
                }
            } else {
                throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
            }
        }
    
        private List<DbEntry> retrieveEntriesFor(String entryName) {
            return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
        }
    
        private DbEntry unMarshal(ResultSet result) throws SQLException {
            DbEntry dbEntry = new DbEntry();
            dbEntry.setEntry_ID(result.getInt("entry_ID"));
            dbEntry.setName(result.getString("name"));
            return dbEntry;
        }
    
        public class DbEntry {
            private int entry_ID;
            private String name;
    
            int getEntry_ID() { return entry_ID; }
            void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
            public String getName() { return name; }
            public void setName(String name) { this.name = name; }
        }
    
        public class DaoException extends Throwable { DaoException(String err_msg) { super(err_msg); } }
    }
    

    STACKTRACE

      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v1.4.3.RELEASE)
    
    2017-01-10 09:41:48.902  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Starting application on alwihasolaptop with PID 1860 (started by alwi in C:\alwi\Workspace_SPZ\GCodeClient)
    2017-01-10 09:41:48.905  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
    2017-01-10 09:41:48.965  INFO 1860 --- [JavaFX-Launcher] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@18660f3: startup date [Tue Jan 10 09:41:48 CET 2017]; root of context hierarchy
    2017-01-10 09:41:49.917  INFO 1860 --- [JavaFX-Launcher] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
    2017-01-10 09:41:49.927  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Started application in 1.384 seconds (JVM running for 1.969)
    Created entry id=73
    Created entry id=74
    Created entry id=75
    Created entry id=76
    Created entry id=77
    Created entry id=78
    java.lang.RuntimeException: Testing data upload procedure break.
        at tmp.dao.EntryDao.db_transaction_test(EntryDao.java:53)
        at tmp.dao.EntryDao$$FastClassBySpringCGLIB$$a857b433.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656)
        at tmp.dao.EntryDao$$EnhancerBySpringCGLIB$$84e8651e.db_transaction_test(<generated>)
        at tmp.SpringTransactional.start(SpringTransactional.java:45)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
        at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
        at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
        at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
        at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
        at java.lang.Thread.run(Thread.java:745)
    Created entry id=73
    Created entry id=74
    Created entry id=75
    Created entry id=76
    Created entry id=77
    Created entry id=78
    2017-01-10 09:41:50.545  INFO 1860 --- [lication Thread] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@18660f3: startup date [Tue Jan 10 09:41:48 CET 2017]; root of context hierarchy
    java.lang.RuntimeException: Testing data upload procedure break.
        at tmp.SpringTransactional.db_transaction_test(SpringTransactional.java:71)
        at tmp.SpringTransactional.start(SpringTransactional.java:52)
        at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
        at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
        at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
        at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
        at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
        at java.lang.Thread.run(Thread.java:745)
    2017-01-10 09:41:50.546  INFO 1860 --- [lication Thread] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
    
    Process finished with exit code 0
    

    SOLUTION:

    Best solution I've found so far is using Spring TransactionTemplate together with additional callback class:

    package tmp.dao;
    
    public abstract class DbTransactionTask { public abstract void executeTask(); }
    

    and in SpringTransactional class db_transaction_test() method (note that @Transactional is out):

    private void db_transaction_test() {
        DbTransactionTask dbTask = new DbTransactionTask() {
            @Override
            public void executeTask() {
                for (int i = 0; i < 10; i++) {
                    try {
                        int entry_name = dao.getEntryId("entry_" + i);
                        System.out.println("Created entry id=" + entry_name);
                    } catch (EntryDao.DaoException e) {
                        e.printStackTrace();
                    }
    
                    if (i == 5) {
                        throw new RuntimeException("Testing data upload procedure break.");
                    }
                }
            }
        };
    
        dao.executeTransactionWithoutResult(dbTask);
    }
    

    EntryDao class requires this additional code:

    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void executeTransactionWithoutResult(DbTransactionTask dbTask) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                dbTask.executeTask();
            }
        });
    }