2009-08-11 2 views
3

Я написал небольшой тест с единственной целью, чтобы лучше понимать транзакции в jdbc. И хотя я сделал все согласно документации, тест не хочет работать нормально.Операции Java sql. Что я делаю не так?

Вот структура таблицы:

CREATE TABLE `default_values` (
    `id` INT UNSIGNED NOT auto_increment, 
    `is_default` BOOL DEFAULT false, 
    PRIMARY KEY(`id`) 
); 

Тест содержит 3 класса:

public class DefaultDeleter implements Runnable 
{ 

    public synchronized void deleteDefault() throws SQLException 
    { 
     Connection conn = null; 
     Statement deleteStmt = null; 
     Statement selectStmt = null; 
     PreparedStatement updateStmt = null; 
     ResultSet selectSet = null; 

     try 
     { 
      conn = DriverManager.getConnection("jdbc:mysql://localhost/xtest", "root", ""); 
      conn.setAutoCommit(false); 
      conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); 

      // Deleting current default entry 
      deleteStmt = conn.createStatement(); 
      deleteStmt.executeUpdate("DELETE FROM `default_values` WHERE `is_default` = true"); 

      // Selecting first non default entry 
      selectStmt = conn.createStatement(); 
      selectSet = selectStmt.executeQuery("SELECT `id` FROM `default_values` ORDER BY `id` LIMIT 1"); 

      if (selectSet.next()) 
      { 
       int id = selectSet.getInt("id"); 

       // Updating found entry to set it default 
       updateStmt = conn.prepareStatement("UPDATE `default_values` SET `is_default` = true WHERE `id` = ?"); 
       updateStmt.setInt(1, id); 
       if (updateStmt.executeUpdate() == 0) 
       { 
        System.err.println("Failed to set new default value."); 
        System.exit(-1); 
       } 
      } 
      else 
      { 
       System.err.println("Ooops! I've deleted them all."); 
       System.exit(-1); 
      } 

      conn.commit(); 
      conn.setAutoCommit(true); 
     } 
     catch (SQLException e) 
     { 
      try { conn.rollback(); } catch (SQLException ex) 
      { 
       ex.printStackTrace(); 
      } 

      throw e; 
     } 
     finally 
     { 
      try { selectSet.close(); } catch (Exception e) {} 
      try { deleteStmt.close(); } catch (Exception e) {} 
      try { selectStmt.close(); } catch (Exception e) {} 
      try { updateStmt.close(); } catch (Exception e) {} 
      try { conn.close(); } catch (Exception e) {} 
     } 
    } 

    public void run() 
    { 
     while (true) 
     { 
      try 
      { 
       deleteDefault(); 
      } 
      catch (SQLException e) 
      { 
       e.printStackTrace(); 
       System.exit(-1); 
      } 

      try 
      { 
       Thread.sleep(20); 
      } 
      catch (InterruptedException e) {} 
     } 
    } 

} 

public class DefaultReader implements Runnable 
{ 

    public synchronized void readDefault() throws SQLException 
    { 
     Connection conn = null; 
     Statement stmt = null; 
     ResultSet rset = null; 

     try 
     { 
      conn = DriverManager.getConnection("jdbc:mysql://localhost/xtest", "root", ""); 

      conn.setAutoCommit(false); 
      conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); 

      stmt = conn.createStatement(); 
      rset = stmt.executeQuery("SELECT * FROM `default_values` WHERE `is_default` = true"); 

      int count = 0; 
      while (rset.next()) { count++; } 

      if (count == 0) 
      { 
       System.err.println("Default entry not found. Fail."); 
       System.exit(-1); 
      } 
      else if (count > 1) 
      { 
       System.err.println("Count is " + count + "! Wtf?!"); 
      } 

      conn.commit(); 
      conn.setAutoCommit(true); 
     } 
     catch (SQLException e) 
     { 
      try { conn.rollback(); } catch (Exception ex) 
      { 
       ex.printStackTrace(); 
      } 

      throw e; 
     } 
     finally 
     { 
      try { rset.close(); } catch (Exception e) {} 
      try { stmt.close(); } catch (Exception e) {} 
      try { conn.close(); } catch (Exception e) {} 
     } 
    } 

    public void run() 
    { 
     while (true) 
     { 
      try 
      { 
       readDefault(); 
      } 
      catch (SQLException e) 
      { 
       e.printStackTrace(); 
       System.exit(-1); 
      } 

      try 
      { 
       Thread.sleep(20); 
      } 
      catch (InterruptedException e) {} 
     } 
    } 

} 

public class Main 
{ 

    public static void main(String[] args) 
    { 
     try 
     { 
      Driver driver = (Driver) Class.forName("com.mysql.jdbc.Driver") 
        .newInstance(); 
      DriverManager.registerDriver(driver); 

      Connection conn = null; 
      try 
      { 
       conn = DriverManager.getConnection("jdbc:mysql://localhost/xtest", "root", ""); 
       System.out.println("Is transaction isolation supported by driver? " + 
         (conn.getMetaData() 
         .supportsTransactionIsolationLevel(
         Connection.TRANSACTION_SERIALIZABLE) ? "yes" : "no")); 
      } 
      finally 
      { 
       try { conn.close(); } catch (Exception e) {} 
      } 

      (new Thread(new DefaultReader())).start(); 
      (new Thread(new DefaultDeleter())).start(); 

      System.in.read(); 
      System.exit(0); 
     } 
     catch (Exception e) 
     { 
      e.printStackTrace(); 
     } 
    } 

} 

Я написал скрипт, который заполняет таблицу с 100к записей (где один из них по умолчанию) для каждого прогона , Но каждый раз, когда я запускаю этот тест, вывод:

Является ли изоляция транзакций поддерживаемой водителем? да

По умолчанию запись не найдена. Потерпеть неудачу.

Что не так с этим кодом?

+0

Я вижу, что некоторые ответы начинают говорить о декларативных транзакциях и использовать Spring (мне нравится Spring), но они просто путают ситуацию. Эта проблема разрешима, и вы получите лучшее понимание работы с необработанным JDBC, что будет скрыто, если вы начнете обертывать его в рамки более высокого уровня (например, JEE или Spring). Я бы предложил в долгосрочной перспективе НЕ делать сырой JDBC, если вы не находитесь в действительно ограниченной среде. Он подвержен ошибкам и PITA. – SteveD

+0

Почему вы предполагаете, что DefaultReader должен преуспеть? Просто потому, что поток запущен до делетира, не означает, что он будет выполняться до удаления? Имейте в виду, что таблицы MyISAM не поддерживают транзакции, это делает таблицы InnoDB. Уровень изоляции SERIALIZABLE может завершиться неудачно, для dbs обычно приходится отказываться от транзакций, если происходит 2 параллельных транзакции SERIALIZABLE. – nos

ответ

3

Пожалуйста, убедитесь, что вы создаете таблицы InnoDB, MyISAM (по умолчанию) не поддерживает транзакции. Вы можете изменить дб создать для этого:

CREATE TABLE `default_values` (
    `id` INT UNSIGNED NOT auto_increment, 
    `is_default` BOOL DEFAULT false, 
    PRIMARY KEY(`id`) 
) Engine=InnoDB; 

другой пример: MySQL transaction with accounting application

+0

Я ценю, что все приведенные выше комментарии помогают вам, но я бы посоветовал вам начать с того, что вы используете таблицу, совместимую с транзакцией, - тогда появляется точка Аарона Дигуллы: какая нить попадает в db в первую очередь. После того, как вы используете транзакционные таблицы, вы можете поместить спать в поток писем (напишите 100 строк в типе времени), затем убедитесь, что удаление удаляет среднюю транзакцию потока записи. Вы должны видеть транзакции, делающие то, что они делают в этот момент. – Mike

+0

Хорошая точка. Я проверю это. – Gris

+0

Кажется, что я действительно сделал такую ​​ошибку noobish. После смены двигателя на приложение InnoDB кажется, что он работает хорошо. Спасибо. – Gris

0

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

@Resource 
private UserTransaction utx; 

, а затем просто использовать его в коде:

utx.begin(); 

// atomic operation in here 

utx.commit(); 

Тогда вам не нужно беспокоиться о тонкостях управления транзакциями.

Редактировать: @Gris: Да, вы правы в этом. Я предположил, что вы разрабатываете веб-приложение. как сказал pjp, весна - хорошая альтернатива в этом случае. Или - в зависимости от размера и сложности приложения - вы могли бы справиться с управлением своими собственными транзакциями.

+1

Но если я правильно понял, это можно использовать только в том случае, если приложение запускается внутри контейнера j2ee. Но моя цель - автономное приложение j2se, а не webapp. – Gris

+1

В качестве альтернативы полностью раздутой j2ee вы можете использовать Spring, который реализует TransactionManager DataSource. – pjp

+0

2 pjp: Возможно, вы правы. Я посмотрю документацию, но я не уверен, что использование такой структуры будет приемлемо из других точек. – Gris

0

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

Также проверьте, что использование «истина» действителен в MySql над числовым значением 1 для булевского типа.

+0

Все операции сами по себе идут правильно. Использование «истины» тоже хорошо (оно используется не как текст (в скобках), а как встроенная константа). Сначала я проверил все запросы от консоли MySQL. – Gris

0

Ответ прост: вы создаете два потока. Они работают совершенно независимо друг от друга. Поскольку вы никоим образом не синхронизируете их, невозможно определить, кто из них попадает в базу данных. Если читатель является первым, дебетер не будет начат, но не будет предмета с is_default == true, так как делетер еще не дошел до этого.

Далее вы полностью изолировали две транзакции (Connection.TRANSACTION_SERIALIZABLE). Это означает, что даже если у дебетера есть возможность обновить базу данных, читатель увидит его только после того, как он закрыл свое соединение и открыл новый.

И если это не так, то дебетер медленнее, чем у читателя, поэтому вероятность того, что запись будет обновлена ​​с is_default == true в то время, когда читатель ее ищет, тонкие.

[EDIT] Теперь вы говорите, что при запуске тестов должен быть один элемент с is_default == true. Добавьте тест, чтобы убедиться, что это действительно так, прежде чем вы начнете два потока. В противном случае вы можете искать неправильную ошибку.

+0

О потоках - это одно из требований. Тест работает как надежное приложение, но окончательное приложение может состоять из нескольких процессов, которые полностью независимы. О транзакциях - как я понимаю, полностью изолированная транзакция должна обеспечивать атомарность замкнутых операций, поэтому все операции внутри обрабатываются как одна сплошная операция (DELETE, SELECT, UPDATE в моем случае). Я ошибаюсь? – Gris

+0

Я использую скрипт, который очищает и заполняет таблицу перед каждым запуском. Я также проверил условия запуска вручную. – Gris

0

Там есть несколько моментов стоит отметить:

  1. ли ваш скрипт для заполнения базы данных перед испытанием на самом деле Работа? Попробуйте сделать select count(*) ... на столе из кода Java, чтобы проверить (это может показаться глупым, но я сделал эту ошибку раньше).

  2. Не делайте System.exit() повсюду, так как это сделает код трудным для проверки - может быть интересно посмотреть, что делает делектор, даже если он кажется, что у вас нет оригинальной записи по умолчанию == true.

Смежные вопросы