JDBC Transaction para Múltiplas Classes/Tabelas

Pessoal, estou desenvolvendo um sistema que pela regra de negócio, um cliente pode ter um ou dois enderecos (endereço de serviço e endereço de cobrança), um ou mais telefones e um ou mais animais de estimação. Assim, eu criei uma classe bean para cada uma dessas entidades além é claro um para as classes de ligação Enderecamento que liga o cliente aos endereços e ContatoTelefonico que liga o cliente aos telefones. Além disso, da mesma forma criei uma classe DAO para cada ume dessas entidades e das classes de ligação. Assim, por exemplo, na classe Telefone no pacote DAO tenho um método para cada função CRUD, ou seja, um de inserção, um de atualização, um de exclusão e um de pesquisa.
No botão salvar eu chamo cada um dos métodos dao responsável por persistir as entidades no banco, na seguinte ordem:

    idCliente = dao.Cliente.inserir(retornarCliente());
    codigosTelefones = dao.Telefone.inserir(telefonesAdcionados);
    codigosEnderecos = dao.Endereco.inserir(retornarLocal());
    dao.ContatoTelefonico.inserir(retornarContatoTelefonico());
    dao.Enderecamento.inserir(retornarEnderecamento());
    codigosAnimais = dao.Animal.inserir(retornarListaAnimais());

O problema é que eu desejo que todos esses métodos participem de uma mesma transação de modo que, caso haja algum problema de inserção não haja o commit, mas sim ou rollback. Tudo está funcionando, mas eu receio por travamentos do BD, etc. Como fazer que todos esses métodos sejam tratados de forma atômica, ou sej colocados em uma única transação do JDBC? Fazer uma classe gigante não parece uma boa ideia. Eu consigo trabalhar com transações em nível de classe (em cada dao), mas não entre diferentes classes. As tentivas que fiz dão erro de fk, como por exemplo executo o PreparedStatement do Cliente, mas não dou commit, daí a classe ContatoTelefonico dá erro de fk para Cliente.
Exemplo (fragmento de dao.Telefone):

public class Telefone {
    private static final int CAMPO_PK = 1;
    
    public static List<Integer> inserir(List<bean.Telefone> telefones) 
            throws SQLException{
        List<Integer> codigos = new ArrayList<>();
        try(Connection conectaBancoDeDados = ConectaBancoDeDados.getConexao()){
            String stringSQLInsercao = "INSERT INTO Telefones(tipo_telefone, "
                    + "numero_telefone, operadora, principal) "
                    + "VALUES(?, ?, ?, ?)";
            
            conectaBancoDeDados.setAutoCommit(false);
            
            try(PreparedStatement pstmt = conectaBancoDeDados
                    .prepareStatement(stringSQLInsercao, 
                            PreparedStatement.RETURN_GENERATED_KEYS)){
                for(bean.Telefone telefone : telefones){
                    pstmt.setString(1, telefone.getTipoTelefone());
                    pstmt.setString(2, telefone.getNumeroTelefone());
                    pstmt.setInt(3, telefone.getOperadora());
                    pstmt.setInt(4, telefone.getPrincipal());
                    pstmt.addBatch();
                }
                pstmt.executeBatch();
                conectaBancoDeDados.commit();
                
                try(ResultSet rstSet = pstmt.getGeneratedKeys()){
                    while(rstSet.next()){
                        codigos.add(rstSet.getInt(1));
                    }
                    rstSet.close();
                }
                
                pstmt.close();
            }
            conectaBancoDeDados.setAutoCommit(true);
            conectaBancoDeDados.close();
        }
        return codigos;
    }

Como resolver?
Desde já agradeço.

Mantenha esse estado de autoCommit como false!

Não aplique o commit até todos os métodos de inserção serem executados!

Sim, eu tentei isso, mas dá erro. Eu apliquei commit aqui para eu testar se estava funcionando. No caso, eu testei dando commit somente na última tabela, mas dá o erro citado. No caso dei commit na dao.Animal (última item a ser incluso), mas parece que o Cliente não é inserido, porque dá erro na em dao.ContatoTelefônico indicando problema com a fk para cliente.

Se está dando erro de fk é porquê a forma como você está tentando relacionar os registros deve estar de forma incorreta ou faltando algum detalhe!
Da uma revisada em modo debug e acompanhe a execução linha por linha.

Eu testei aqui… A chave está chegando. Eu dei um System.out.println() no PreparedStatement antes do execute e o SQL está montado certinho, vindo com o id e tudo do cliente:

Cli: 11
Pstmt: com.mysql.jdbc.JDBC4PreparedStatement@5ea72a25: INSERT INTO ContatoTelefonico(cliente, telefone) VALUES(11, 1)

O engraçado é que sei eu deixo para ‘commitar’ em cada tabela, funciona perfeitamente.

Você inicia a conexão com o banco em cada DAO separadamente?

Sim:

try(Connection conectaBancoDeDados = ConectaBancoDeDados.getConexao()){
   ....
}

A minha classe de conexão:

public class ConectaBancoDeDados {
    public static final String DRIVER = "com.mysql.jdbc.Driver";
    public static final String URL = "jdbc:mysql://localhost/"
            + "PetShop";
    public static final String USUARIO = "**********";
    public static final String SENHA = "*****";
   
    public static Connection getConexao() throws SQLException{
        try{
            Class.forName(DRIVER);
                return DriverManager.getConnection(URL, USUARIO, SENHA);
        }catch(ClassNotFoundException cnfe){
            throw new SQLDataException(cnfe.getMessage());
        }
    }
    
    public static void main(String[] args){
        try{
            getConexao();
            System.out.println("Banco de dados conectado com sucesso!");
        }catch(SQLException sqle){
            System.out.println("Impossível se conectar ao banco de dados. "
                    + "ERRO: " + sqle.getMessage());
        }
    }
}

Se cada classe DAO cria uma conexão nova separada, as classes não tem como se conversarem no quesito transações!
Pode ser esse o fator do seu código não funcionar como esperado.

1 curtida

Pois é, eu havia pensado nisso, só não sei como usar a mesma conexão em cada classe.

public class DatabaseConnection {

    private static DatabaseConnection instance;
    private Connection connection;
    private String url = "jdbc:mysql://localhost/PetShop";
    private String username = "**********";
    private String password = "*****";

    private DatabaseConnection() throws SQLException {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            this.connection = DriverManager.getConnection(url, username, password);
            this.connection.setAutoCommit(false);
        } catch (ClassNotFoundException ex) {
            System.out.println("Database Connection Creation Failed : " + ex.getMessage());
        }
    }

    public Connection getConnection() {
        return connection;
    }

    public static DatabaseConnection getInstance() throws SQLException {
        if (instance == null) {
            instance = new DatabaseConnection();
        } else if (instance.getConnection().isClosed()) {
            instance = new DatabaseConnection();
        }

        return instance;
    }
}

Usa uma classe de conexão Singleton igual no exemplo acima, depois só fazer a chamada nas classes pra utilizar da seguinte forma.

private DatabaseConnection dbcon = DatabaseConnection.getInstance();
private Connetion con = dbcon.getConnetcion();
1 curtida

Eu consegui resolver. Era o fato de haver uma chamada a uma nova conexão ao banco de dados em cada entidade do pacote DAO. Também tive que readequar o try de try-with-resource para try-catch, já que o primeiro fecha automaticamente o recurso.