第3章第一个helloapp应用 视频讲解 Hibernate是Java应用和关系数据库之间的桥梁,它能进行Java对象和关系数据之间的映射。Hibernate内部封装了通过JDBC访问数据库的操作,向上层应用提供面向对象的数据访问API。在Java应用中使用Hibernate包含以下步骤。 (1) 创建Hibernate的配置文件。 (2) 创建持久化类。 (3) 创建对象关系映射文件。 (4) 通过Hibernate API编写访问数据库的代码。 本章通过一个简单的helloapp应用示例,演示如何运用Hibernate来访问关系数据库。helloapp应用的功能非常简单,它通过Hibernate保存、更新、删除、加载及查询Customer对象。图31显示了Hibernate在helloapp应用中所处的位置。 图31Hibernate在helloapp应用中所处的位置 helloapp应用既能作为独立的Java程序运行,还能作为Java Web应用运行,该应用的源代码位于配套源代码包的sourcecode/chapter3/helloapp目录下。 3.1创建Hibernate的配置文件 Hibernate从其配置文件中读取和数据库连接有关的信息,这个配置文件位于应用的classpath中。Hibernate的配置文件有以下两种形式。 (1) Java属性文件,采用“键=值”的形式。默认文件名为hibernate.properties,默认存放路径为classpath的根目录。 (2) XML格式的配置文件。默认文件名为hibernate.cfg.xml,默认存放路径为classpath的根目录。 3.5.1节会介绍Hibernate在初始化过程中加载这两种配置文件的时机。Hibernate对XML格式的配置文件提供了强有力的支持,可以利用它来灵活地配置各种元素。因此本书范例主要采用XML格式的配置文件。 3.1.1用Java属性文件作为Hibernate配置文件 下面介绍如何以Java属性文件的格式来创建Hibernate的配置文件。这种配置文件的默认文件名为hibernate.properties,例程31为示范代码。 例程31hibernate.properties hibernate.dialect=org.hibernate.dialect.MySQLDialect hibernate.connection.driver_class=com.mysql.jdbc.Driver hibernate.connection.url= jdbc:mysql://localhost:3306/SAMPLEDB?useSSL=false hibernate.connection.username=root hibernate.connection.password=1234 hibernate.show_sql=true 以上hibernate.properties文件包含了一系列属性及其属性值,Hibernate将根据这些属性来连接数据库,本例为连接MySQL数据库的配置代码。表31对hibernate.properties文件中的所有属性做了描述。 表31Hibernate配置文件的属性 属性描述 hibernate.dialect指定数据库使用的SQL方言 hibernate.connection.driver_class指定数据库的驱动程序 hibernate.connection.url指定连接数据库的URL hibernate.connection.username指定连接数据库的用户名 hibernate.connection.password指定连接数据库的口令 hibernate.show_sql默认为false,如果为true,表示在程序运行时,会在控制台输出SQL语句,这有利于跟踪Hibernate的运行状态。在应用开发和测试阶段,可以把这个属性设为true,以便跟踪和调试应用程序; 在应用发布阶段,应该把这个属性设为false,以便减少应用的输出信息,提高运行性能 Hibernate能够访问多种关系数据库,如MySQL、Oracle和Sybase等。尽管多数关系数据库都支持标准的SQL语言,但是它们往往还有各自的SQL方言,就像不同地区的人既能说标准的普通话,还能讲各自的方言一样。hibernate.dialect属性用于指定被访问数据库所使用的SQL方言,当Hibernate生成SQL查询语句,或者使用native对象标识符生成策略时,都会参考本地数据库的SQL方言。第6章将详细介绍Hibernate的各种对象标识符生成策略。 3.1.2XML格式的Hibernate配置文件 XML格式的配置文件的默认名字为hibernate.cfg.xml,这个文件默认情况下也存放在classpath的根目录中。 XML格式的配置文件不仅能设置所有的Hibernate配置选项,还能够通过元素声明需要加载的映射文件,参见例程32。 例程32hibernate.cfg.xml文件 org.hibernate.dialect.MySQLDialect com.mysql.jdbc.Driver jdbc:mysql://localhost:3306/sampledb&useSSL=false root 1234 true 元素的子元素指定需要加载的映射文件。一个元素中可以包含多个子元素。 19.6节会进一步介绍Hibernate的XML格式配置文件的用法。 3.2创建持久化类 持久化类指其实例需要被Hibernate持久化到数据库中的类。持久化类通常都是域模型中的实体域类。持久化类符合JavaBean的规范,包含一些属性,以及与之对应的getXXX()和setXXX()方法。例程33定义了一个名为Customer的持久化类。 例程33Customer.java package mypack; import java.io.Serializable; import java.sql.Date; import java.sql.Timestamp; public class Customer implements Serializable{ private Long id; private String name; private String email; private String password; private int phone; private boolean married; private String address; private char sex; private String description; private byte[] image; private Date birthday; private Timestamp registeredTime; public Customer(){} public Long getId(){ return id; } /* Hibernate可以调用此方法,为id属性赋值,而业务逻辑层无法访问此方法。 3.5.3节将介绍把setId()方法设为private访问级别的原因。 */ private void setId(Long id){ this.id = id; } public String getName(){ return name; } public void setName(String name){ this.name=name; } //此处省略email、password和phone等属性的getXXX()和setXXX()方法 … } Hibernate允许这些属性的getXXX()和setXXX()方法可以采用任意的访问级别,包括public、protected、默认和private。getXXX()和setXXX()方法必须符合特定的命名规则,get和set后面紧跟属性的名字,并且属性名的首字母为大写,如name属性的get方法为getName(),如果把get方法命名为getname()或者getNAME(),会导致Hibernate在运行时抛出以下异常。 org.hibernate.PropertyNotFoundException: Could not find a getter for name in class mypack.Customer 在默认情况下,当Hibernate试图读取一个Customer对象的name属性时,会根据JavaBean的命名规则,自动去调用Customer对象的getName()方法,如果不存在这个方法,就会抛出以上异常。 5.1.2节还将会介绍,Hibernate并不强迫持久化类的属性必须有相应的get和set方法,因为Hibernate也可以直接访问持久化类的各种访问级别的属性。 如果持久化类的属性为boolean类型,那么它的get方法名既可以用get作为前缀,也可以用is作为前缀。例如,Customer类的married属性为boolean类型,因此以下两种get方法是等价的。 public boolean isMarried(){ return married; } 或者: public boolean getMarried(){ return married; } Hibernate并不要求持久化类必须实现java.io.Serializable接口,但是对于采用RMI或Java EE分布式结构的Java应用,当Java对象在不同的进程节点之间传输时,这个对象所属的类必须实现Serializable接口。此外,在Java Web应用中,如果希望对HttpSession(表示HTTP会话)中存放的Java对象进行持久化,那么这个Java对象所属的类也必须实现Serializable接口。 Customer持久化类有一个id属性,用来唯一标识Customer类的每个对象。在面向对象术语中,这个id属性被称为对象标识符(Object Identifier,OID),通常它都用整数表示,当然也可以设为其他类型。如果customerA.getId().equals(customerB.getId())的结果是true,就表示customerA和customerB对象指的是同一个客户,它们和CUSTOMERS表中的同一条记录对应。 Hibernate要求持久化类必须提供一个不带参数的默认构造方法,因为在程序运行时,Hibernate会运用Java反射机制,调用java.lang.reflect.Constructor.newInstance()方法来构造持久化类的实例。一般情况下,这个默认构造方法可以采用任意的访问级别。但是如果对这个持久化类使用延迟检索策略,为了使Hibernate能够在运行时为这个持久化类创建动态代理,要求持久化类的默认构造方法的访问级别必须是public、protected或默认类型,而不能是private类型。第16章将详细介绍Hibernate的延迟检索策略及动态代理的概念。 在Customer类中没有引入任何Hibernate API,Customer类不需要继承Hibernate的类,或实现Hibernate的接口,这提高了持久化类的独立性。如果日后要改用其他的ORM产品,如由Hibernate改为MyBatis,不需要修改持久化类的代码。 无论是基于CMP的实体EJB,还是基于BMP的实体EJB,它们的共同特点是都必须运行在EJB容器中。而Hibernate支持的持久化类不过是普通的Java类,它们能够运行在任何一种Java环境中。 这种具有较高独立性和可移植性的持久化类也被称为POJO。 3.3创建数据库Schema 在本例中,与Customer类对应的数据库表名为CUSTOMERS,它在MySQL数据库中的DDL定义如下: create table CUSTOMERS ( ID bigint not null primary key, NAME varchar(15) not null, EMAIL varchar(128) not null, PASSWORD varchar(8) not null, PHONE int , ADDRESS varchar(255), SEX char(1) , IS_MARRIED bit, DESCRIPTION text, IMAGE blob, BIRTHDAY date, REGISTERED_TIME timestamp ); CUSTOMERS表有一个ID字段,它是表的主键,它和Customer类的id属性对应。CUSTOMERS表中的字段使用了各种各样的SQL类型,参见表32。 表32CUSTOMERS表的字段使用的SQL类型 字段名SQL类型说明 IDBIGINT整数,占8字节,取值范围为-2^63~2^63 - 1 NAMEVARCHAR变长字符串,占0~255字节 SEXCHAR定长字符串,占0~255字节 IS_MARRIEDBIT布尔类型 DESCRIPTIONTEXT长文本数据,占0~65535字节。如果字符串长度小于255,可以用VARCHAR或CHAR类型来表示; 如果字符串长度大于255,可以定义为TEXT类型 IMAGEBLOB二进制长数据,占0~65 535字节,BLOB是Binary Large Object的缩写。在本例中,IMAGE字段用来存放图片数据 BIRTHDAYDATE代表日期,格式为YYYYMMDD REGISTERED_TIMETIMESTAMP代表日期和时间,格式为YYYYMMDDHHMMSS 3.4创建对象关系映射文件 Hibernate采用XML格式的文件来指定对象和关系数据之间的映射。在运行时,Hibernate将根据这个映射文件来生成各种SQL语句。在本例中,将创建一个名为Customer.hbm.xml的文件,它用于把Customer类映射到CUSTOMERS表。例程34为Customer.hbm.xml文件的代码。 例程34Customer.hbm.xml 3.4.1映射文件的文档类型定义(DTD) 在例程34的开头声明了DTD(Document Type Definition,文档类型定义),它对XML文件的语法和格式做了定义。Hibernate的XML解析器将根据DTD来核对XML文件的语法。 每一种XML文件都有独自的DTD文件。Hibernate的对象关系映射文件使用的DTD文件的下载网址为http://www.hibernate.org/dtd/hibernatemapping3.0.dtd。此外,在Hibernate下载软件包的展开目录的project\hibernatecore\src\main\resources\org\hibernate目录下也提供了hibernatemapping3.0.dtd文件。在这个文件中,描述顶层元素的代码如下: 描述顶层元素的子元素的代码如下: 元素是对象关系映射文件的根元素,其他元素(即以上DTD代码中括号以内的元素,如子元素)必须嵌入在元素以内。在元素中又嵌套了很多子元素,如子元素。在以上DTD代码中,还使用了一系列的特殊符号来修饰元素,表33描述了这些符号的作用。在创建自己的对象关系映射文件时,如果不熟悉某种元素的语法,可以参考DTD文件。 表33DTD中特殊符号的作用 符号含义 无符号该子元素在父元素内必须存在且只能存在一次 +该子元素在父元素内必须存在,可以存在一次或者多次 *该子元素在父元素内可以不存在,或者存在一次或者多次,它是比较常用的符号 ?该子元素在父元素内可以不存在,或者只存在一次,它是比较常用的符号 根据表33可以看出,在元素中,等子元素可以不存在,或者存在一次或者多次; 在元素中,子元素必须存在且只能存在一次,子元素可以不存在,或者存在一次或者多次。 此外,在映射文件中,父元素中的各种子元素的定义必须符合特定的顺序。例如,根据元素的DTD可以看出,必须先定义子元素,再定义子元素,以下映射代码颠倒了子元素的位置。 Hibernate的XML解析器在运行时会抛出如下InvalidMappingException异常。 org.hibernate.InvalidMappingException: Could not parse mapping document from resource mypack/Customer.hbm.xml 3.4.2把Customer持久化类映射到CUSTOMERS表 例程34用于映射Customer类。如果需要映射多个持久化类,那么既可以在同一个映射文件中映射所有类,也可以为每个类创建单独的映射文件,映射文件和类同名,扩展名为hbm.xml。后一种做法更值得推荐,因为在团队开发中,这有利于管理和维护映射文件。 元素指定类和表的映射,它的name属性设定类名,table属性设定表名。以下代码表明和Customer类对应的表为CUSTOMERS表。 如果没有设置元素的table属性,Hibernate将直接以类名作为表名,也就是说,在默认情况下,Hibernate认为与mypack.Customer类对应的表为Customer表。 元素包含一个子元素及多个子元素。子元素设定持久化类的OID和表的主键的映射。以下代码表明Customer类的id属性和CUSTOMERS表中的ID字段对应。 元素的子元素指定对象标识符生成器,它负责为OID生成唯一标识符。 子元素设定类的属性和表的字段的映射。子元素主要包括name、type、notnull和column属性。 1. 元素的name属性 元素的name属性指定持久化类的属性的名字。 2. 元素的type属性 元素的type属性指定Hibernate映射类型。Hibernate映射类型是Java类型与SQL类型的桥梁。表34列出了Customer类的属性的Java类型、Hibernate映射类型,以及CUSTOMERS表的字段的SQL类型这三者之间的对应关系。 表34Java类型、Hibernate映射类型及SQL类型之间的对应关系 Customer类的属性Java类型Hibernate映射类型CUSTOMERS表的字段SQL类型 namejava.lang.StringstringNAMEVARCHAR(15) emailjava.lang.StringstringEMAILVARCHAR(128) passwordjava.lang.StringstringPASSWORDVARCHAR(8) phoneintintPHONEINT addressjava.lang.StringstringADDRESSVARCHAR(255) sexcharcharacterSEXCHAR(1) marriedbooleanbooleanIS_MARRIEDBIT descriptionjava.lang.StringtextDESCRIPTIONTEXT imagebyte[]materialized_blobIMAGEBLOB birthdayjava.sql.DatedateBIRTHDAYDATE registeredTimejava.sql.TimestamptimestampREGISTERED_TIMETIMESTAMP 从表34可以看出,如果Customer类的一个属性为java.lang.String类型,并且与此对应的CUSTOMERS表的字段为VARCHAR类型,那么应该把Hibernate映射类型设为string。例如对name属性的映射代码如下: 如果Customer类的一个属性为java.lang.String类型,并且与此对应的CUSTOMERS表的字段为TEXT类型,那么应该把Hibernate映射类型设为text。例如对description属性的映射代码如下: 如果Customer类的一个属性为byte[]类型,并且与此对应的CUSTOMERS表的字段为BLOB类型,那么应该把Hibernate映射类型设为materialized_blob。例如对image属性的映射代码如下: 如果没有为某个属性显式设定映射类型,Hibernate会运用Java反射机制先识别出持久化类的特定属性的Java类型,然后自动使用与之对应的默认的Hibernate映射类型。例如,Customer类的address属性为java.lang.String类型,与java.lang.String对应的默认的映射类型为string,因此以下两种设置方式是等价的。 或者: 对于Customer类的description属性,尽管它是java.lang.String类型,由于CUSTOMERS表的DESCRIPTION字段为text类型,因此必须显式地把映射类型设为text。 3. 元素的notnull属性 元素的notnull属性默认为false,如果为true,表明不允许为null。如以下代码表明不允许Customer类的name属性为null。 Hibernate在持久化一个Customer对象时,会先检查它的name属性是否为null,如果为null,就会抛出以下异常。 Exception in thread "main" org.hibernate.PropertyValueException: not-null property references a null or transient value: mypack.Customer.name 如果数据库中CUSTOMERS表的NAME字段不允许为null,但在映射文件中没有设置notnull属性。例如: 那么Hibernate在持久化一个Customer对象时,不会先检查它的name属性是否为null,而是直接通过JDBC API向CUSTOMERS表插入相应的数据,由于CUSTOMERS表的NAME字段设置了not null约束,因此数据库会抛出如下错误。 708 ERROR JDBCExceptionReporter:58 - General error, message from server: "Column 'NAME' cannot be null" 值得注意的是,对于实际Java应用,当持久化一个Java对象时,不应该依赖Hibernate或数据库来负责数据验证。在四层应用结构中,应该由表述层或者业务逻辑层负责数据验证。因为错误发现得越早,越容易纠正,并且可以减少错误带来的不良后果,提高程序的运行性能。 例如,对于Customer对象的name属性,事实上在表述层就能检查name属性是否为null,假如表述层、业务逻辑层和Hibernate持久化层都没有检查name属性是否为null,那么数据库层会监测到NAME字段违反了数据完整性约束,从而抛出异常,如图32所示,包含非法数据的Customer对象从表述层依次传到数据库层,随后从数据库层抛出的错误信息又依次传到表述层,这种做法显然会降低数据验证的效率。 图32由数据库层负责数据验证 既然如此,把元素的notnull属性设为true有何意义呢?这主要是便于在软件开发和测试阶段能捕获表述层或者业务逻辑层应该处理而未处理的异常,提醒开发人员在表述层或者业务逻辑层中加入必要的数据验证逻辑。 4. 元素的column属性 元素的column属性指定与类的属性映射的表的字段名。以下代码表明和address属性对应的字段为ADDRESS字段。 如果没有设置元素的column属性,Hibernate将直接以类的属性名作为字段名,也就是说,在默认情况下,Hibernate认为与Customer类的address属性对应的字段为address字段。 元素还可以包括子元素,它和元素的column属性一样,都可以设定与类的属性映射的表的字段名。以下两种设置方式是等价的。 或者: 元素的子元素比column属性提供更多的功能,它可以更加详细地描述表的字段。例如,以下子元素指定CUSTOMERS表中的NAME字段的SQL类型为varchar(15),不允许为null,并且为这个字段建立了索引。 子元素主要和hbm2ddl工具联合使用。当使用hbm2ddl工具来自动生成数据库Schema时,hbm2ddl工具将依据子元素提供的信息来定义表的字段。关于hbm2ddl工具的用法参见4.2.2节。如果数据库Schema是手动创建的,就不必通过子元素设定字段的详细信息,只需要设定它的name属性和notnull属性就可以了,例如: 或者: 除了notnull属性以外,子元素的多数属性(如sqltype或index属性)都不会影响Hibernate的运行时行为。 图33显示了Customer.hbm.xml配置的对象关系映射。 图33Customer.hbm.xml配置的映射 Hibernate采用XML文件来配置对象关系映射,有以下优点。 (1) Hibernate既不会渗透到上层域模型中,也不会渗透到下层关系数据模型中。 (2) 域模型和关系数据模型彼此独立。软件开发人员可以独立设计域模型,不必强迫遵守任何规范; 数据库设计人员也可以独立设计关系数据模型,不必强迫遵守任何规范。 (3) 对象关系映射不依赖于任何程序代码,如果需要修改对象关系映射,只需要修改XML文件,不需要修改任何程序,这提高了软件的灵活性,并且使维护更加方便。 3.5通过Hibernate API操纵数据库 Hibernate对JDBC进行了封装,提供了更加面向对象的API。图34和图35对比了直接通过JDBC API及通过Hibernate API来访问数据库的两种方式。 图34通过JDBC API访问数据库 图35通过Hibernate API访问数据库 例程35的BusinessService类演示了通过Hibernate API对Customer对象进行持久化的操作。 3.4节提到Hibernate没有渗透到域模型中,即在持久化类中没有引入任何Hibernate API。但是对于应用中负责处理业务的过程域对象,应该借助Hibernate API来操纵数据库。 例程35BusinessService.java package mypack; import javax.servlet.ServletContext; import org.hibernate.*; import org.hibernate.query.Query; import org.hibernate.boot.*; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import java.io.*; import java.sql.Date; import java.sql.Timestamp; import java.util.*; public class BusinessService{ public static SessionFactory sessionFactory; /** 初始化Hibernate,创建SessionFactory实例 */ static{ //创建标准服务注册器 StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder() .configure()//加载XML格式的Hibernate配置文件 .build(); try{ //创建代表映射元数据的MetaData对象 Metadata metadata = new MetadataSources( standardRegistry ) .buildMetadata(); //创建SessionFactory对象 sessionFactory = metadata.getSessionFactoryBuilder() .build(); }catch(RuntimeException e){ //销毁标准服务注册器 StandardServiceRegistryBuilder.destroy( standardRegistry ); e.printStackTrace(); throw e; } } /** 查询所有的Customer对象, 然后调用printCustomer()方法打印Customer对象信息 */ public void findAllCustomers(ServletContext context, PrintWriter out)throws Exception{…} /** 持久化一个Customer对象 */ public void saveCustomer(Customer customer){ Session session = sessionFactory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); session.save(customer); tx.commit(); }catch (RuntimeException e) { if (tx != null) { tx.rollback(); } throw e; } finally { session.close(); } } /** 按照OID加载一个Customer对象,然后修改它的属性 */ public void loadAndUpdateCustomer(Long customer_id, String address){……} /**删除Customer对象 */ public void deleteCustomer(Customer customer){ Session session = sessionFactory.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); session.delete(customer); tx.commit(); }catch (RuntimeException e) { if (tx != null) { tx.rollback(); } throw e; } finally { session.close(); } } /** 选择向控制台还是向Web网页输出Customer对象的信息 */ private void printCustomer(ServletContext context, PrintWriter out,Customer customer)throws Exception{ if(context!=null) printCustomerInWeb(context,out,customer); else printCustomer( out,customer); } /** 把Customer对象的信息输出到控制台,如DOS 控制台*/ private void printCustomer(PrintWriter out,Customer customer) throws Exception{ byte[] buffer=customer.getImage(); FileOutputStream fout=new FileOutputStream("photo_copy.gif"); fout.write(buffer); fout.close(); out.println("------以下是"+customer.getName()+"的个人信息------"); out.println("ID: "+customer.getId()); out.println("口令: "+customer.getPassword()); out.println("E-mail: "+customer.getEmail()); out.println("电话: "+customer.getPhone()); out.println("地址: "+customer.getAddress()); … } /** 把Customer对象的信息输出到动态网页 */ private void printCustomerInWeb(ServletContext context, PrintWriter out,Customer customer)throws Exception{ //保存照片 byte[] buffer=customer.getImage(); String path=context.getRealPath("/"); FileOutputStream fout=new FileOutputStream(path+"photo_copy.gif"); fout.write(buffer); fout.close(); out.println("------以下是"+customer.getName()+"的个人信息------" +"
"); out.println("ID: "+customer.getId()+"
"); out.println("口令: "+customer.getPassword()+"
"); out.println("E-mail: "+customer.getEmail()+"
"); out.println("电话: "+customer.getPhone()+"
"); out.println("地址: "+customer.getAddress()+"
"); … } public void test(ServletContext context,PrintWriter out) throws Exception{ Customer customer=new Customer(); customer.setName("Tom"); customer.setEmail("tom@yahoo.com"); customer.setPassword("1234"); customer.setPhone(55556666); customer.setAddress("Shanghai"); customer.setSex('M'); customer.setDescription("I am very honest."); //设置Customer对象的image属性,它是字节数组 //存放photo.gif文件中的二进制数据 //photo.gif文件和BusinessService.class文件位于同一个目录下 InputStream in=this.getClass().getResourceAsStream("photo.gif"); byte[] buffer = new byte[in.available()]; in.read(buffer); customer.setImage(buffer); //设置Customer对象的birthday属性,它是java.sql.Date类型 customer.setBirthday(Date.valueOf("1980-05-06")); saveCustomer(customer); findAllCustomers(context,out); loadAndUpdateCustomer(customer.getId(),"Beijing"); findAllCustomers(context,out); deleteCustomer(customer); } public static void main(String args[]) throws Exception{ new BusinessService().test(null,new PrintWriter(System.out,true)); sessionFactory.close(); } } 这个例子演示了通过Hibernate API访问数据库的一般流程。首先应该在应用的启动阶段对Hibernate进行初始化,然后再通过Hibernate的Session接口来访问数据库。 3.5.1Hibernate的初始化 Hibernate的初始化也称作程序引导(BootStrap)。在本范例中BusinessService类的静态代码块负责Hibernate的初始化工作,如读取Hibernate的配置信息及对象关系映射信息,最后创建SessionFactory实例。当JVM(Java Virtual Machine,Java虚拟机)加载BusinessService类时,会执行该静态代码块。初始化过程包括如下步骤。 (1) 创建一个标准服务注册器StandardServiceRegistry对象。 //创建标准服务注册器 StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder() .build();//创建StandardServiceRegistry对象 StandardServiceRegistry标准服务注册器为运行Hibernate提供了一组服务,包括:  org.hibernate.boot.registry.classloading.spi.ClassLoaderService: 类加载服务。  org.hibernate.integrator.pi.IntegratorService: 集成服务。  org.hibernate.boot.registry.selector.spi.StrategySelector: 策略选择服务。 StandardServiceRegistryBuilder类的build()方法负责创建StandardServiceRegistry对象。在通过new语句创建StandardServiceRegistryBuilder对象时,它的构造方法会加载hibernate.properties配置文件,如果加载成功,就把配置文件中的数据加载到内存中; 如果不存在hibernate.properties配置文件,也不会报错。 如果希望加载XML格式的Hibernate配置文件,那么需要调用StandardServiceRegistryBuilder对象的configure()方法。代码如下: //创建标准服务注册器 StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder() .configure();//加载XML格式的Hibernate配置文件 .build(); //创建StandardServiceRegistry对象 该代码采用了方法链编程风格,它能使应用程序代码更加简洁。在使用这种编程风格时,最好把每个调用方法放在不同的行,否则在跟踪程序时,无法跳入每个调用方法中。 StandardServiceRegistryBuilder类的不带参数的configure()方法会按照默认方式加载classpath根路径下的hibernate.cfg.xml配置文件。如果不存在该文件,configure()方法会抛出如下异常。 org.hibernate.HibernateException: /hibernate.cfg.xml not found 此外,还可以通过StandardServiceRegistryBuilder类的带参数的configure(String resourceName)方法来显式指定需要加载的XML格式的配置文件。代码如下: //创建标准服务注册器 StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder() .configure("mypack/hibernate.cfg.xml");//加载指定的配置文件 .build();//创建StandardServiceRegistry对象 默认情况下,StandardServiceRegistry标准服务注册器为运行Hibernate提供了一组标准服务,如果希望采用客户化的服务,则可以利用BootstrapServiceRegistryBuilder,按照如下方式先创建BootstrapServiceRegistry引导服务注册器,再创建StandardServiceRegistry标准服务注册器。 BootstrapServiceRegistry bootstrapRegistry = new BootstrapServiceRegistryBuilder(); applyClassLoader(customClassLoader);//加入客户化ClassLoader applyIntegrator( customIntegrator ); //加入客户化Integrator build(); //创建BootstrapServiceRegistry对象 StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder(bootstrapRegistry) .build(); //创建StandardServiceRegistry对象 创建的StandardServiceRegistry将采用客户化的ClassLoader和Integrator服务。 当通过StandardServiceRegistryBuilder类的不带参数的构造方法创建StandardServiceRegistryBuilder对象时,该构造方法会隐式地先创建BootstrapServiceRegistry对象。 (2) 创建代表映射元数据的MetaData对象。 //创建代表映射元数据的MetaData对象 Metadata metadata = new MetadataSources( standardRegistry ) .buildMetadata(); 当使用XML格式的Hibernate配置文件时,其中的元素指定了Customer.hbm.xml映射文件。在创建MetadataSources对象时,它的构造方法会把元素指定的映射文件中的元数据加载到内存中。MetadataSources类的buildMetaData()方法创建并返回一个MetaData对象。 如果采用Java属性文件作为Hibernate配置文件,则必须通过MetadataSources类的addResource(String name)方法把相关的映射文件的元数据加入到内存中。代码如下: //创建代表映射元数据的MetaData对象 Metadata metadata = new MetadataSources( standardRegistry ) .addResource("mypack/Customer.hbm.xml" )//加载映射文件 .buildMetadata(); MetadataSources类的addResource()方法会把classpath根目录下的mypack/Customer.hbm.xml文件中的映射信息读入到内存中。 (3) 创建SessionFactory对象。 sessionFactory = metadata.getSessionFactoryBuilder() .build(); 该代码创建一个SessionFactory实例,并把所有配置信息复制到SessionFactory对象的缓存中。SessionFactory代表一个数据库存储源,如果应用只有一个数据库存储源,那么只需要创建一个SessionFactory实例。当SessionFactory对象创建后,该对象不和MetaData对象以及MetaDataSource对象关联。因此,如果修改MetaDataSource对象包含的配置信息,不会对SessionFactory对象的行为有任何影响。 由于Java语言是纯面向对象的语言,因此它不可能像C语言那样直接操纵内存,如声明一段可用的内存空间。步骤(3)中提到了缓存的概念,这里的缓存其实指的是Java对象的属性(通常是一些集合类型的属性)占用的内存空间。例如,在Hibernate早期版本的SessionFactory的实现类中定义了许多集合类型的属性,这些属性用于存放Hibernate配置信息、映射元数据信息等。代码如下: public final class SessionFactoryImpl implements SessionFactory, SessionFactoryImplementor { private final transient Map entityPersisters; private final transient Map classMetadata; private final transient Map collectionPersisters; private final transient Map collectionMetadata; private final transient Map collectionRolesByEntityParticipant; private final transient Map identifierGenerators; private final transient Map namedQueries; private final transient Map namedSqlQueries; private final transient Map sqlResultSetMappings; private final transient Map filters; private final transient Map imports; private final transient Interceptor interceptor; private final transient Settings settings; private final transient Properties properties; private transient SchemaExport schemaExport; private final transient TransactionManager transactionManager; private final transient QueryCache queryCache; private final transient UpdateTimestampsCache updateTimestampsCache; private final transient Map queryCaches; private final transient Map allCacheRegions = new HashMap(); … } 对于Hibernate后期版本的SessionFactory实现类,则把以上属性封装到一些自定义的类中。如果对象的缓存很大,就称为重量级对象; 如果对象占用的内存空间很小,就称为轻量级对象。SessionFactory是一个重量级对象,如果应用只有一个数据存储源,只需要创建一个SessionFactory实例,因为随意地创建SessionFactory实例会占用大量内存空间。 SessionFactory的缓存可分为两类: 内置缓存和外置缓存(也称为第二级缓存)。SessionFactory的内置缓存中存放了Hibernate配置信息和映射元数据信息等; SessionFactory的外置缓存是一个可配置的缓存插件,在默认情况下,SessionFactory不会启用它,外置缓存能存放大量数据库数据的副本,它的物理介质可以是内存或者硬盘。第22章将对外置缓存做详细介绍。 3.5.2Hibernate的遗留初始化方式 对于早期版本的Hibernate,采用org.hibernate.cfg.Configuration类来初始化Hibernate。尽管Hibernate 5依然支持这种初始化方式,但是这种方式已经不再提倡使用。 1. 使用Java属性文件作为Hibernate配置文件时的初始化方式 以下是用Configuration类来初始化Hibernate的程序代码。 //根据默认位置的Hibernate配置文件的配置信息,创建一个Configuration实例 Configuration config = new Configuration(); //加载Customer类的对象-关系映射文件 config.addClass(Customer.class); //创建SessionFactory实例 sessionFactory = config.buildSessionFactory(); 在通过new语句创建Configuration对象时,它的构造方法会加载hibernate.properties配置文件。 2. 使用XML格式Hibernate配置文件时的初始化方式 如果希望加载XML格式的Hibernate配置文件,那么需要调用Configuration对象的configure()方法。代码如下: sessionFactory = new Configuration() .configure()//加载XML格式的Hibernate配置文件 .buildSessionFactory(); 当Configuration对象的configure()方法从classpath根路径下加载XML格式的Hibernate配置文件hibernate.cfg.xml时,如果该配置文件不存在,会抛出HibernateException异常。 当使用XML格式的Hibernate配置文件时,其中的元素指定了Customer.hbm.xml映射文件。Configuration对象的configure()方法把元素指定的映射文件中的元数据加载到内存中,因此不需要再调用Configuration对象的config.addClass(Customer.class)方法来加载Customer类的映射文件。 3.5.3访问Hibernate的Session接口 初始化过程结束后,就可以调用SessionFactory实例的openSession()方法来获得Session实例,然后通过它执行访问数据库的操作。Session接口提供了操纵数据库的各种方法,如: (1) save()方法或persist()方法: 把Java对象保存到数据库中。 (2) update()方法: 更新数据库中的Java对象。 (3) delete()方法: 把特定的Java对象从数据库中删除。 (4) load()方法或get()方法: 从数据库中加载Java对象。 Hibernate 的save()和 persist()方法都用来保存持久化对象,两者的区别在于: save()方法返回持久化对象的对象标识符值(即对应数据库表中记录的主键值); 而persist()方法没有任何返回值。 Session是一个轻量级对象。通常将每一个Session实例和一个数据库事务绑定,也就是说,每执行一个数据库事务,都应该先创建一个新的Session实例。如果事务执行中出现异常,应该撤销事务。不论事务执行成功与否,最后都应该调用Session的close()方法,从而释放Session实例占用的资源。以下代码演示了用Session来执行事务的流程,其中Transaction类用来控制事务。 Session session = factory.openSession(); Transaction tx; try { //开始一个事务 tx = session.beginTransaction(); //执行事务 … //提交事务 tx.commit(); }catch (RuntimeException e) { //如果出现异常,就撤销事务 if (tx!=null) tx.rollback(); throw e; }finally { //不管事务执行成功与否,最后都关闭Session session.close(); } 从Hibernate 3版本开始,Hibernate抛出的异常都属于运行时异常,20.5.1节将对此做进一步阐述。 图36为正常执行数据库事务(即没有发生异常)的时序图。 图36正常执行数据库事务的时序图 BusinessService类提供了保存、删除、查询和更新Customer对象的各种方法。BusinessService类的main()方法调用test()方法,test()方法又调用以下方法。 1. saveCustomer()方法 该方法调用如下Session的save()方法,把Customer对象持久化到数据库中。 tx = session.beginTransaction(); session.save(customer); tx.commit(); 当运行session.save()方法时,Hibernate执行以下SQL语句。 insert into CUSTOMERS (ID, NAME, EMAIL, PASSWORD, PHONE, ADDRESS, SEX, IS_MARRIED,DESCRIPTION, IMAGE, BIRTHDAY, REGISTERED_TIME) values(1,'Tom','tom@yahoo.com','1234',55556666,'Shanghai', 'M',0,'I am very honest.', ,'1980-05-06',null) 在test()方法中并没有设置Customer对象的id属性,Hibernate会根据映射文件的配置,采用increment标识符生成器自动以递增的方式为Customer对象的id属性赋值。在Customer.hbm.xml文件中相关的映射代码如下: 在test()方法中也没有设置Customer对象的registeredTime属性,因此在以上insert语句中,REGISTERED_TIME字段的值为null。但由于REGISTERED_TIME字段的SQL类型为TIMESTAMP类型,如果insert语句没有为TIMESTAMP类型的字段赋值,底层数据库会自动把当前的系统时间赋值给TIMESTAMP类型的字段。因此,执行完以上insert语句后,REGISTERED_TIME字段的值并不为null,而是为插入该记录时的系统时间。 2. findAllCustomers()方法 该方法通过org.hibernate.query.Query接口来查询所有的Customer对象。代码如下: tx = session.beginTransaction();//开始一个事务 Query query=session.createQuery("from Customer as c" +" order by c.name asc"); List customers=query.list(); for (Iterator it = customers.iterator(); it.hasNext();) { printCustomer(context,out,(Customer) it.next()); } tx.commit(); //提交事务 org.hibernate.query.Query接口是Hibernate API中的查询接口。以上代码先通过Session的createQuery()方法创建了一个Query实例,向Session的createQuery()方法传递的参数为from Customer as c order by c.name asc,它使用的是面向对象的HQL(Hibernate Query Language,Hibernate查询语言)。运行query.list()方法时,Hibernate执行以下SQL语句。 select * from CUSTOMERS order by NAME asc; org.hibernate.query.Query接口继承了JPA API中的javax.persistence.Query接口,在javax.persistence.Query接口中定义了getResultList()方法,它和org.hibernate.query.Query接口的list()方法完成相同的功能。在org.hibernate.query.Query接口中,也可以调用getResultList()方法。此外,还可以在Session的createQuery()方法中设定需要检索的对象类型。代码如下: Query query=session.createQuery( "from Customer as c" +" order by c.name asc",Customer.class); List customers=query.getResultList(); for(Iterator it = customers.iterator(); it.hasNext();){ //无须对it.next()方法的返回对象进行强制类型转换 printCustomer(context,out, it.next()); } tx.commit(); //提交事务 3. loadAndUpdateCustomer()方法 该方法调用Session的get()方法,加载Customer对象,然后再修改Customer对象的属性。代码如下: tx = session.beginTransaction(); Customer c=session.get(Customer.class,customer_id); c.setAddress(address); tx.commit(); 该代码先调用Session的get()方法,它按照参数指定的OID从数据库中检索出匹配的Customer对象,Hibernate会执行以下SQL语句。 select * from CUSTOMERS where ID=1; loadAndUpdateCustomer()方法接着修改Customer对象的address属性。那么,Hibernate会不会同步更新数据库中相应的CUSTOMERS表的记录呢?答案是肯定的。Hibernate会自动进行脏检查(Dirty Check),按照内存中的Customer对象的状态的变化,来同步更新数据库中相关的数据,Hibernate会执行以下SQL语句。 update CUSTOMERS set NAME="Tom",EMAIL="tom@yahoo.com" … ADDRESS="Beijing"… where ID=1; 尽管只有Customer对象的address属性发生了变化,但是Hibernate执行的update语句中会包含所有的字段。 在BusinessService类的test()方法中按如下方式调用loadAndUpdateCustomer()方法。 saveCustomer(customer); //第一个参数为customer.getId(),表示Customer对象的ID loadAndUpdateCustomer(customer.getId(),"Beijing"); 传给saveCustomer()方法的customer对象的id属性为null。当执行saveCustomer(customer)方法时,Session的save()方法把Customer对象持久化到数据库中,并自动为Customer对象的id属性赋值。 Customer对象的id属性与CUSTOMERS表的ID主键对应,这是一个代理主键,不具有任何业务含义,主键的取值通常由持久化层的Hibernate或者由数据库系统自动生成。业务逻辑层一般不负责为不具有业务含义的OID赋值。因此,作为一种良好的编程规范,可以在Customer类的定义中把setId()方法的访问级别设为private。例如: private void setId(Long id) 这样,业务逻辑层就无法为Customer对象的id属性赋值了,但是Hibernate能通过Java反射机制来访问Customer对象的setId()方法,从而为Customer对象的id属性赋值。 4. printCustomer(ServletContext context,PrintWriter out,Customer customer)方法 该方法打印Customer对象的信息。当helloapp应用作为独立应用程序运行时,将调用printCustomer(PrintWriter out,Customer customer)方法,向控制台输出Customer信息; 当helloapp应用作为Java Web应用运行时,将调用printCustomerInWeb(ServletContext context,PrintWriter out,Customer customer)方法向Web客户输出Customer信息。代码如下: private void printCustomerInWeb(ServletContext context, PrintWriter out,Customer customer)throws Exception{ //把Customer对象的image属性包含的二进制图片数据保存 //到photo_copy.gif文件中 byte[] buffer=customer.getImage(); String path=context.getRealPath("/"); FileOutputStream fout=new FileOutputStream(path +"photo_copy.gif"); fout.write(buffer); fout.close(); out.println("------以下是"+customer.getName() +"的个人信息------"+"
"); out.println("ID: "+customer.getId()+"
"); out.println("口令: "+customer.getPassword()+"
"); out.println("E-mail: "+customer.getEmail()+"
"); out.println("电话: "+customer.getPhone()+"
"); out.println("地址: "+customer.getAddress()+"
"); String sex=customer.getSex()=='M'? "男":"女"; out.println("性别: "+sex+"
"); String marriedStatus=customer.isMarried()? "已婚":"未婚"; out.println("婚姻状况: "+marriedStatus+"
"); out.println("生日: "+customer.getBirthday()+"
"); out.println("注册时间: "+customer.getRegisteredTime()+"
"); out.println("自我介绍: "+customer.getDescription()+"
"); out.println("

"); } 5. deleteCustomer()方法 该方法调用Session的delete()方法,删除参数指定的Customer对象。代码如下: tx = session.beginTransaction(); session.delete(customer); tx.commit(); 运行session.delete()方法时,Hibernate根据Customer对象的OID,执行以下delete语句。 delete from CUSTOMERS where ID=1; 3.6运行helloapp应用 helloapp应用既能作为独立的Java程序来运行,还能作为Java Web应用来运行。当作为独立应用程序时,将直接运行BusinessService类的main()方法; 当作为Java Web应用时,需要在Java Web服务器上才能运行,本书采用Tomcat服务器。 3.6.1创建用于运行本书范例的系统环境 1. 安装软件 运行本书的例子,需要安装以下软件。 (1) JDK 8或者以上版本: Hibernate 5要求安装JDK 8或者以上的版本。如果安装JDK 7或者以下版本,可能会导致程序无法正常编译或运行。 (2) ANT: 它是发布、编译和运行Java应用的工具软件。 (3) Hibernate: 它是本书介绍的ORM工具软件。 (4) MySQL: 它是本书范例使用的关系数据库。 (5) Tomcat: 它是本章范例使用的Java Web服务器,其他章节的范例都不需要用到它。 附录E列出了这些软件的下载方式。 这些软件的安装都很简单,其中JDK的安装软件是可运行程序,只需要直接运行即可。MySQL 8以前的一些低版本安装软件是可运行程序,而MySQL 8的安装软件是需要先解压到本地,再手动配置后才能启动MySQL服务器。 MySQL 8的安装配置和启动过程如下: (1) 把MySQL 8的安装软件解压到本地,假定解压后MySQL的根目录为C:\mysql。 (2) 在MySQL的根目录下创建一个my.ini文件,它是MySQL的配置文件。my.ini文件的内容如下: [mysqld] # 设置3306端口 port=3306 # 设置MySQL的安装目录 basedir=C:\mysql # 设置MySQL数据库的数据的存放目录 datadir=C:\mysql\Data # 允许最大连接数 max_connections=200 # 允许连接失败的次数 max_connect_errors=10 # 服务端使用的字符集默认为utf8mb4 character-set-server=utf8mb4 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB # 默认使用mysql_native_password插件认证 #mysql_native_password default_authentication_plugin=mysql_native_password [mysql] # 设置MySQL客户端默认字符集 default-character-set=utf8mb4 [client] # 设置MySQL客户端连接服务端时默认使用的端口 port=3306 default-character-set=utf8mb4 如果读者的MySQL的根目录不是位于C:\mysql,那么需要对my.ini文件中的basedir和datadir属性做相应的修改。 (3) 在Windows操作系统的Path系统环境变量中添加C:\mysql\bin目录,参见图37。这一设置便于在DOS命令行中,不管当前位于哪个目录,都可以直接运行C:\mysql\bin目录下的可执行程序,如mysqld.exe管理服务程序和mysql.exe客户程序。 图37在Path系统环境变量中添加C:\mysql\bin目录 (4) 以管理员的身份打开DOS命令行窗口。假定Windows安装在C盘下,在文件资源管理器中,转到C:\Windows\System32目录下,右击cmd.exe程序,在下拉菜单中选择“以管理员身份运行”选项,参见图38。之所以要以管理员身份运行cmd.exe程序,是因为只有操作系统的管理员才具有权限去创建和启动MySQL服务。 图38以管理员身份运行cmd.exe程序,打开DOS命令行窗口 (5) 在DOS命令行中运行如下命令,创建并注册MySQL服务。 mysqld --install 该命令会创建一个服务名为mysql的服务,并且会在操作系统中注册该服务。如果要删除该服务,可以运行mysqld remove命令。mysqld命令对应C:\mysql\bin目录下的mysqld.exe程序。 (6) 在DOS命令行中运行如下命令,对MySQL服务进行初始化。 mysqld --initialize-insecure 该命令会参照C:\mysql\my.ini中的datadir属性,在C:\mysql目录下创建Data目录,以后MySQL服务器会把所有数据库的数据都放在此目录下。这个命令还会创建一个root超级用户,口令为空。 (7) 在DOS命令行中运行如下命令,启动mysql服务。 net start mysql 该命令将启动MySQL服务,即启动MySQL服务器。如果要终止MySQL服务,可以运行net stop mysql命令。图39展示了在DOS命令行中运行上述步骤中命令的过程。 图39初始化、创建并启动MySQL服务 (8) 如图310所示,在DOS命令行运行mysql u root p命令,以root用户的身份登录到MySQL的mysql.exe客户程序。root用户的初始口令为空,因此当系统提示输入口令时,直接按回车键即可。接下来在MySQL客户程序中执行如下修改root用户口令的SQL命令。 ALTER USER root@localhost IDENTIFIED BY '1234'; 以上命令把root用户的口令改为1234,本书所有范例程序连接MySQL服务器时会用root用户来连接,并且口令为1234。 图310在MySQL的客户程序中修改root用户的口令 Tomcat、ANT和Hibernate的安装软件是压缩软件包,只需要把压缩文件解压到本地硬盘即可。 2. 设置环境变量 安装好以上软件后需要在操作系统中设置以下系统环境变量。 (1) JAVA_HOME: JDK的安装目录。 (2) ANT_HOME: ANT的安装目录。 (3) CATALINA_HOME: Tomcat的安装目录。 (4) Path: 把%JAVA_HOME%/bin目录添加到Path变量中,以便在当前路径下,从DOS命令行直接运行JDK的javac和java命令; 把%ANT_HOME%/bin目录添加到Path变量中,以便在当前路径下,从DOS命令行直接运行ANT。 其中,JAVA_HOME和ANT_HOME环境变量是必须设置的,而CATALINA_HOME和Path环境变量不是必须设置的。 在Windows操作系统中创建JAVA_HOME等环境变量的步骤如下。 (1) 在Windows操作系统中,单击“开始”按钮,选择“控制面板”→“系统和安全”→“系统”→“高级系统设置”选项,打开“系统属性”对话框,单击“环境变量”按钮,接下来就可以创建JAVA_HOME系统环境变量了。单击“新建”按钮,具体设置参见图311。设置完成后,单击“确定”按钮。 图311设置JAVA_HOME环境变量 (2) 接下来可以重复以上步骤创建ANT_HOME等其他环境变量。 3. 运行程序 环境变量创建好以后,就可以启动各个服务器,然后运行本章范例程序。 1) 启动Tomcat服务器 运行\bin\startup.bat,就会启动Tomcat服务器,然后通过浏览器访问http://localhost:8080/,如果Tomcat启动成功,就可看到Tomcat的主页。 2) 启动MySQL服务器 在DOS命令行运行net start mysql,就会启动MySQL服务器。 3) 运行MySQL的客户程序 (1) MySQL安装目录下的bin\mysql.exe程序是MySQL的客户程序。在DOS下转到MySQL安装目录的bin目录下,输入如下命令。 C:\mysql\bin> mysql -u root -p 图312MySQL的客户程序 提示输入root用户的口令,此处输入口令“1234”。 Enter password: **** 然后就会进入图312所示的命令行客户程序。 (2) 创建数据库SAMPLEDB,SQL命令如下: create database SAMPLEDB; (3) 进入SAMPLEDB数据库,SQL命令如下: use SAMPLEDB; (4) 在SAMPLEDB数据库中创建CUSTOMERS表,SQL命令如下: create table CUSTOMERS ( ID bigint not null primary key, NAME varchar(15) not null, EMAIL varchar(128) not null, PASSWORD varchar(8) not null, PHONE int , ADDRESS varchar(255), SEX char(1) , IS_MARRIED bit, DESCRIPTION text, IMAGE blob, BIRTHDAY date, REGISTERED_TIME timestamp ); (5) 退出MySQL客户程序,输入命令“exit”。 在本书配套源代码包的sourcecode/chapter3/helloapp/schema目录下提供了创建SAMPLEDB数据库和CUSTOMERS表的SQL脚本文件,文件名为sampledb.sql。 如果不想在mysql.exe程序中手动输入SQL语句,也可以直接运行sampledb.sql,步骤如下: (1) 转到MySQL安装目录的bin目录下,输入如下命令。 C:\mysql\bin> mysql –u root –p 在build.xml文件中先定义了如下三个属性。 source.root属性指定Java源文件的路径,class.root属性指定Java类的路径,lib.dir属性指定所有JAR文件的路径。 接着定义了如下三个target(任务)。 (1) prepare target: 如果存在classes子目录,先将它删除。接着重新创建classes子目录。然后把src子目录下所有扩展名为.properties、.hbm.xml、.xml,以及.gif的文件复制到WEBINF/classes目录下。 (2) compile target: 编译src子目录下的所有Java源文件。编译生成的类文件存放在WEBINF/classes子目录下。 (3) run target: 运行BusinessService类。 图315build.xml文件中三个 target的依赖关系 这三个target的依赖关系参见图315。依赖关系指在执行当前target之前必须先执行所依赖的target。元素的depends属性指定所依赖的target。根据图315可以看出,当运行run target时,会依次执行prepare target、compile target和run target。 把helloapp应用作为独立应用程序运行的步骤如下。 (1) 启动MySQL服务器。 (2) 在MySQL服务器中安装helloapp应用的数据库。创建数据库的脚本为schema/sampledb.sql。在MySQL中运行该脚本,它负责创建SAMPLEDB数据库,然后在该数据库中创建CUSTOMERS表。 (3) 在DOS命令行下进入helloapp根目录,然后输入如下命令。 ant run 该命令将依次执行prepare target、compile target和run target,run target运行BusinessService类的main()方法,在DOS控制台输出很多信息,以下是部分内容。 Hibernate: select max(ID) from CUSTOMERS Hibernate: insert into CUSTOMERS (NAME, EMAIL, PASSWORD, PHONE, ADDRESS, SEX, IS_MARRIED, DESCRIPTION, IMAGE, BIRTHDAY, REGISTERED_TIME, ID) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) Hibernate: select customer0_.ID as ID0_, customer0_.NAME as NAME0_,customer0_.EMAIL as EMAIL0_, customer0_.PASSWORD as PASSWORD0_, customer0_.PHONE as PHONE0_, customer0_.ADDRESS … customer0_.REGISTERED_TIME as REGISTERED12_0_ from CUSTOMERS customer0_ order by customer0_.NAME asc ------以下是Tom的个人信息------ ID: 1 口令: 1234 E-mail: tom@yahoo.com 电话: 55556666 地址: Shanghai 性别: 男 婚姻状况: 未婚 生日: 1980-05-06 注册时间: 2018-12-06 10:54:06.0 自我介绍: I am very honest. Hibernate: select customer0_.ID as ID0_0_, customer0_.NAME as NAME0_0_, customer0_.EMAIL as EMAIL0_0_, customer0_.PASSWORD … as BIRTHDAY0_0_, customer0_.REGISTERED_TIME as REGISTERED12_0_0_ from CUSTOMERS customer0_ where customer0_.ID=? Hibernate: update CUSTOMERS set NAME=?, EMAIL=?, PASSWORD=?, PHONE=?, ADDRESS=?, SEX=?, IS_MARRIED=?, DESCRIPTION=?, IMAGE=?, BIRTHDAY=?, REGISTERED_TIME=? where ID=? Hibernate: select customer0_.ID as ID0_, customer0_.NAME as NAME0_,customer0_.EMAIL as EMAIL0_, customer0_.PASSWORD as PASSWORD0_, customer0_.PHONE as PHONE0_, customer0_.ADDRESS as ADDRESS0_, customer0_.SEX … from CUSTOMERS customer0_ order by customer0_.NAME asc ------以下是Tom的个人信息------ ID: 1 口令: 1234 E-mail: tom@yahoo.com 电话: 55556666 地址: Beijing 性别: 男 婚姻状况: 未婚 生日: 1980-05-06 注册时间: 2018-12-06 10:54:06.0 自我介绍: I am very honest. Hibernate: delete from CUSTOMERS where ID=? 由于hibernate.properties文件的show_sql属性为true,因此Hibernate把运行时执行的SQL语句输出到了控制台。当运行session.get(Customer.class,customer_id)方法时,Hibernate执行的SQL语句为: select customer0_.ID as ID0_0_, customer0_.NAME as NAME0_ 0_, customer0_.EMAIL as EMAIL0_0_, customer0_.PASSWORD as PASSWORD0_0_, customer0_.PHONE as PHONE0_0_, customer0_.ADDRESS as ADDRESS0_0_, … customer0_.REGISTERED_TIME as REGISTERED12_0_0_ from CUSTOMERS customer0_ where customer0_.ID=? 当运行session.save(customer)方法时,Hibernate执行的SQL语句为: select max(ID) from CUSTOMERS; insert into CUSTOMERS (NAME, EMAIL, PASSWORD, PHONE, ADDRESS, SEX, IS_MARRIED, DESCRIPTION, IMAGE, BIRTHDAY, REGISTERED_TIME, ID) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); 确切地说,以上SQL语句是在Hibernate初始化的时候创建的。在初始化时,Hibernate会根据对象关系映射文件中的映射信息,预定义一些带参数的SQL语句,?表示参数。这些预定义SQL语句存放在SessionFactory的缓存中。在运行时,Hibernate会把具体的参数值插入到SQL语句中。 图316显示了运行完run target之后的helloapp应用的主要目录结构,其中,classes子目录及它包含的内容是新建的,此外,在helloapp根目录下还新建了photo_copy.gif文件。 图316运行完run target之后的helloapp应用的主要目录结构 除了用ANT来运行BusinessService类外,也可以在DOS命令行下直接运行BusinessService类。假如当前路径为\helloapp,先设置classpath,把WEBINF\classes目录及WEBINF\lib目录下的所有JAR文件都添加到classpath中,命令如下: set classpath=WEB-INF\classes; WEB-INF\lib\hibernate-core-5.3.7.Final.jar; WEB-INF\lib\mysqldriver.jar;… 然后再执行如下命令。 java mypack.BusinessService 3.6.4把helloapp应用作为Java Web应用运行 在本应用中创建了一个名为DBServlet的Servlet类,它在doPost()方法中调用BusinessService类的test()方法,参见例程37; 在本应用中还创建了一个名为hello.jsp的JSP文件,它负责把请求转发给DBServlet,参见例程38。 例程37DBServlet.java package mypack; import javax.servlet.*; import javax.servlet.http.*; import java.io.*; import java.util.*; public class DBServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { try{ response.setContentType("text/html;charset=GBK"); new BusinessService() .test(this.getServletContext(),response.getWriter()); }catch(Exception e){e.printStackTrace();} } } 例程38hello.jsp hello.jsp 把helloapp应用作为Java Web应用运行的步骤如下。 (1) 启动MySQL服务器。 (2) 在MySQL服务器中安装helloapp应用的数据库。创建数据库的脚本为schema/ sampledb.sql。在MySQL中运行该脚本,它负责创建SAMPLEDB数据库,然后在该数据库中创建CUSTOMERS表。 (3) 把整个helloapp目录复制到\webapps目录下,然后启动Tomcat服务器。 (4) 在IE浏览器中访问http://localhost:8080/helloapp/DBServlet或http://localhost:8080/ helloapp/hello.jsp,都会看到如图317所示的输出结果。 图317hello.jsp的输出网页 3.7小结 本章通过helloapp应用,演示如何利用Hibernate来持久化Java对象。通过这个例子,读者应该掌握以下内容。 (1) 创建Hibernate的配置文件,在配置文件中提供连接特定数据库的信息。 (2) 创建对象关系映射文件,Hibernate根据该映射文件来生成SQL语句。本例的Customer类中包含各种Java类型的属性,如 long、char、boolean、java.lang.String、java.sql.Date、 java.sql.Timestamp和byte[]。 在CUSTOMERS表中包含各种SQL类型的字段,如 BIGINT、CHAR、BIT、VARCHAR、TEXT、DATE、TIMESTAMP和BLOB。 Hibernate提供了一组内置的映射类型作为连接Java类型和SQL类型的桥梁,常用的映射类型包括 long、character、boolean、string、text、date、timestamp和materialized_blob。 (3) 在应用程序中通过Hibernate API来访问数据库。在应用启动时先初始化Hibernate,创建一个SessionFactory实例; 接下来每次执行数据库事务时,先从SessionFactory中获得一个Session实例,再通过Session实例来保存、更新、删除、加载Java对象,以及通过Query实例来查询Java对象。 (4) 掌握存储二进制大数据及长文本数据的技巧。Customer类的image属性为byte[]类型,代表二进制大数据,用于存放GIF图片的二进制数据; description属性为java. lang.String类型,代表长文本数据,可以存放长度超过255的字符串。在CUSTOMERS表中,IMAGE字段为BLOB类型,DESCRIPTION字段为TEXT类型。Hibernate的materialized_blob映射类型用于映射字节数组,text映射类型用于映射长字符串。 BusinesssService类通过Hibernate API可以很方便地把GIF图片及长文本数据存储到数据库中,然后又把它们检索出来。 (5) 创建持久化类。持久化类不过是普通的JavaBean,Hibernate不强迫持久化类遵守特定的规范,并且持久化类无须引入任何Hibernate API,由此可以看出Hibernate没有渗透到域模型中。如果ORM软件渗透到域模型中,就意味着在持久化类中必须引入ORM软件的API,或者类的设计必须符合特定规范,这削弱了域模型的独立性和灵活性,如果日后要改用其他ORM软件,必须对模型做较大的改动。 当然,任何ORM软件都很难做到对持久化类完全没有限制,Hibernate也不例外,Hibernate要求持久化类必须提供不带参数的默认构造方法,此外,对于持久化类的集合类型的属性,Hibernate要求把属性定义为Java集合接口类型,14.7节将对此做详细解释。 (6) 在开发Java应用时,为了提高开发效率,缩短开发周期,常需要集成第三方提供的Java软件,除了本书重点介绍的ORM映射工具Hibernate外,还有其他如MVC框架Struts、日志工具Log4J、Web服务软件Apache AXIS等。在自己的应用中集成这些第三方软件时,大体步骤都很相似。 首先,把它们的JAR文件复制到classpath中; 然后创建它们的配置文件(XML格式的文件或者Java属性文件),这些配置文件通常也位于classpath中; 最后在程序中访问它们的API。 作为软件使用者,如果仅想快速掌握一个新的Java软件的使用方法,而不打算深入了解软件内在原理和结构,只需要了解它的API及配置文件的使用方法。如果想对软件的运用达到得心应手的地步,还应该了解软件本身的组成原理和结构。 3.8思考题 1. 为了让Hibernate顺利操纵持久化类的实例,持久化类的定义必须遵守哪些规范?(多选) (a) 持久化类必须提供默认的不带参数的构造方法 (b) 持久化类的属性必须有相应的get和set方法 (c) 持久化类的get和set方法必须为public类型 (d) 与持久化类的属性对应的get和set方法必须符合特定的命名规则 2. 在对象关系映射文件中,元素的type属性指定什么类型?(单选) (a) SQL类型(b) Hibernate映射类型 (c) Java类型 3. 在本章介绍的helloapp应用中,向数据库中保存一个Customer对象时,它的OID(即id属性)是由谁来赋值的?(单选) (a) 在Customer.hbm.xml文件中预先设定 (b) 由Hibernate赋值 (c) 由数据库赋值 (d) 由业务逻辑层的BusinessService类赋值 4. 关于Hibernate的配置文件,以下哪些说法正确?(多选) (a) 配置文件可以采用Java属性文件格式,也可以采用XML格式 (b) 当配置文件采用Java属性文件格式时,默认的文件名为hibernate.properties (c) 在Hibernate的配置文件中可指定持久化类与表的具体映射信息 (d) 在Hibernate的配置文件中可指定数据库的连接信息 5. 假定有一个持久化类的data属性为byte[]类型,在数据库中相应表的DATA字段为BLOB类型。在对象关系映射文件中,应该为这个持久化类的data属性指定什么Hibernate映射类型?(单选) (a) byte[](b) blob (c) text(d) materialized_blob 6. 在Hibernate API中,哪个接口提供了用于保存、更新、加载和删除实体域对象的方法?(单选) (a) SessionFactoryBuilder(b) SessionFactory (c) Session (d) Transaction 7. 在Hibernate API中,对于一个数据库存储源,通常只需要创建该接口的一个实例,哪个接口的实例属于重量级对象?(单选) (a) Configuration(b) SessionFactory (c) Session(d) Transaction 8. 关于Hibernate API,以下哪些说法正确?(多选) (a) SessionFactory提供了创建Session实例的方法 (b) SessionFactoryBuilder提供了创建Session实例的方法 (c) Session提供了创建Transaction实例的方法 (d) Session提供了创建Query实例的方法