NetBeans 平台 CRUD 应用程序教程
本教程介绍了如何将 Java DB 数据库集成到 NetBeans 平台应用程序中。我们先从分析 Java DB 数据库入手,将通过这些数据库创建实体类。但请注意,这些说明并非仅适用于 Java DB。对于 NetBeans IDE 所支持的任何关系数据库,这些说明均适用。接下来,我们将把这些实体类和相关 JPA JAR 的模块一起包装到一个模块中。
在以上模块成为应用程序的一部分之后,我们将创建一个为应用程序提供用户界面的新模块。该新模块将为用户提供一个显示数据库中数据的树状分层结构。然后,我们将创建另一个模块,以使用户能够编辑第一个模块显示的数据。我们将使用不同模块将查看器和编辑器分开,以使用户能够为同一查看器安装不同的编辑器,因为外部供应商会创建各种不同的编辑器,有些用于商业用途,有些则是免费提供的。正是 NetBeans 平台的模块化体系结构促成了这种灵活性。
有了编辑器后,我们将开始添加 CRUD 功能。首先,上面所述的查看器将处理代表“读取”的 "R"。接下来,将处理代表“更新”的 "U",然后依次是代表“创建”的 "C" 和代表“删除”的 "D"。
在本教程结束时,您将了解到多种帮助您创建此类应用程序的 NetBeans 平台功能。例如,您将会了解到 UndoRedo.Manager 和 ExplorerManager,以及一些 NetBeans 平台 Swing 组件,如 TopComponent 和 BeanTreeView。
注意:本文档使用的是 NetBeans IDE 6.8 发行版。如果使用的是早期版本,请参见本文档的 6.7 版。
目录

要学习本教程,您需要具备下表中列出的软件和资源。
| 软件或资源 | 要求的版本 |
|---|---|
| NetBeans IDE | 版本 6.8 |
| Java Developer Kit (JDK) | 版本 6 或 版本 5 |
在本教程中创建的应用程序将如下所示:
建议您在开始学习本教程之前,先观看截屏视频系列 Top 10 NetBeans APIs(最主要的 10 个 NetBeans API)。本教程中提到的许多概念都在该截屏视频系列中进行了详细讨论。
设置应用程序
首先让我们来创建一个新的 NetBeans 平台应用程序。
- 选择“文件”>“新建项目”(Ctrl+Shift+N)。在“类别”下选择“NetBeans 模块”。在“项目”下选择“NetBeans 平台应用程序”。单击“下一步”。
- 在“名称和位置”面板的“项目名称”字段中键入 DBManager。单击“完成”。
IDE 将创建 DBManager 项目。此项目是一个容器,可包含您将创建的所有其他模块。
集成数据库
为了集成数据库,您需要从数据库创建实体类,并将这些实体类及其相关的 JAR 一起集成到 NetBeans 平台应用程序所包含的模块中。
创建实体类
在此部分,您将从一个选定的数据库生成实体类。
- 就本示例而言,将使用“服务”窗口连接到 NetBeans IDE 附带的样例数据库:
或者,您可以随意使用任何数据库,并根据特定的用例调整操作步骤。对于 MySQL,请参见连接 MySQL 数据库。
- 在 IDE 中,选择“文件”|“新建项目”,然后选择 "Java" |“Java 类库”以创建一个名为 CustomerLibrary 的新库项目。
- 在“项目”窗口中,右键单击该库项目,选择“文件”|“新建文件”,然后从“数据库”中选择“持久性”|“实体类”。在向导中,选择数据库和所需的表。此处,我们选择 "Customer",将会自动添加 "Discount Code",因为这两个表是关联的。
- 指定持久性策略,它可以是任何可用的选项。此处,由于我们需要选择一些内容,因此,我们选择了 EclipseLink:
- 将 "demo" 指定为将在其中生成实体类的包的名称。
- 单击“完成”。完成此步骤后,查看生成的代码,注意,在一个名为 META-INF 的文件夹中出现了一个 persistence.xml 文件,并且每个表都有了实体类,等等:
- 生成 Java 库,在库项目的 "dist" 文件夹中将出现一个 JAR 文件,可在“文件”窗口中查看此文件。
将实体类 JAR 包装到模块中
在此部分,将在应用程序中添加第一个模块!新 NetBeans 模块将包装在上一节中创建的 JAR 文件。
- 在“项目”窗口中右键单击 DBManager 的“模块”节点,然后选择“添加新库”。
- 选择上一子部分创建的 JAR 并完成向导,指定任意值。假设应用程序用于处理 shop.org 上的客户,在这种情况下,唯一标识符 "org.shop.model" 对应于代码名称基:
现在,新应用程序中已包含第一个定制模块,它包装的 JAR 包含实体类和 persistence.xml 文件:
创建其他相关模块
在此部分,将创建两个新模块,用来包装 EclipseLink JAR 和数据库连接器 JAR。
- 执行与为实体类 JAR 创建库包装器相同的操作,但现在使用的是 EclipseLink JAR,它位于先前创建的 "CustomerLibrary" Java 库中:

在“库包装模块”向导中,您可以按住 Ctrl 键并单击以选择多个 JAR。
- 接下来,再创建一个库包装模块,这次是为 Java DB 客户端 JAR 创建的,它位于 JDK 分发的 db/lib/derbyclient.jar 中。
设计用户界面
在此部分,将创建一个简单的原型用户界面,此界面提供了一个使用 JTextArea 显示从数据库检索的数据的窗口。
- 在“项目”窗口中右键单击 DBManager 的“模块”节点,然后选择“添加新模块”。创建一个名为 CustomerViewer 的新模块,其代码名称基为 org.shop.ui。
- 在“项目”窗口中,右键单击该新模块,然后选择“新建”|“窗口组件”。指定应在 editor 位置创建该窗口组件,并且在应用程序启动时应将其打开。将 Customer 设置为该窗口的类名前缀。
- 使用“组件面板”(Ctrl-Shift-8) 将一个 JTextArea 拖放到新窗口中。

- 在 TopComponent 构造函数的末尾添加以下代码:
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager(); Query query = entityManager.createQuery("SELECT c FROM Customer c"); List<Customer> resultList = query.getResultList(); for (Customer c : resultList) { jTextArea1.append(c.getName() + " (" + c.getCity() + ")" + "\n"); }因为您未在提供 Customer 对象和持久性 JAR 的模块上设置依赖关系,将使用表示错误的红色下划线标记上面的语句。此问题将在下一部分中解决。
在上面,您可以看到对一个名为 "CustomerLibraryPU" 的持久性单元的引用,此名称是在 persistence.xml 文件中设置的。此外,还有一个对名为 Customer 的实体类的引用,该实体类位于实体类模块中。如果不同于上面的内容,请根据需要修改这些代码。
设置依赖关系
在此部分,将使一些模块能够使用其他模块中的代码。通过在相关模块之间设置约定来清楚地执行此操作,即不会随意重复滥用代码(在没有诸如 NetBeans 平台所提供的严格模块化体系结构时容易发生此情况)。
- 实体类模块需要依赖于 Derby 客户端模块以及 EclipseLink 模块。右键单击 CustomerLibrary 模块,选择“属性”,然后使用“库”标签设置对 CustomerLibrary 模块所需的两个模块的依赖关系。
- CustomerViewer 模块需要依赖于 EclipseLink 模块以及实体类模块。右键单击 CustomerViewer 模块,选择“属性”,然后使用“库”标签设置对 CustomerViewer 模块所需的两个模块的依赖关系。
- 在“源”视图中打开 CustomerTopComponent,在编辑器中单击鼠标右键,然后选择“修复导入”。IDE 现在可以添加所需的导入语句,因为提供所需类的模块现已可用于 CustomerTopComponent。
现在,您已在应用程序中的各模块之间设置了约定,从而可以控制不同代码段之间的依赖关系。
运行原型
在此部分,将运行该应用程序,以便查看能否正确访问数据库。
- 启动数据库服务器。
- 运行应用程序。您应看到如下所示的内容:

现在,您已具有一个简单原型,它包含的 NetBeans 平台应用程序将显示数据库中的数据,下一节将对其进行扩展。
集成 CRUD 功能
为了创建与 NetBeans 平台顺利集成的 CRUD 功能,需要实现一些特定的 NetBeans 平台编码模式。以下部分详细介绍了这些模式。
读取
在此部分,将针对 NetBeans 平台资源管理器视图更改上一部分中引入的 JTextArea。NetBeans 平台资源管理器视图是一种 Swing 组件,与标准 Swing 组件相比,此组件与 NetBeans 平台集成的效果更好。它们支持很多功能,其中之一是上下文概念,以便与上下文相关联。
为了表示数据,NetBeans 平台 Node 类将提供一个通用的分层结构模型,此模型可通过任何 NetBeans 平台资源管理器视图显示。此部分末尾说明了如何将资源管理器视图与 NetBeans 平台“属性”窗口进行同步。
- 对于 TopComponent,在“设计”视图中删除 JTextArea,并在“源”视图中注释掉其相关代码:
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager(); Query query = entityManager.createQuery("SELECT c FROM Customer c"); List<Customer> resultList = query.getResultList(); //for (Customer c : resultList) { // jTextArea1.append(c.getName() + " (" + c.getCity() + ")" + "\n"); //} - 右键单击 CustomerViewer 模块,选择“属性”,然后使用“库”标签设置对节点 API 以及资源管理器和属性表单 API 的依赖关系。
- 接下来,更改类签名以实现 ExplorerManager.Provider:
final class CustomerTopComponent extends TopComponent implements ExplorerManager.Provider
将需要覆盖 getExplorerManager()
@Override public ExplorerManager getExplorerManager() { return em; }在类的顶部声明并初始化 ExplorerManager:
private static ExplorerManager em = new ExplorerManager();
有关以上代码的详细信息,请观看 Top 10 NetBeans APIs(最主要的 10 个 NetBeans API),特别是处理节点 API 以及资源管理器和属性表单 API 的截屏视频。
- 切换到 TopComponent 的“设计”视图,在“组件面板”中单击鼠标右键,选择“组件面板管理器”|“从 JAR 添加”。然后找到 NetBeans IDE 安装目录下 platform11/modules 文件夹中的 org-openide-explorer.jar。选择 BeanTreeView,然后完成向导。现在,应在“组件面板”中看到 BeanTreeView。将其从“组件面板”拖放到窗口上。
- 创建一个工厂类,它将为数据库中的每个客户创建一个新的 BeanNode:
import demo.Customer; import java.beans.IntrospectionException; import java.util.List; import org.openide.nodes.BeanNode; import org.openide.nodes.ChildFactory; import org.openide.nodes.Node; import org.openide.util.Exceptions; public class CustomerChildFactory extends ChildFactory<Customer> { private List<Customer> resultList; public CustomerChildFactory(List<Customer> resultList) { this.resultList = resultList; } @Override protected boolean createKeys(List<Customer> list) { for (Customer Customer : resultList) { list.add(Customer); } return true; } @Override protected Node createNodeForKey(Customer c) { try { return new BeanNode(c); } catch (IntrospectionException ex) { Exceptions.printStackTrace(ex); return null; } } } - 返回到 CustomerTopComponent,使用 ExplorerManager 将来自 JPA 查询的结果列表传递到 Node:
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager(); Query query = entityManager.createQuery("SELECT c FROM Customer c"); List<Customer> resultList = query.getResultList(); em.setRootContext(new AbstractNode(Children.create(new CustomerChildFactory(resultList), true))); //for (Customer c : resultList) { // jTextArea1.append(c.getName() + " (" + c.getCity() + ")" + "\n"); //} - 运行应用程序。在应用程序运行后,打开“属性”窗口。请注意,尽管数据可用并显示在 BeanTreeView 中,但 BeanTreeView 不会与“属性”窗口(可通过选择“窗口”|“属性”打开该窗口)保持同步。换言之,在树状分层结构中上下移动时,“属性”窗口中不会显示任何内容。
- 通过向 TopComponent 中的构造函数添加以下代码,将“属性”窗口与 BeanTreeView 进行同步。
associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
这里我们将 TopComponent 的 ActionMap 和 ExplorerManager 添加到 TopComponent 的 Lookup 中。此操作的一个副效应是“属性”窗口开始显示选定 Node 的显示名称和工具提示文本。
- 再次运行应用程序,注意,“属性”窗口现在与资源管理器视图保持同步:
现在,您可以在树状分层结构中查看数据,就如同使用 JTree 一样。但是,您还可以切换到其他浏览器视图,而无需更改该模型,因为 ExplorerManager 会在模型和视图之间进行协调。最后,您还可以将视图与“属性”窗口进行同步。
更新
在此部分,将首先创建一个编辑器。该编辑器将由一个新的 NetBeans 模块提供。因此,首先需要创建一个新的模块。然后,在新模块中创建一个新的 TopComponent,其中含有两个 JTextField(分别用于允许用户编辑的两个列)。您将需要使查看器模块与编辑器模块进行通信。每当在查看器模块中选择新的 Node 时,都会将当前的 Customer 对象添加到 Lookup 中。在编辑器模块中,将需要侦听 Lookup 以确定是否引入了 Customer 对象。每当将新的 Customer 对象引入到 Lookup 时,都会在编辑器中更新 JTextField。
接下来,将 JTextField 与 NetBeans 平台的撤销、重做和保存功能进行同步。换言之,当用户更改 JTextField 时,您希望可以使用 NetBeans 平台的现有功能,以便无需创建新功能,即可轻松获得 NetBeans 平台支持。为此,您需要使用 UndoRedoManager 和 SaveCookie。
- 创建一个新模块,命名为 CustomerEditor,并将 org.shop.editor 作为其代码名称基。
- 右键单击 CustomerEditor 模块,然后选择“新建”|“窗口组件”。确保指定在 editor 位置显示该窗口,并在应用程序启动时将其打开。在向导的最后一个面板中,将 "Editor" 设置为类名称前缀。
- 使用“组件面板”(Ctrl-Shift-8) 向新窗口中添加两个 JLabel 和两个 JTextField。将标签的文本设置为 "Name" 和 "City",并将两个 JTextField 的变量名称设置为 jTextField1 和 jTextField2。
在 GUI 生成器中,该窗口的外观应与下图类似:

- 返回到 CustomerViewer 模块并更改其 layer.xml 文件,指定 CustomerTopComponent 窗口将以 explorer 模式显示。
在更改 layer.xml 文件后,右键单击该应用程序项目,然后选择“清理”。为什么要这样做?因为每当运行应用程序并将其关闭后,都会将窗口位置存储到用户目录中。因此,如果 CustomerViewer 最初以 editor 模式显示,则会一直处于 editor 模式,直到执行“清理”操作,此操作会重置用户目录(即,删除用户目录),并使 CustomerViewer 在 layer.xml 文件中当前设置的位置显示。
同时,检查当用户调整应用程序大小时,CustomerViewer 中的 BeanTreeView 能否水平或垂直拉伸。检查方法是打开窗口,选择 BeanTreeView,然后单击 GUI 生成器工具栏中的箭头按钮。
- 运行应用程序,并确保在应用程序启动时显示以下内容:

- 现在我们可以开始添加一些代码。第一,我们需要在编辑器中显示当前选中的 Customer 对象:
- 首先调整 CustomerViewer 模块,以便每当选择了新的 Node 时,都会将当前的 Customer 对象添加到查看器窗口的 Lookup 中。为此,请在 CustomerChildFactory 类中创建 AbstractNode,而不是 BeanNode。这样,您就可以将当前 Customer 对象添加到该 Node 的 Lookup 中,如下所示(请注意粗体部分):
@Override protected Node createNodeForKey(Customer c) { Node node = new AbstractNode(Children.LEAF, Lookups.singleton(c)); node.setDisplayName(c.getName()); node.setShortDescription(c.getCity()); return node; // try { // return new BeanNode(c); // } catch (IntrospectionException ex) { // Exceptions.printStackTrace(ex); // return null; // } }现在,每当创建新的 Node(当用户在查看器中选择新的客户时即会执行此操作)时,就会将新的 Customer 对象添加到该 Node 的 Lookup 中。
- 现在,我们将通过某种方式来更改编辑器模块,使其窗口最终侦听被添加到 Lookup 的 Customer 对象。首先,在编辑器模块中设置对提供实体类的模块以及提供持久性 JAR 的模块的依赖关系。
- 接下来,更改 EditorTopComponent 类签名以实现 LookupListner:
public final class EditorTopComponent extends TopComponent implements LookupListener
- 覆盖 resultChanged,以便在将新的 Customer 对象引入 Lookup 中时,对 JTextField 进行更新:
@Override public void resultChanged(LookupEvent lookupEvent) { Lookup.Result r = (Lookup.Result) lookupEvent.getSource(); Collection<Customer> coll = r.allInstances(); if (!coll.isEmpty()) { for (Customer cust : coll) { jTextField1.setText(cust.getName()); jTextField2.setText(cust.getCity()); } } else { jTextField1.setText("[no name]"); jTextField2.setText("[no city]"); } } - 现在定义了 LookupListener,我们需要将其添加到某个对象。这里,我们将其添加到从全局上下文中获取的 Lookup.Result 中。全局上下文将代理选定 Node 的上下文。例如,如果在树状分层结构中选择了 "Ford Motor Co",则会将 "Ford Motor Co" 的 Customer 对象添加到该 Node 的 Lookup 中,这意味着 "Ford Motor Co" 的 Customer 对象当前在全局上下文中可用(因为该节点为当前选定的 Node)。随后即会将此对象传递到 resultChanged,以填充该文本字段。
每当编辑器窗口打开时,便开始执行上述所有操作,即会激活 LookupListener,如下所示:
@Override public void componentOpened() { result = Utilities.actionsGlobalContext().lookupResult(Customer.class); result.addLookupListener(this); resultChanged(new LookupEvent(result)); } @Override public void componentClosed() { result.removeLookupListener(this); result = null; }由于编辑器窗口会在应用程序启动时打开,因此在应用程序启动时 LookupListener 即可用。
- 最后,在类的顶部声明结果变量,如下所示:
private Lookup.Result result = null;
- 再次运行应用程序,注意,每当选择一个新的 Node 时,编辑器窗口即会更新。

但请注意,将焦点切换到编辑器窗口时会发生什么情况:

由于该 Node 不再是当前节点,因此 Customer 对象不再位于全局上下文中。这是因为全局上下文代理的是当前 Node 的 Lookup(如上文中所述)。因此,在这种情况下,我们不能使用全局上下文。而应使用 Customer 窗口提供的本地 Lookup。
将此行:
result = Utilities.actionsGlobalContext().lookupResult(Customer.class);
重写为:
result = WindowManager.getDefault().findTopComponent("CustomerTopComponent").getLookup().lookupResult(Customer.class);字符串 "CustomerTopComponent" 是 CustomerTopComponent 的 ID,它是一个字符串常量,可以在 CustomerTopComponent 源代码中找到。上述方法有一个缺点,即,EditorTopComponent 仅在找到 ID 为 "CustomerTopComponent" 的 TopComponent 时才发挥作用。可通过以下两种方法解决此问题:明确记录此问题,以使其他编辑器的开发人员了解他们需要这样标识查看器 TopComponent,或者重写该选定模型,如 Tim Boudreau 在此处所述。
如果使用上述一种方法,您会发现将焦点切换到 EditorTopComponent 时,上下文并未丢失,如下所示:

由于您现在使用的是 AbstractNode 而不是 BeanNode,“属性”窗口中不会显示任何属性。您需要自行提供这些属性,如节点 API 教程中所述。
- 首先调整 CustomerViewer 模块,以便每当选择了新的 Node 时,都会将当前的 Customer 对象添加到查看器窗口的 Lookup 中。为此,请在 CustomerChildFactory 类中创建 AbstractNode,而不是 BeanNode。这样,您就可以将当前 Customer 对象添加到该 Node 的 Lookup 中,如下所示(请注意粗体部分):
- 然后,让我们来处理撤销/重做功能。当用户更改某个 JTextField 时,我们希望启用“撤销”按钮、“重做”按钮以及“编辑”菜单中的相关菜单项。为此,NetBeans 平台提供了 UndoRedo.Manager。
- 在 EditorTopComponent 项部声明并实例化一个新的 UndoRedoManager:
private UndoRedo.Manager manager = new UndoRedo.Manager();
- 接下来,覆盖 EditorTopComponent 中的 getUndoRedo() 方法:
@Override public UndoRedo getUndoRedo() { return manager; } - 在 EditorTopComponent 的构造函数中,向 JTextField 中添加一个 KeyListener,并在需要实现的相关方法中,添加 UndoRedoListener:
jTextField1.getDocument().addUndoableEditListener(manager); jTextField2.getDocument().addUndoableEditListener(manager);
- 运行应用程序并显示运行中的撤销和重做功能,即相关按钮和菜单项。功能将按预期方式运行。您可能需要更改 KeyListener,以免任何键都可启用撤销/重做功能。例如,当按下 Enter 键时,您可能不希望启用撤销/重做功能。因此,请调整上述代码以满足您的业务需求。
- 在 EditorTopComponent 项部声明并实例化一个新的 UndoRedoManager:
- 接下来,我们需要集成 NetBeans 平台的保存功能。
- 缺省情况下,NetBeans 平台工具栏中提供了“全部保存”按钮。在当前情况下,我们并不希望保存“全部”,因为“全部”指许多不同的文档。在本例中,只有一个“文档”,即供树状分层结构中所有节点重复使用的编辑器。删除“全部保存”按钮,然后添加“保存”按钮,方法是向 CustomerEditor 模块的层文件中添加以下代码:
<folder name="Toolbars"> <folder name="File"> <file name="org-openide-actions-SaveAction.shadow"> <attr name="originalFile" stringvalue="Actions/System/org-openide-actions-SaveAction.instance"/> <attr name="position" intvalue="444"/> </file> <file name="org-openide-actions-SaveAllAction.shadow_hidden"/> </folder> </folder>现在运行应用程序时,将在工具栏中看到一个不同的图标。现在我们可以使用“保存”按钮,而非“全部保存”按钮。
- 设置对于对话框 API 和节点 API 的依赖关系。
- 在 EditorTopComponent 构造函数中添加一个调用,以便在每次检测到更改时触发一个方法(将在下一步骤中定义):
public EditorTopComponent() { ... ... ... jTextField1.getDocument().addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent arg0) { fire(true); } public void removeUpdate(DocumentEvent arg0) { fire(true); } public void changedUpdate(DocumentEvent arg0) { fire(true); } }); jTextField2.getDocument().addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent arg0) { fire(true); } public void removeUpdate(DocumentEvent arg0) { fire(true); } public void changedUpdate(DocumentEvent arg0) { fire(true); } }); //Create a new instance of our SaveCookie implementation: impl = new SaveCookieImpl(); //Create a new instance of our dynamic object: content = new InstanceContent(); //Add the dynamic object to the TopComponent Lookup: associateLookup(new AbstractLookup(content)); } ... ... ... - 以下是上面提到的两种方法。首先,每当检测到更改,就会触发该方法。每次检测到更改时,就会将节点 API 中的 SaveCookie 实现添加到 InstanceContent 中:
public void fire(boolean modified) { if (modified) { //If the text is modified, //we add SaveCookie impl to Lookup: content.add(impl); } else { //Otherwise, we remove the SaveCookie impl from the lookup: content.remove(impl); } } private class SaveCookieImpl implements SaveCookie { @Override public void save() throws IOException { Confirmation message = new NotifyDescriptor.Confirmation("Do you want to save \"" + jTextField1.getText() + " (" + jTextField2.getText() + ")\"?", NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.QUESTION_MESSAGE); Object result = DialogDisplayer.getDefault().notify(message); //When user clicks "Yes", indicating they really want to save, //we need to disable the Save action, //so that it will only be usable when the next change is made //to the JTextArea: if (NotifyDescriptor.YES_OPTION.equals(result)) { fire(false); //Implement your save functionality here. } } } - 运行应用程序并注意“保存”按钮的启用/禁用情况。

现在,单击上面对话框中的“确定”时什么也不会发生。在下一个步骤中,我们将添加一些 JPA 代码,用于处理更改的持久性。
- 接下来,我们会添加 JPA 代码以持久保留更改。要执行此操作,请替换注释 "//Implement your save functionality here."。应使用以下代码替换该注释:
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager(); entityManager.getTransaction().begin(); Customer c = entityManager.find(Customer.class, customer.getCustomerId()); c.setName(jTextField1.getText()); c.setCity(jTextField2.getText()); entityManager.getTransaction().commit();当前未定义 customer.getCustomerId()() 中的 "customer"。请在 resultChanged 中添加下面的粗体行(在类顶部声明 Customer customer; 后),以使当前 Customer 对象设置 customer,上面的持久性代码将使用它获取当前 Customer 对象的 ID。
@Override public void resultChanged(LookupEvent lookupEvent) { Lookup.Result r = (Lookup.Result) lookupEvent.getSource(); Collection<Customer> c = r.allInstances(); if (!c.isEmpty()) { for (Customer customer : c) { customer = cust; jTextField1.setText(customer.getName()); jTextField2.setText(customer.getCity()); } } else { jTextField1.setText("[no name]"); jTextField2.setText("[no city]"); } } - 运行应用程序并更改一些数据。目前,没有“刷新”功能(将在下一步中添加),因此,要查看更改的数据,请重新启动应用程序。例如,此处的树状分层结构显示了保留的 "Toyota Motor Co" 客户名称:

- 缺省情况下,NetBeans 平台工具栏中提供了“全部保存”按钮。在当前情况下,我们并不希望保存“全部”,因为“全部”指许多不同的文档。在本例中,只有一个“文档”,即供树状分层结构中所有节点重复使用的编辑器。删除“全部保存”按钮,然后添加“保存”按钮,方法是向 CustomerEditor 模块的层文件中添加以下代码:
- 第四,我们需要添加刷新客户查看器的功能。您可能希望添加一个定期刷新查看器的 Timer。而在此例中,我们将向根节点添加一个“刷新”菜单项,以便用户可以手动刷新查看器。
- 在 CustomerViewer 模块的主包中,创建一个新的 Node,用于替换在查看器中用作子对象根的 AbstractNode。注意,我们还会将一个“刷新”操作绑定到新的根节点。
public class CustomerRootNode extends AbstractNode { public CustomerRootNode(Children kids) { super(kids); setDisplayName("Root"); } @Override public Action[] getActions(boolean context) { Action[] result = new Action[]{ new RefreshAction()}; return result; } private final class RefreshAction extends AbstractAction { public RefreshAction() { putValue(Action.NAME, "Refresh"); } public void actionPerformed(ActionEvent e) { CustomerTopComponent.refreshNode(); } } } - 将以下方法添加到 CustomerTopComponent,用于刷新视图:
public static void refreshNode() { EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager(); Query query = entityManager.createQuery("SELECT c FROM Customer c"); List<Customer> resultList = query.getResultList(); em.setRootContext(new CustomerRootNode(Children.create(new CustomerChildFactory(resultList), true))); }现在,用对以上方法的调用替换 CustomerTopComponent 构造函数中的以上代码。在上面的突出显示部分,我们可以看到现在使用的是 CustomerRootNode,而不是 AbstractNode。CustomerRootNode 包括“刷新”操作,此操作将调用以上代码。
- 在保存功能中,添加对上述方法的调用,以便每次保存数据时,都会自动进行刷新。可以使用不同方法为保存功能实现此扩展。例如,您可能希望创建一个包含刷新操作的新模块。然后,在查看器模块和编辑器模块之间共享该模块,以便为两者提供相同的功能。
- 再次运行应用程序,注意,您拥有了一个新的根节点,其中带有“刷新”操作。

- 更改一些数据并保存,调用“刷新”操作,注意,将更新查看器。
- 在 CustomerViewer 模块的主包中,创建一个新的 Node,用于替换在查看器中用作子对象根的 AbstractNode。注意,我们还会将一个“刷新”操作绑定到新的根节点。
现在,您已学会了如何让 NetBeans 平台处理对 JTextField 所做的更改。当文本发生更改时,即会启用或禁用 NetBeans 平台的“撤销”和“重做”按钮。此外,还会正确启用和禁用“保存”按钮,让用户将更改的数据保存到数据库。
创建
在此部分,将允许用户在数据库中创建一个新的条目。
- 右键单击 CustomerEditor 模块,然后选择“新建操作”。使用“新建操作”向导创建一个新的“始终启用”操作。新的操作应显示在工具栏和/或菜单栏中的任意位置。在向导的下一步中,调用操作 NewAction。
确保有一个 16x16 的图标,当希望从工具栏调用此操作时,向导将强制选择此图标。
- 在新建操作中,使 TopComponent 处于打开状态,并使 JTextField 保留空白:
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public final class NewAction implements ActionListener { public void actionPerformed(ActionEvent e) { EditorTopComponent tc = EditorTopComponent.getDefault(); tc.resetFields(); tc.open(); tc.requestActive(); } }此操作将实现 ActionListener 类,此类通过层文件中的条目绑定到应用程序,并由“新建操作”向导在此处生成。设想一下将现有的 Swing 应用程序移植到 NetBeans 平台会有多么容易,因为您将可以使用与原始应用程序中相同的 Action 类,而无需重写这些类以符合 NetBeans 平台提供的 Action 类的标准!
在 EditorTopComponent 中,添加以下方法以重置 JTextField 并创建新的 Customer 对象:
public void resetFields() { customer = new Customer(); jTextField1.setText(""); jTextField2.setText(""); } - 在 SaveCookie 中,确保返回的 null 表示已保存新条目,而非更新了现有条目:
public void save() throws IOException { Confirmation message = new NotifyDescriptor.Confirmation("Do you want to save \"" + jTextField1.getText() + " (" + jTextField2.getText() + ")\"?", NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.QUESTION_MESSAGE); Object result = DialogDisplayer.getDefault().notify(msg); //When user clicks "Yes", indicating they really want to save, //we need to disable the Save button and Save menu item, //so that it will only be usable when the next change is made //to the text field: if (NotifyDescriptor.YES_OPTION.equals(result)) { fire(false); EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager(); entityManager.getTransaction().begin(); if (customer.getCustomerId() != null) { Customer c = entityManager.find(Customer.class, cude.getCustomerId()); c.setName(jTextField1.getText()); c.setCity(jTextField2.getText()); entityManager.getTransaction().commit(); } else { Query query = entityManager.createQuery("SELECT c FROM Customer c"); List<Customer> resultList = query.getResultList(); customer.setCustomerId(resultList.size()+1); customer.setName(jTextField1.getText()); customer.setCity(jTextField2.getText()); //add more fields that will populate all the other columns in the table! entityManager.persist(customer); entityManager.getTransaction().commit(); } } } - 再次运行应用程序,并向数据库中添加一个新客户:
删除
在此部分,将使用户能够删除数据库中选定的条目。使用上面介绍的概念和代码,自己实现“删除”操作。
- 创建一个新的操作 DeleteAction。确定要将其绑定到 "Customer" 节点,还是绑定到工具栏、菜单栏、快捷键或上述内容的组合。根据要绑定到的位置,您需要在代码中使用不同的方法。再次阅读教程以获取帮助,特别参见如何创建“新建”操作的部分,并将其与根节点上的“刷新”操作进行比较。
- 获取当前 Customer 对象,返回“您是否确定?”对话框,然后删除该条目。有关此阶段的相关帮助,请再次阅读教程,重点查看实现“保存”功能的部分。现在不是保存,而是从数据库中删除条目。
另请参见
NetBeans 平台 CRUD 教程到此结束。本文档介绍了如何针对给定数据库创建一个带有 CRUD 功能的新 NetBeans 平台应用程序。有关创建和开发应用程序的更多信息,请参见以下资源:
