通信达 交易已锁定_使用交易和锁定

通信达 交易已锁定

在你开始前

本教程适用于需要在单个Derby(或IBM Cloudscape)数据库上处理多个同时用户的开发人员。 想要了解有关构建Java GUI应用程序的更多信息的开发人员也会发现它很有帮助。

关于本教程

在第1部分中,您使用Java数据库连接(JDBC)连接到数据库,从而创建了基本的实体类。 第2部分使用这些类创建应用程序,首先使用GUI,然后使用基于Web的界面,并研究了将Derby嵌入或合并到应用程序中的不同方法。 单用户嵌入式应用程序变成了多用户联网应用程序,其中通过为每个用户提供唯一的架构来解决并发问题。

尽管第2部分中的日历应用程序允许多个用户,但用户无法查看彼此的数据。 在本教程中,您将通过将所有内容移回默认架构并使用Derby中的锁定和事务控制并发问题来创建此公共日历。

在本教程中,您将了解:

  • 如何创建日期选择器。
  • 如何使用Java代码轻松显示信息表。
  • 如何使该表中的信息可编辑。
  • 如何创建和使用数据库事务。
  • 不同类型的选择异常。
  • 数据库隔离级别以及如何控制它们。
  • 不同类型的数据库锁。
  • 作用域对数据库锁的影响。
  • 失控锁定类型。

先决条件

本教程涵盖了Java GUI应用程序的构建和更改,因此您至少应该对Java语言有基本的了解,以随教程的这一部分一起学习。 您还应该至少对Derby的工作原理有基本的了解。 您可以在第1部分“ 了解JDBC ”和第2部分“ 嵌入选项 ”(developerWorks,2005年9月)中获得基础知识。

系统要求

Apache Derby是IBM Cloudscape数据库的开源中心,因此它具有许多功能,例如占用空间小以及能够轻松将其嵌入到应用程序中(请参阅参考资料 ,以获得有关Cloudscape的更多信息的链接) )。 Derby还提供了到其他数据库(例如IBMDB2®)的简便升级途径。

要继续学习本教程,您需要具备以下工具:

  • Java 2 Platform Standard Edition 1.4.2或更高版本 。 请注意,您必须拥有Java Developer Kit,而不仅仅是Java Runtime Environment。 从Sun网站下载Java代码。
  • 现有的应用程序 。 本教程演示了对第2部分中创建的应用程序的更改。下载这些
  • 您可以从Apache Software Foundation下载Apache Derby
  • JavaMail 。 要发送电子邮件提醒,您需要mail.jar,它现在是Java软件包的一部分。 您还需要Activation.jar,可作为JavaBeans激活框架的一部分下载。 确保两个文件都在您的类路径中。
  • Web应用程序服务器 ,例如Apache的Jakarta Tomcat。 您可以使用任何与servlet兼容的应用程序服务器,但是本教程是使用Tomcat 5.5版编写的。
  • JCalendar 。 为了使生活更轻松,您将向应用程序添加日历选择器。 下载所需的
  • 文本编辑器或集成开发环境(IDE) 。 您可以使用IDE(例如Eclipse)轻松创建Derby应用程序,但我将假设您使用的是简单的文本编辑器。

总览

到目前为止,日历应用程序已经经历了多次迭代。 让我们看看最终将在哪里结束。

到目前为止的申请

第2部分的结尾,该应用程序包括三个部分:

  1. 三个基本类: EventClass ,它处理创建和编辑实际事件; Calendar ,仅用于检索特定日期的一系列事件; 和Reminder ,它们为每个事件发送电子邮件提醒。
  2. CalendarFrame类组成的GUI应用程序,它创建事件的输入表单和现有事件的显示表单。
  3. 基于Web的应用程序,由单个servlet组成,该servlet还包含一个输入表单,显示区域以及编辑现有事件的能力。

您可以按照第2部分中的说明下载这些

常见事件日历的潜在问题

在第2部分中,您非常费力地分离输入的数据以防止并发问题。 但是,这是什么意思?

当多个日历用户同时处理同一天时,会出现并发问题。 在某些情况下,这无关紧要。 例如,一个,两个或137个用户同时查看单个日期并不重要,只要他们都没有尝试更改它。

假设有两个用户正在观看定于2005年10月28日举行的同一活动:一位退休程序员的颁奖典礼。 两者都同时在屏幕上显示事件。 鲍勃决定更改描述以指示将位置更改为安静的餐厅,特洛伊决定将其更改为游乐园。 当他们俩都试图保存更改时,哪一个胜出? 如果鲍勃先保存更改,特洛伊的更改将使其全部消失,而且他们都不知道。

为防止这种情况发生,您必须使用事务,锁定和隔离级别。

事务,锁定和隔离级别

事务是一组必须全部成功或失败的数据库操作。 例如,假设您有连续两天发生的事件,例如为期两天的会议。 如果移动一个,则也需要移动另一个。

因此,如果您发出一条SQL update语句来更改第一个而成功,然后发出第二条update语句来更改第二个而失败,则应该撤消或回滚第一个。

事务通过创建必须一起成功的一组操作来解决此问题。

因此,当您创建事务并执行诸如插入数据之类的操作时,该操作直到您完成或提交事务后才生效。 这并不是说您看不到更改。 无论是否已提交更改,您始终可以看到您的更改。

问题是,“还有谁能看到他们?” 这是本教程通过研究锁和隔离级别来探讨的问题。

申请变更

在本教程中,您将通过两种方式调整应用程序。

第一个更改解决了可用性问题。 该应用程序的概念证明仅要求可以由多个用户添加和显示事件。 为了增加用户的便利性,需要添加更容易的日期管理和事件编辑。

第二个变化涉及交易管理。 概念证明可立即执行所有数据库更改; 但是,在这种情况下,应用程序会根据请求将每个更改发送到数据库,但在用户明确要求之前,不会提交这些更改。 在此之前,用户还可以选择回滚事务。

该应用程序还将能够模拟多个用户会话,并专门选择不同的锁定机制和隔离级别。

GUI和Web之间的主要区别

在进行应用程序更改时,重要的是要了解GUI和Web日历应用程序之间的差异。

在GUI应用程序中,用户在启动应用程序时会启动数据库连接,并保持该连接,直到应用程序终止为止,这可能会花费大量时间。 在这段时间内,用户可能发起多个交易,这也可能花费大量时间。

在基于Web的应用程序中情况并非如此。 Web与数据库的交互本质上是无状态的。 用户提交表单以添加或编辑事件,然后Servlet连接,进行更改并断开连接,从而提交事务。

因此,除非您想进行复杂的会话管理,否则事务管理将不再是问题,这不在本教程的讨论范围之内。 取而代之的是,Web应用程序将保持原样,受锁中其他会话的影响,但不受影响。

交易介绍

该过程的核心是应用程序将操作分组为事务的能力,因此它有助于准确了解它们是什么以及它们如何工作。

插入资料

要开始查看事务,请打开三个命令窗口。 首先,以网络模式启动数据库,如清单1所示。

清单1.启动数据库
>set DERBY_INSTALL=c:\derby
>set CLASSPATH=%DERBY_INSTALL%\lib\derby.jar;%DERBY_INSTALL%\lib\derbytools.jar;
>set CLASSPATH=%DERBY_INSTALL%\lib\derbynet.jar;%CLASSPATH%
>java org.apache.derby.drda.NetworkServerControl start -h 0.0.0.0

当然,请确保对DERBY_INSTALL使用您自己的值。 在其他窗口中,打开ij工具,如清单2所示。

清单2.启动客户端
>set DERBY_INSTALL=c:\derby
>set CLASSPATH=%DERBY_INSTALL%\lib\derby.jar;%DERBY_INSTALL%\lib\derbytools.jar;
>set CLASSPATH=%DERBY_INSTALL%\lib\derbyclient.jar;%CLASSPATH%
>set PATH=C:\derby\frameworks\NetworkServer\bin;%PATH%
>ij
>java -Dij.driver=org.apache.derby.jdbc.ClientDriver 
-Dij.protocol=jdbc:derby://localhost:1527/ -Dij.user=APP 
-Dij.password=APP  org.apache.derby.tools.ij
ij version 10.1
ij>

默认情况下,该工具在自动提交模式下运行。 换句话说,它永远不会创建事务,因为它在每次操作之后都会提交。 例如,在一个客户机窗口中,创建一个表并插入一些数据(请参见清单3 )。

清单3.插入数据
ij> connect 'c:\derby\calendar';
ij> create table demo (theText varchar(50), minValue int, maxValue int);
0 rows inserted/updated/deleted
ij> insert into demo (theText, minValue, maxValue) values ('first value', 
10, 20);
1 row inserted/updated/deleted
ij> insert into demo (theText, minValue, maxValue) values ('second value', 
15, 20);
1 row inserted/updated/deleted

现在转到另一个窗口并选择数据,如清单4所示。

清单4.查看数据
ij> connect 'c:\derby\calendar';
ij> select * from demo;
THETEXT                                           |MINVALUE   |MAXVALUE
--------------------------------------------------------------------------
first value                                       |10         |20
second value                                      |15         |20

启用了自动提交功能,因此数据库在每个语句之后执行一次提交。

现在,让我们看看关闭自动提交功能并且在每个语句之后不提交数据库时会发生什么。

创建交易

Derby使您可以关闭自动提交模式,因此您可以查看创建事务时发生的情况。 在第一个窗口中,关闭autocommit,并插入一条新记录,如清单5所示

清单5.关闭自动提交
ij> autocommit off;
ij> insert into demo (theText, minValue, maxValue) values ('third value', 
100, 200);
1 row inserted/updated/deleted

现在,尝试在另一个窗口中再次选择数据,如清单6所示

清单6.选择未提交的数据
ij> select * from demo;
THETEXT                                           |MINVALUE   |MAXVALUE
--------------------------------------------------------------------------
ERROR 40XL1: A lock could not be obtained within the time requested

您大约需要一分钟才能看到响应,因为这是数据库等待的默认时间。 那么,为什么会出现错误? 发生这种情况有两个原因。 首先,默认情况下,Derby不允许您看到尚未提交的行。 其次,尽管Derby通常默认情况下会锁定单个行,但是如果Derby认为这是一个优势,它会自动将锁升级为表锁。 因为在一个小型数据库中有一个小型表,所以这一行代表了数据库的很大一部分,因此它已锁定了整个表。 (本教程稍后将介绍不同类型的锁 。)

进行交易

只要事务处于活动状态,锁就会保留在原处。 您可以通过提交来结束事务。 在原始窗口中, commit事务:

ij> commit;

现在,尝试在第二个窗口中再次选择数据(请参见清单7 )。

清单7.查看提交的数据
ij> select * from demo;
THETEXT                                           |MINVALUE   |MAXVALUE
--------------------------------------------------------------------------
first value                                       |10         |20
second value                                      |15         |20
third value                                       |100        |200

回滚交易

将数据保留在某种状态中的全部目的是要承认它可能永远不会提交的可能性。 它可能会回滚或撤消。 回滚事务时,实际上是在取消自开始事务以来所做的所有更改。 例如,在第一个窗口中,插入两个新记录(请参见清单8 )。

清单8.插入更多记录
ij> insert into demo (theText, minValue, maxValue) values ('fourth  value',\
100, 300);
1 row inserted/updated/deleted
ij> insert into demo (theText, minValue, maxValue) values ('fifth  value', 
200, 300);
1 row inserted/updated/deleted

现在,在同一窗口中,选择数据,如清单9所示。

清单9.查看未决数据
ij> select * from demo;
THETEXT                                           |MINVALUE   |MAXVALUE
--------------------------------------------------------------------------
first value                                       |10         |20
second value                                      |15         |20
third value                                       |100        |200
fourth value                                      |100        |300
fifth value                                       |200        |300

在第二个窗口中,选择数据(请参见清单10 )。

清单10.没有看到待处理的数据
ij> select * from demo;
THETEXT                                           |MINVALUE   |MAXVALUE
--------------------------------------------------------------------------
first value                                       |10         |20
second value                                      |15         |20
third value                                       |100        |200

现在在第一个窗口中,执行回滚:

ij> rollback;

现在在两个窗口中重复执行select语句,如清单11所示。

清单11.查看永久数据
ij> select * from demo;
THETEXT                                           |MINVALUE   |MAXVALUE
--------------------------------------------------------------------------
first value                                       |10         |20
second value                                      |15         |20
third value                                       |100        |200

提交和回滚是事务管理的重要组成部分,并将集成到更新的应用程序中。 它还有助于了解幕后的真实情况。

事务,锁和隔离级别

您刚刚看到的是数据库中的典型隔离级别。 德比还提供其他级别。

异常类型

事务彼此隔离,以防止多个人使用同一数据库时可能发生的问题。 这些问题称为异常 ,它们分为以下三类。

脏读

当一个用户对数据库中已有的数据进行更改时,在提交更改之前,该数据将被视为脏数据。 由于更改可以随时回滚,因此不视为永久更改; 因此,另一个事务查看脏数据被认为是异常。

原因:一个事务对另一个事务已更改但未提交的数据执行选择。

不可重复读

从数据库中选择数据时,您希望取回数据库中的数据。 例如,如果您拉出当天的事件列表,则可以合理地期望您可以根据该数据做出决策。 当一个事务正在查看不再代表数据库内容的数据时,将发生不可重复的读取。

原因:一个事务选择了一组数据,但是当该事务处于活动状态(换句话说,在提交之前)时,另一个事务对该数据进行了更改。

幻影阅读

这种异常类似于不可重复的读取 ,因为它涉及到您期望所查看的数据代表数据库中的内容。 但是,在这种情况下,问题是插入的数据而不是更新的数据。 例如,如果您选择了2005年10月28日的所有事件,并且-当您查看该集合时-我添加了一个应作为该SQL结果一部分的附加事件,该附加记录将在那里存储幻影阅读。

原因:一个事务选择了一组数据。 但是,当该事务处于活动状态时,另一个事务将插入应作为结果一部分的数据。

数据库锁

为了防止某些或所有这些异常,数据库建立了锁定系统。 当事务尝试对数据库执行任何操作(包括选择语句)时,通常必须首先请求适当的锁。 哪种类型的锁适用于何种情况取决于几个因素,包括隔离级别 。 但是,首先了解可用锁的类型很重要。

共享锁

当事务选择数据时,通常会首先请求共享锁。 如果第二个事务想要选择相同的数据,则它也可以获得共享锁。 如果持有共享锁的事务要更改数据,则必须首先将该锁转换为互斥锁。

排他锁

当事务对数据进行更改时,它必须首先请求对该数据进行排他锁。 一旦锁定到位,其他任何事务都无法获得对数据的锁定,因此在锁定到位后,其他任何事务都无法更改该数据。 排他锁放置到位后,甚至无法添加共享锁,因此另一个事务无法再选择该数据。

(第三种类型的锁( 更新锁)是这两种类型的组合。)

提交或回滚事务将释放该事务的所有锁。

锁定范围

范围在锁定中也很重要。 Derby提供了两种不同的主要锁定范围:行锁定和表锁定。

例如,假设您更新Event表中的一行。 如果数据库使用行锁定,则该事务将在该单行上持有排他锁。 另一个事务无法选择该行(因为它无法获得共享锁),但是它可以选择该表中的其他行,或者插入或更新其他数据。

另一方面,如果数据库使用表锁定,则在更新该单个记录后,您将对整个表持有排他锁,因此没有人可以对该表执行select语句,更不用说对数据进行更改了。

显然,如果您的系统同时被多个会话使用,则行锁定将提供更好的性能,并且默认情况下,这就是Derby使用的模式。 但是,如果您开始积累太多的锁,以致使用表锁更有效,则Derby会自动进行更改。 您可以使用derby.storage.rowLocking属性控制此行为的阈值。 (请参阅调整德比的相关信息一节。)例如,在此应用程序的初步发展,正如你前面看到的,几乎空的桌子是如此之小,德比总是选择表锁。 直到添加了大量记录后,才可以观察到行锁定。

隔离度

现在,您将看到可能出现的不同类型的异常,Derby可以使用这些锁定来防止异常。 它们的使用方式决定了事务的隔离级别 。

Derby事务可以存在以下四个隔离级别之一:

TRANSACTION_READ_UNCOMMITTED

使用此隔离级别的事务不请求锁。 换句话说,如果您执行更新,则另一个事务可以看到您未提交的数据。 在这种模式下,脏读,不可重复读和幻像读都是可能的。 无论您使用表锁还是行锁都没有关系,因为事务不请求任何锁。

TRANSACTION_READ_COMMITTED

在这种模式下,数据库在您更新(或删除或插入)数据时请求排他锁,这可以防止脏读,因为一旦建立了锁,另一个事务就无法获得对数据的共享锁。 另一方面,数据库仅请求选择的共享锁。 在查看数据时,其他事务可以更改它,因此不可重复的读取(在另一个事务中更新正在查看的行)和幻像的读取(在另一个事务中添加应在正在查看的集合中的数据)仍然是可能的。

这两个隔离级别是典型应用程序中最常用的级别,而TRANSACTION_READ_COMMITTED是大多数RDBMS系统的默认设置。 但是,还有两个附加级别用于更专门的目的。

TRANSACTION_REPEATABLE_READ

当用户需要确定所查看的数据在完成使用之前不会更改时,可以使用此隔离级别。 例如,假设您执行以下选择:

select * from Event where eventMonth = 12 and 
        eventDay = 28 and eventYear = 2005

在行锁定的情况下,数据库将阻止从另一个事务更新该数据,直到您提交发出选择的事务为止。 因此,在交易过程中,所有读取都是完全可重复的。 (当然,这也意味着您也无法从其他人的事务中读取未提交的数据。)但是,在行锁定模式下,此级别不会阻止对表的插入,因此,当脏读和不可重复读退出时,幻像读取仍然是可能的。

如果使用表锁定,则系统将像在其余模式TRANSACTION_SERIALIZABLE中一样进行操作。

TRANSACTION_SERIALIZABLE

在这种模式下,您可以确定要查看的数据,因为数据库将事务视为好像是一个接一个地而不是并发地运行。 在上一个示例中,select语句以表锁定模式生成对整个表的锁定,从而阻止了更新,删除和插入,直到事务提交为止。

即使在行锁定模式下,数据库也会通过发出范围锁定来防止所有三种类型的异常,包括幻像读取。 因此,在这种情况下,数据库将锁定满足此where子句的所有记录,即使它们尚不存在。 因此,在这种情况下,您将无法在该日期之前添加新事件。

更新应用程序:EventClass

您可以在“ 下载”部分中获得完整的EventClass文件,但让我们看一下该系列文章的第2部分以来所做的一些更改。

改组

第2部分以来的第一个更改是各种各样的真实组织。 以前,如果使用事件数据实例化了EventClass对象,则该对象将继续在数据库中创建记录。 现在,构造函数仅填充类属性,如清单12所示。

清单12.重新组织EventClass
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Date;

import javax.swing.JOptionPane;

public class EventClass {

     public static void add(Connection conn, String newTitle,
          String newDescription, String newRemindersTo, Date date) {
          java.util.Calendar calendar = java.util.Calendar.getInstance();
          calendar.setTime(date);
          int month = calendar.get(java.util.Calendar.DAY_OF_MONTH);
          int day = calendar.get(java.util.Calendar.DAY_OF_MONTH);
          int year = calendar.get(java.util.Calendar.YEAR);
          add(conn, newTitle, newDescription, newRemindersTo, month, day, year);
     }

     public static void add(Connection conn, String newTitle,
          String newDescription, String newRemindersTo, int eventMonth,
          int eventDay, int eventYear) {
          System.out.println("Creating event for " + eventMonth + "/" + eventDay
                    + "/" + eventYear);
          System.out.println(newTitle);
          System.out.println(newDescription);
          System.out.println("Reminders to: " + newRemindersTo);

          try {
               PreparedStatement ps = conn
                         .prepareStatement("insert into Event (title, description,"
                                   + "remindersTo, eventMonth, "
                                   + "eventDay, eventYear)" + "values (?,?,?,?,?,?)");
               ps.setString(1, newTitle);
               ps.setString(2, newDescription);
               ps.setString(3, newRemindersTo);
               ps.setInt(4, eventMonth);
               ps.setInt(5, eventDay);
               ps.setInt(6, eventYear);
               ps.executeUpdate();
               System.out.println("Record updated");
               ps.close();
          } catch (Exception e) {
               ...
               e.printStackTrace();
          }
     }
...
     public EventClass(int eventId) {
          this.id = eventId;
     }

     public EventClass(String newTitle, String newDescription,
          String newRemindersTo, int eventMonth, int eventDay, int eventYear) {
          this.title = newTitle;
          this.description = newDescription;
          this.remindersTo = newRemindersTo;
          this.eventDay = eventDay;
          this.eventMonth = eventMonth;
          this.eventYear = eventYear;
     }
...
}

将对象保存到数据库的功能现在已移至add()方法。

发现问题

每种对数据库进行更改的方法都有可能遇到其要更改的表被锁定的情况。 该类现在可以让用户知道发生了什么(请参见清单13 )。

清单13.检测锁
...
     private boolean update() {

          try {
               Statement s = this.conn.createStatement();
               PreparedStatement ps = conn
                         .prepareStatement("update event set title=?, "
                                   + "description=?, " + "remindersTo=?,"
                                   + "eventMonth=?, eventDay=?, "
                                   + "eventYear=? where id = ?");
               ps.setString(1, this.title);
               ps.setString(2, this.description);
               ps.setString(3, this.remindersTo);
               ps.setInt(4, this.eventMonth);
               ps.setInt(5, this.eventDay);
               ps.setInt(6, this.eventYear);
               ps.setInt(7, this.id);
               ps.executeUpdate();
               System.out.println("Record updated");
               ps.close();
               s.close();
          } catch (Exception e) {
               if (e.getMessage().indexOf("lock") >= 0) {
                   JOptionPane.showMessageDialog(null,
                        "Unable to update this event as the row "
                                + "and/or table is locked.", "Lock",
                        JOptionPane.WARNING_MESSAGE);
               }
               e.printStackTrace();
          }
          return true;
     }
}

最后的更改涉及此更改的发生频率。

对数据库的增量更新

最后, EventClass也进行了更改,以便每次进行任何更改时都调用update()方法,如清单14所示。

清单14.增量更改
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Date;

import javax.swing.JOptionPane;

public class EventClass {

     public static void add(Connection conn, String newTitle,
          String newDescription, String newRemindersTo, Date date) {
          java.util.Calendar calendar = java.util.Calendar.getInstance();
          calendar.setTime(date);
          int month = calendar.get(java.util.Calendar.DAY_OF_MONTH);
          int day = calendar.get(java.util.Calendar.DAY_OF_MONTH);
          int year = calendar.get(java.util.Calendar.YEAR);
          add(conn, newTitle, newDescription, newRemindersTo, month, day, year);
     }

     public static void add(Connection conn, String newTitle,
          String newDescription, String newRemindersTo, int eventMonth,
          int eventDay, int eventYear) {
          System.out.println("Creating event for " + eventMonth + "/" + eventDay
                    + "/" + eventYear);
          System.out.println(newTitle);
          System.out.println(newDescription);
          System.out.println("Reminders to: " + newRemindersTo);

          try {
               PreparedStatement ps = conn
                         .prepareStatement("insert into Event (title, description,"
                                   + "remindersTo, eventMonth, "
                                   + "eventDay, eventYear)" + "values (?,?,?,?,?,?)");
               ps.setString(1, newTitle);
               ps.setString(2, newDescription);
               ps.setString(3, newRemindersTo);
               ps.setInt(4, eventMonth);
               ps.setInt(5, eventDay);
               ps.setInt(6, eventYear);
               ps.executeUpdate();
               System.out.println("Record updated");
               ps.close();
          } catch (Exception e) {
            if (e.getMessage().indexOf("lock") >= 0) {
                JOptionPane.showMessageDialog(null,
                        "Unable to add this event as the"
                                + "table is locked.", "Lock",
                        JOptionPane.WARNING_MESSAGE);
            }
               e.printStackTrace();
          }
     }

     private Connection conn;

     private String description;

     private int eventDay;

     private int eventMonth;

     private int eventYear;

     private int id;

     private String remindersTo;

     private String title;

     public EventClass(int eventId) {
          this.id = eventId;
     }

     public EventClass(String newTitle, String newDescription,
          String newRemindersTo, int eventMonth, int eventDay, int eventYear) {
          this.title = newTitle;
          this.description = newDescription;
          this.remindersTo = newRemindersTo;
          this.eventDay = eventDay;
          this.eventMonth = eventMonth;
          this.eventYear = eventYear;
     }

     public boolean delete(Connection conn) {
          try {
               Statement s = conn.createStatement();
               String sql = "delete from Event where id= " + this.getId();
               System.out.println(sql);

               int rowsDeleted = s.executeUpdate(sql);

               System.out.println(rowsDeleted + " record(s) deleted");
               s.close();
          } catch (Exception e) {
            if (e.getMessage().indexOf("lock") >= 0) {
                JOptionPane.showMessageDialog(null,
                        "Unable to delete this event as the row"
                                + "and/or table is locked.", "Lock",
                        JOptionPane.WARNING_MESSAGE);
            }
               e.printStackTrace();
          }
          return true;
     }

     public String getDescription() {
          return this.description;
     }

     public int getEventDay() {
          return this.eventDay;
     }

     public int getEventMonth() {
          return this.eventMonth;
     }

     public int getEventYear() {
          return this.eventYear;
     }

     public int getId() {
          return this.id;
     }

     public String getRemindersTo() {
          return this.remindersTo;
     }

     public String getTitle() {
          return this.title;
     }

     public void load() {
          try {
               Statement s = conn.createStatement(
                         ResultSet.TYPE_SCROLL_INSENSITIVE,
                         ResultSet.CONCUR_READ_ONLY);

               ResultSet rs = s.executeQuery("SELECT * FROM Event "
                         + "where id = " + this.getId());
               if (rs.next()) {
                    this.title = rs.getString(2);
                    this.description = rs.getString(3);
                    this.remindersTo = rs.getString(4);
                    this.eventMonth = rs.getInt(5);
                    this.eventDay = rs.getInt(6);
                    this.eventYear = rs.getInt(7);
               }
          } catch (Exception e) {
               e.printStackTrace();
          }
     }

     public void setConnection(Connection conn) {
          this.conn = conn;
     }

     public void setDescription(String value) {
          this.description = value;
          update();
     }

     public void setEventDay(int value) {
          this.eventDay = value;
          update();
     }

     public void setEventMonth(int value) {
          this.eventMonth = value;
          update();
     }

     public void setEventYear(int value) {
          this.eventYear = value;
          update();
     }

     public void setId(int value) {
          this.id = value;
          update();
     }

     public void setRemindersTo(String value) {
          this.remindersTo = value;
          update();
     }

     public void setTitle(String value) {
          this.title = value;
          update();
     }

     private boolean update() {
...

显然,在您自己的应用程序中,您应该确定这是否可行,将进入数据库的数据的即时性与大量更新所涉及的性能问题进行交换。

使用表格模型

这样就可以处理后端的更改。 现在让我们看一下前端。 GUI应用程序在表中显示事件,如果不是模型,则该任务可能很繁琐。 为了避免在讨论实际界面时被误导,让我们看一下这些模型的工作方式,以便在出现这些模型时就知道它在做什么。

什么是表格模型?

如果您是一名程序员,并且我告诉过您我希望您在表格中显示信息,那么您将知道需要按照一定的顺序进行操作。 您需要布置各种行和列,跟踪哪一行代表哪个对象,依此类推。 而且,如果我希望该表是可编辑的,则将需要更长的任务列表。

事实是,这是一个定义明确的事件序列。 如果您知道必须在应用程序中的多个位置执行此操作,则可能决定创建某种自动化流程来简化此过程。 好吧,有人已经这样做就不足为奇了。 实际上,模型的概念是Swing的一部分,Swing是用于组合GUI的Java API。

该应用程序使用AbstractTableModel对象来指示GUI如何执行操作,例如填充表和对值的变化做出React。 在创建表时设置模型(请参见清单15 )。

清单15.使用表模型
...
   this.eventModel = new EventTableModel();
   reloadTableModel();
   final JTable eventTable = new JTable(this.eventModel);
   tablePanel.add(new JScrollPane(eventTable), BorderLayout.CENTER);
...

EventTableModel是定义这些行为的类,因此eventTable对象立即知道该怎么做。 让我们看一下该类。

定义表格

第一步是将类定义为AbstractTableModel的子级,并定义表本身的结构,如清单16所示。

清单16.定义表本身
import java.sql.Connection;
import java.util.Date;

import javax.swing.table.AbstractTableModel;

public class EventTableModel extends AbstractTableModel {

	String[] columns = { "Title", "Description", "Reminders To" };

	EventClass[] events;

	public int getRowCount() {
		if (events != null) {
			return events.length;
		}
		return 0;
	}

	public int getColumnCount() {
		return columns.length;
	}

	public String getColumnName(int col) {
		return columns[col];
	}

}

EventClass[]数组表示表将显示的数据。 行数基于此数组的大小。 列由columns数组定义。

获取和设置值

Java代码开始显示表之后,它需要知道什么数据进入每个表单元格,如清单17所示。

清单17.提供单个单元
...
	public String getColumnName(int col) {
		return columns[col];
	}

	public Object getValueAt(int rowIndex, int columnIndex) {
		if ((events != null) && (rowIndex < events.length)
				&& (columnIndex < columns.length)) {
			switch (columnIndex) {
			case 0:
				return events[rowIndex].getTitle();
			case 1:
				return events[rowIndex].getDescription();
			case 2:
				return events[rowIndex].getRemindersTo();
			default:
				return null;
			}
		} else {
			return null;
		}
	}

	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return true;
	}

	public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
		if ((events != null) && (rowIndex < events.length)
				&& (columnIndex < columns.length)) {
			switch (columnIndex) {
			case 0:
				events[rowIndex].setTitle(aValue.toString());
				break;
			case 1:
				events[rowIndex].setDescription(aValue.toString());
				break;
			case 2:
				events[rowIndex].setRemindersTo(aValue.toString());
				break;
			}
		}
	}
}

如果该单元格是可编辑的-使用行和列信息,您可以分别为每个单元格设置该属性-GUI需要知道如果用户实际对其进行编辑该怎么做。 在这种情况下,GUI在适当的EventClass对象上设置适当的值。

将行映射到对象

当然,GUI需要知道如何将表中的行与特定对象相关联(请参见清单18 )。

清单18.将行映射到对象
...
				events[rowIndex].setRemindersTo(aValue.toString());
				break;
			}
		}
	}
	
	public EventClass getEventAtRow(int rowIndex) {
		if ((this.events != null) && (rowIndex < this.events.length)) {
			return this.events[rowIndex];
		}
		return null;
	}
}

在这种情况下,因为使用了数组,所以非常简单。

刷新桌子

最后,GUI需要知道如何刷新自身,如清单19所示。

清单19.刷新模型
...
public class EventTableModel extends AbstractTableModel {

	String[] columns = { "Title", "Description", "Reminders To" };

	EventClass[] events;

	public void reloadEventsForDate(Connection conn, Date date) {
		events = Calendar.getEvents(conn, date);
		fireTableDataChanged();
	}

	public int getRowCount() {
		if (events != null) {
...

reloadEventsForDate()方法获取一个连接和一个日期,并使用Calendar类获取适当事件的数组。 拥有后,它将调用fireTableDataChanged()方法,该方法是从AbstractTableModel类继承的。 该方法知道如何使用您已经定义的参数(例如getColumnCount()getValueAt()来显示表。

更新应用程序:CalendarFrame

现在是时候将所有内容放在一起。 该应用程序的主体(包括每个GUI元素)在CalendarFrame类中。

整体应用

第2部分开始 ,日历应用程序进行了许多更改,以使人们更易于使用和维护。 这些更改中有些对最终用户可见,而有些则不可见。 主要更改包括新的外观和风格,其中包括使选择日期更加容易的日历选择器。 而且,如您所见,该应用程序现在具有一个可编辑的表格,用户可以通过该表格更改现有事件的信息。

其他变化对最终用户而言并不那么明显。 例如,每次将更改(例如附加记录)添加到数据库时,应用程序的早期版本就会连接到数据库。 现在,当用户打开应用程序时它将连接,并在整个用户会话期间保持该连接。

为了便于维护,现在将连接由单独的类处理。

数据库类

Database类严格来说是一个实用程序类,提供常量和必要的功能,例如返回要由应用程序的其余部分使用的Connection (请参见清单20 )。

清单20.数据库类
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

class Database {

   private static final String EMBEDDED_DRIVER = 
                      "org.apache.derby.jdbc.EmbeddedDriver";
   private static final String NETWORKED_DRIVER = 
                        "org.apache.derby.jdbc.ClientDriver";
   private static final String PROTOCOL = "jdbc:derby";
   private static final String EMBEDDED_DB = "c:/derby/calendar";
   private static final String NETWORK_DB = 
                            "//localhost:1527/c:/derby/calendar";
   public static final int EMBEDDED = 1;
   public static final int NETWORKED = 2;

   static Connection getConnection(int DB_TYPE) throws Exception {
      return getConnection(null, DB_TYPE);
   }

   static Connection getConnection(String options, int DB_TYPE)
         throws Exception {
      
      String url = PROTOCOL + ':';

      if (DB_TYPE == EMBEDDED) {
         url += EMBEDDED_DB;
         Class.forName(EMBEDDED_DRIVER).newInstance();
      } else if (DB_TYPE == NETWORKED) {
         url += NETWORK_DB;
         Class.forName(NETWORKED_DRIVER).newInstance();
      }

      if (options != null) {
         url += ';' + options;
      }
      return DriverManager.getConnection(url);
   }

   public static void closeConnection(Connection conn) {
      if (conn == null) {
         return;
      }
      
      try {
         conn.close();
         DriverManager.getConnection("jdbc:derby:;shutdown=true");
      } catch (SQLException se) {
         //se.printStackTrace();
      }
   }

   public static void createTableIfNotExists() {
      Connection conn = null;
      try {
         conn = Database.getConnection("create=true", EMBEDDED);
         Statement st = conn.createStatement();
         st.executeUpdate("create table Event (id INT GENERATED "
                     + "ALWAYS AS IDENTITY, title VARCHAR(50), "
                     + "description VARCHAR(255), remindersTo "
                     + "VARCHAR(255), eventMonth INT, eventDay INT, "
                     + "eventYear INT)");
      } catch (Exception e) {
         // e.printStackTrace();
      } finally {
         Database.closeConnection(conn);
      }
   }
}

常量使您可以轻松地在Derby数据库的嵌入式和网络形式之间进行选择,而getConnection()closeConnection()方法则可以实现您想像的功能。 createTableIfNotExists()方法尝试创建表,如果该表已经存在,则会以静默方式失败。

创建框架和连接

让我们创建GUI的各个区域。 总体目标是一个框架(请参见清单21 ),其中包括三个部分:左边两个,右边一个。

清单21.创建框架
...
public class CalendarFrame extends JFrame implements PropertyChangeListener {

    private Connection conn;

    private JCalendar calendarPicker;

    private EventTableModel eventModel;

    public CalendarFrame() {
        super();
        this.setTitle("Derby Calendar");
        this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        try {
            this.conn = Database.getConnection(Database.NETWORKED);
            this.conn.setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }

        this.getContentPane().setLayout(new BorderLayout());
        this.getContentPane().add(layoutMenuBar(), BorderLayout.NORTH);

        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        splitPane.setDividerLocation(0.6);
        splitPane.setLeftComponent(layoutLeftPanel());
        splitPane.setRightComponent(layoutRightPanel());
        this.getContentPane().add(splitPane, BorderLayout.CENTER);
    }

    private JComponent layoutLeftPanel() {
        JSplitPane viewSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        return viewSplitPane;
    }

    private JPanel layoutRightPanel() {
        JPanel tablePanel = new JPanel(new BorderLayout());
        return tablePanel;
    }

    private JComponent layoutMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        return menuBar;
    }

    public static void main(String args[]) {
        Database.createTableIfNotExists();
        // Set the SQL lock time out to just 1 second (default is 60)
        System.setProperty("derby.locks.waitTimeout", "1");
        CalendarFrame w = new CalendarFrame();
        w.setSize(750, 550);
        w.setVisible(true);
    }

    public void propertyChange(PropertyChangeEvent evt) {
    }

}

main方法开始。 首先,请确保通过调用createTableIfNotExists建立了数据库。 如果一切都已经设置好,它只会静默失败。 接下来,将锁定超时设置为一秒,以查看会话冲突时发生的情况。 对于生产应用程序,您可能会将其设置得更高。

接下来,创建实际的框架并显示它。

创建类涉及首先获得与数据库的连接。 如果要回滚更改,则必须关闭autocommit属性。

最后,布置GUI拼图的各个部分(请参见图1 )。 该应用程序现在使用JSplitPane对象,使用户可以轻松地确定接口的哪些区域需要更多空间。

图1.基本框架
基本框架

添加基本​​输入表单

接下来,创建基本的界面元素,如清单22所示。

清单22.基本输入表单
...
    private JComponent layoutLeftPanel() {

        JSplitPane viewSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);

        // Edit Panel
        JPanel editPanel = new JPanel(new GridBagLayout());
        GridBagConstraints cons = new GridBagConstraints();
        cons.fill = GridBagConstraints.HORIZONTAL;
        cons.anchor = GridBagConstraints.PAGE_START;
        cons.insets = new Insets(5, 2, 5, 2);
        cons.ipadx = 2;
        cons.ipady = 2;

        int rows = 1;
        cons.gridy = rows++;
        cons.gridx = 0;
        editPanel.add(new JLabel("Title:"), cons);
        final JTextField titleBox = new JTextField(10);
        cons.gridx = 1;
        editPanel.add(titleBox, cons);

        cons.gridy = rows++;
        cons.gridx = 0;
        cons.gridheight = 1;
        editPanel.add(new JLabel("Reminders:"), cons);
        final JTextField reminderBox = new JTextField(10);
        cons.gridx = 1;
        editPanel.add(reminderBox, cons);

        cons.gridy = rows;
        cons.gridx = 0;
        rows += 7;
        cons.gridheight = 7;
        cons.weighty = 1;
        cons.fill = GridBagConstraints.BOTH;
        editPanel.add(new JLabel("Description:"), cons);
        final JTextArea descriptionBox = new JTextArea(8, 10);
        cons.gridx = 1;
        editPanel.add(new JScrollPane(descriptionBox), cons);

        viewSplitPane.setBottomComponent(editPanel);
        return viewSplitPane;
    }

...

这些基本上与您在第2部分中添加的元素相同。 但是这些布局更为具体(请参见图2 )。

图2.基本输入表单
基本报名表

日历选择器

至少从GUI的角度来看,这才是有趣的地方。 您可以让日历选择器添加到GUI中,而不是让用户操纵多个下拉菜单并希望提供一个有效的日期,如清单23所示。

清单23.添加日历选择器
...
    private JComponent layoutLeftPanel() {

        // Calendar picker
        JPanel pickerPanel = new JPanel(new BorderLayout());
        JPanel titlePanel = new JPanel();
        titlePanel.add(new JLabel("<html><h3>Select a Date</h3></html>"));
        pickerPanel.add(titlePanel, BorderLayout.NORTH);
        JSplitPane viewSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        this.calendarPicker = new JCalendar();
        pickerPanel.add(this.calendarPicker, BorderLayout.CENTER);
        viewSplitPane.setTopComponent(pickerPanel);
        this.calendarPicker.addPropertyChangeListener(this);

        // Edit Panel
        JPanel editPanel = new JPanel(new GridBagLayout());
        GridBagConstraints cons = new GridBagConstraints();
        cons.fill = GridBagConstraints.HORIZONTAL;
        cons.anchor = GridBagConstraints.PAGE_START;
        cons.insets = new Insets(5, 2, 5, 2);
        cons.ipadx = 2;
        cons.ipady = 2;

        int rows = 1;
        cons.weightx = 0.5;
        cons.gridwidth = 1;
        cons.gridy = rows++;
        editPanel.add(new JLabel("Date: "), cons);
        cons.gridx = 1;
        final JDateChooser dateChooser = new JDateChooser();
        editPanel.add(dateChooser, cons);

        cons.gridy = rows++;
        cons.gridx = 0;
        editPanel.add(new JLabel("Title:"), cons);
...

在这里,您将日历添加到两个位置(请参见图3 )。 上面板为用户提供了一种选择显示日期的方式。 下部面板使用户可以激活选择器以选择新事件的日期。

图3.添加日历
添加日历

添加活动

现在已经有了表单,如何将事件添加到数据库呢? 数据库操作发生在EventClass ,但是您仍然需要从GUI调用该对象(参见清单24 )。

清单24.添加一个事件
...
    private JComponent layoutLeftPanel() {
...
        final JTextArea descriptionBox = new JTextArea(8, 10);
        cons.gridx = 1;
        editPanel.add(new JScrollPane(descriptionBox), cons);

        cons.gridy = rows++;
        cons.gridx = 0;
        cons.gridwidth = 2;
        cons.weightx = 1;
        cons.weightx = 0;
        cons.fill = GridBagConstraints.NONE;
        JButton addButton = new JButton("Add Event");
        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                EventClass.add(CalendarFrame.this.conn, titleBox.getText(),
                        descriptionBox.getText(), reminderBox.getText(),
                        dateChooser.getDate());
                CalendarFrame.this.calendarPicker
                        .setDate(dateChooser.getDate());
            }
        });
        editPanel.add(addButton, cons);
        viewSplitPane.setBottomComponent(editPanel);
        return viewSplitPane;
    }
...

在这里,您将添加如图4所示的Add Event按钮及其侦听器。 侦听器调用EventClass ,通过将用户启动时创建的应用程序传递给Connection添加事件,用户输入的信息以及当前显示在编辑日历选择器中的日期。 然后,它为上方的日历选择器设置日期,稍后在将数据表添加到框架右侧时会显示该日期。

图4. Add Event按钮
“添加事件”按钮

摆桌子

您可以使用界面添加喜欢的所有事件,但是如果用户无法显示它们,那么指向它的意义就不多了。 如前所述,该应用程序使用一个与表模型关联的表来显示特定日期的数据(请参见清单25 )。

清单25.添加显示表
...
    private JPanel layoutRightPanel() {
        JPanel tablePanel = new JPanel(new BorderLayout());
        JPanel topPanel = new JPanel();
        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.PAGE_AXIS));
        topPanel.add(new JLabel("<html><h3>Daily Event View</h3>"));
        tablePanel.add(topPanel, BorderLayout.NORTH);

        this.eventModel = new EventTableModel();
        reloadTableModel();
        final JTable eventTable = new JTable(this.eventModel);
        tablePanel.add(new JScrollPane(eventTable), BorderLayout.CENTER);

        return tablePanel;
    }

    private JComponent layoutMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        return menuBar;
    }
...
    public void propertyChange(PropertyChangeEvent evt) {
    }

    private void reloadTableModel() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    CalendarFrame.this.getContentPane().setCursor(
                         Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                    CalendarFrame.this.eventModel.reloadEventsForDate(
                         CalendarFrame.this.conn,
                         CalendarFrame.this.calendarPicker.getDate());
                } finally {
                    CalendarFrame.this.getContentPane().setCursor(
                         Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                }
            }
        });
    }
}

请注意,首先创建EventTableModel ,而不是表本身,然后通过调用reloadTableMode()方法来填充它。 该方法设置光标,以便用户知道正在发生的事情,但更重要的是,它调用模型的reloadEventsForDate()方法,该方法为特定单元格设置数据。

处理完eventModel之后,将其与JTable关联并显示该表(请参见图5 )。

图5.添加表
添加表格

但是,当用户单击日期时,您仍然必须处理更新表的问题。

刷新事件列表

放置完表格后,需要确保该表格始终显示适当的数据。 换句话说,如果用户在日历选择器中选择新的一天,则需要从数据库中请求适当的信息并将其显示在表中(请参见清单26 )。 (看看EventTableMode有多方便?)

清单26.刷新表
...
    private JComponent layoutLeftPanel() {
...
        JButton addButton = new JButton("Add Event");
        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                EventClass.add(CalendarFrame.this.conn, titleBox.getText(),
                        descriptionBox.getText(), reminderBox.getText(),
                        dateChooser.getDate());
                CalendarFrame.this.calendarPicker
                        .setDate(dateChooser.getDate());
                reloadTableModel();
            }
        });
        editPanel.add(addButton, cons);
        viewSplitPane.setBottomComponent(editPanel);
        return viewSplitPane;
    }

    private JPanel layoutRightPanel() {
...
        this.eventModel = new EventTableModel();
        reloadTableModel();
        final JTable eventTable = new JTable(this.eventModel);
        tablePanel.add(new JScrollPane(eventTable), BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();

        JButton refreshButton = new JButton("Refresh");
        refreshButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                reloadTableModel();
            }
        });
        buttonPanel.add(refreshButton);
        tablePanel.add(buttonPanel, BorderLayout.SOUTH);
        return tablePanel;
    }

    private JComponent layoutMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        return menuBar;
    }

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

    public void propertyChange(PropertyChangeEvent evt) {
        reloadTableModel();
    }

    private void reloadTableModel() {
        SwingUtilities.invokeLater(new Runnable() {
...

通过将reloadTableModel()方法添加到propertyChange ,您可以告诉应用程序每次用户选择新日期时都请求新数据。 此外,用户现在只需单击刷新按钮即可请求最新数据。

当用户添加新事件时,该应用程序还会更改为适当的日期并显示新数据。 这样,用户就可以在适当位置看到新事件(请参见图6 )。

图6.刷新表
刷新桌子

发送提醒

现在是时候开始添加其他各种按钮和功能了。 从提醒开始(请参见清单27 )。

清单27.发送提醒
...
    private JPanel layoutRightPanel() {
...
        JPanel buttonPanel = new JPanel();

        JButton remindButton = new JButton("Remind");
        remindButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    int row = eventTable.getSelectedRow();
                    if (row >= 0) {
                        EventClass event = CalendarFrame.this.eventModel
                                .getEventAtRow(row);
                        Reminder rem = new Reminder();
                        rem.sendReminder(CalendarFrame.this.conn, 
                                             event.getId());
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        buttonPanel.add(remindButton);
...
        tablePanel.add(buttonPanel, BorderLayout.SOUTH);
        return tablePanel;
    }
...

在这种情况下,“提醒”按钮不会发送所有提醒,而只会发送用户选择的事件的特定提醒。 (在这里可以将行映射到事件很方便。)

删除按钮的工作方式相同。

删除记录

Delete按钮还取决于能够将一行数据映射到特定的EventClass对象(请参见清单28 )。

清单28.删除事件
...
    private JPanel layoutRightPanel() {
...
        buttonPanel.add(remindButton);

        JButton deleteButton = new JButton("Delete");
        deleteButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    int row = eventTable.getSelectedRow();
                    if (row >= 0) {
                        EventClass event = CalendarFrame.this.eventModel
                                .getEventAtRow(row);
                        event.delete(CalendarFrame.this.conn);
                        reloadTableModel();
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
        buttonPanel.add(deleteButton);

        JButton refreshButton = new JButton("Refresh");
        refreshButton.addActionListener(new ActionListener() {
...
        tablePanel.add(buttonPanel, BorderLayout.SOUTH);
        return tablePanel;
    }

...

在这种情况下,应用程序获取对表中该行所代表的特定EventClass对象的引用,并在重新加载表之前使用该对象自己的delete()方法将其删除(参见图7 )。

图7.删除记录
删除记录

管理交易

如果将应用程序设置为使用自动提交,则无需添加任何代码。 当然,您也将无法撤消更改。 在本节中,您将添加事务管理功能。

提交变更

用户对数据库进行更改后,他或她需要做出一个决定:保存更改,还是还原到更改之前的数据库? 首先为用户提供保存更改的机会(请参见清单29 )。

清单29.提交更改
...
    private JPanel layoutRightPanel() {
...
        buttonPanel.add(refreshButton);

        JButton saveButton = new JButton("Save");
        saveButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    CalendarFrame.this.conn.commit();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        });
        buttonPanel.add(saveButton);
        tablePanel.add(buttonPanel, BorderLayout.SOUTH);
        return tablePanel;
    }
...

该按钮的操作非常简单:获取该应用程序实例正在使用的连接,并使用它来提交当前事务。

还原按钮的工作方式几乎相同。

回滚更改

Revert按钮还使用到数据库的连接(请参见清单30 )。

清单30.回滚更改
...
    private JPanel layoutRightPanel() {
...
        buttonPanel.add(refreshButton);

        JButton revertButton = new JButton("Revert");
        revertButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                try {
                    CalendarFrame.this.conn.rollback();
                    reloadTableModel();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        });
        buttonPanel.add(revertButton);

        JButton saveButton = new JButton("Save");
...
        tablePanel.add(buttonPanel, BorderLayout.SOUTH);
        return tablePanel;
    }
...

After rolling back the current transaction (see Figure 8 ), you should refresh the table to show the old data.

Figure 8. Rolling back changes
Rolling back changes

Setting the isolation level

Part of the project includes enabling users to decide whether they want to see data that hasn't been saved (committed) yet, so you need to give them that choice (see Listing 31 ).

Listing 31. Choosing an isolation level
...
    private JComponent layoutMenuBar() {
        JMenuBar menuBar = new JMenuBar();

        ActionListener transListener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JCheckBoxMenuItem item = (JCheckBoxMenuItem) e.getSource();
                String text = item.getText();
                try {
                    if (text.equals("See all data")) {
                        CalendarFrame.this.conn
                             .setTransactionIsolation(
                                   Connection.TRANSACTION_READ_UNCOMMITTED);
                    } else if (text.equals("See only saved data")) {
                        CalendarFrame.this.conn
                             .setTransactionIsolation(
                                     Connection.TRANSACTION_READ_COMMITTED);
                    } 
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        };

        JMenu menu = new JMenu("Isolation Level");
        ButtonGroup group = new ButtonGroup();

        JMenuItem item = new JCheckBoxMenuItem("See all data");
        group.add(item);
        menu.add(item);
        item.addActionListener(transListener);

        item = new JCheckBoxMenuItem("See only saved data");
        group.add(item);
        menu.add(item);
        group.setSelected(item.getModel(), true);
        item.addActionListener(transListener);

        menuBar.add(menu);
        return menuBar;
    }
...

The phrase isolation level means nothing to most users, and the term TRANSACTION_READ_COMMITTED means even less, so you'll have to come up with names that represent what you're really trying to do. In this case, you're creating a new menu with two items added to it, their names explaining in more friendly terms what it is they do.

When the user chooses one of these items, the transListener analyzes the label on the menu item and uses it to determine which isolation level to use for the current connection (see Figure 9 ).

Figure 9. Choosing the isolation level
Choosing the isolation level

Transactions in action

It's time to stop talking about transactions and isolation levels and finally see them in action. Open two different instances of the CalendarFrame application.

TRANSACTION_READ_COMMITTED

Make sure both application instances are set to See only saved data , which corresponds to the TRANSACTION_READ_COMMITTED isolation level.

In the first window, pick a day and add an event. Then go to the second window and display the events for that day. After a pause, you should get a dialog box (see Figure 10 ) telling you that the application can't retrieve the data because it's locked.

Figure 10. When you can't get a lock on the data
When you can't get a lock on the data

Now, in the first window, click the Save button to commit the transaction. Go back to the second window and try to refresh the page, but only once (see Figure 11 ).

Figure 11. Committing the transaction
Committing the transaction

Notice that you can now request the same data that was previously locked.

TRANSACTION_READ_UNCOMMITTED

In the previous example, you saw how the database kept one user from seeing uncommitted data for another user. But what if you don't want that to happen? In both windows, set the isolation level to See all data .

Now add a new record in the first window, but don't save your changes. Go to the second window and display the events for that date. You should see all the events for that date, including the one you just entered a moment ago (see Figure 12 ).

Figure 12. Seeing dirty data
Seeing 'dirty' data

Now users can complete all their event-management tasks, including seeing what other users are up to.

摘要

This tutorial is the final part in a three-part series chronicling the building of a Java-based calendar and reminder system that uses the Derby database as its data storage system. Part 1 covered the basic database and e-mail entities. Part 2 built those entities into a simple application that explored Derby's embedded, network, and Web-based modes. It also introduced the concept of multiple users.

This tutorial took the overall system and tackled concurrency, discussing transactions, locking, and isolation levels. It also described significant improvements to the overall look and feel and usability of the application.

You now have a system that can be used by multiple individuals at the same time, all editing a common event calendar. You can also choose whether or not to see data that has not yet been committed to the database.


翻译自: https://www.ibm.com/developerworks/opensource/tutorials/os-ad-derbycal3/os-ad-derbycal3.html

通信达 交易已锁定

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值