内容


无需容器的对象关系映射

用 Hibernate 和 Spring 开发事务持久层

Comments

如果关心开发人员的最新热点,那么您可能听说过 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面编程)。不过,像许多开发人员一样,您可能不清楚在自己的开发工作中如何使用这些技术。在本文中,通过具体介绍使用 Hibernate 和 Spring 在企业应用程序中构建一个事务持久层,您会认识到这些技术。

Hibernate 是 Java 平台上的一种流行的、容易使用的开放源代码对象关系(OR)映射框架。Spring 是一个 AOP 框架和 IOC 容器。这两种技术一起提供了本文中介绍的开发工作的基础。将使用 Hibernate 把一些持久性对象映射到关系数据库中,用 Spring 使 Hibernate 更容易使用并提供声明性事务支持。由于为示例类编写测试代码时使用了 DbUnit,我还附带介绍了一点 TDD (测试驱动的开发)的内容。

注意,本文假定读者熟悉 Java 平台上的企业开发,包括 JDBC、OR 映射内容、J2EE 设计模式如 DAO,以及声明性事务支持,如 Enterprise JavaBean (EJB)技术所提供的事务支持。理解这里的讨论不需要成为这些技术的专家,也不需要熟悉 AOP、IOC 或者 TDD,因为在本文中对这三者都做了介绍。

我将首先介绍两种开发技术,然后分析例子。

Hibernate 简介

Hibernate 是 Java 平台上的一种全功能的、开放源代码 OR 映射框架。Hibernate 在许多方面类似于 EJB CMP CMR (容器管理的持久性 / 容器管理的关系)和 JDO(Java Data Objects)。与 JDO 不同,Hibernate 完全着眼于关系数据库的 OR 映射,并且包括比大多数商业产品更多的功能。大多数 EJB CMP CMR 解决方案使用代码生成实现持久性代码,而 JDO 使用字节码修饰。与之相反,Hibernate 使用反射和运行时字节码生成,使它对于最终用户几乎是透明的(以前 Hibernate 的实现只使用反射,它有助于调试,当前版本保留了这种选项)。

Hibernate 可以模拟继承(有几种方式)、关联(一对一或者一对多、containment 和 aggregation)和 composition。我将在本文中讨论每种关系类型的几个例子。

Hibernate 提供了一种称为 Hibernate Query Language (HQL) 的 查询语言,它类似于 JDO 的 JDOQL 和 EJB 的 EJB QL,尽管它更接近于前者。但是 Hibernate 没有就此止步:它还可以进行直接的 SQL 查询和 / 或使用 object criteria很容易地在运行时构成查询条件。在本文的例子中我将只使用 HQL。

与 EJB CMP CMR 不同,Hibernate 像 JDO 一样可以在 J2EE 容器内部或者外部工作,这可以让那些进行 TDD 和敏捷开发的人受益。

Spring 简介

AOP 专家 Nicholas Lesiecki 第一次向我解释 AOP 时,他说的我一个词也没理解,我觉得就像第一次考虑使用 IOC 容器的可能性时一样。每一种技术的概念基础本身就需要很好地消化,每一种技术所使用的各种各样的缩写让事情更糟了——特别是其中许多术语与我们已经使用的根本不一样了。

像许多技术一样,理解这两种技术的实际使用比学习理论更容易。经过自己对 AOP 和 IOC 容器实现(即 XWork、PicoContainer 和 Spring)的分析,我发现这些技术可以帮助我获得功能,而不会在多框架中添加基于代码的依赖性。它们都将成为我后面开发项目的一部分。

简单地说,AOP 让开发人员可以创建非行为性的关注点,称为横切关注点,并将它们插入到应用程序代码中。使用 AOP 后,公共服务(比如日志、持久性、事务等)就可以分解成方面并应用到域对象上,同时不会增加域对象的对象模型的复杂性。

IOC 允许创建一个可以构造对象的应用环境,然后向这些对象传递它们的协作对象。正如单词 倒置 所表明的,IOC 就像反过来的 JNDI。没有使用一堆抽象工厂、服务定位器、单元素(singleton)和直接构造(straight construction),每一个对象都是用其协作对象构造的。因此是由容器管理协作对象(collaborator)。

Spring 既是一个 AOP 框架、也是一个 IOC 容器。我记得 Grady Booch 说过,对象最好的地方是可以替换它们,而 Spring 最好的地方是它有助于您替换它们。有了 Spring,只要用 JavaBean 属性和配置文件加入依赖性(协作对象)。然后可以很容易地在需要时替换具有类似接口的协作对象。

Spring 为 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是将要用 AOP 为示例应用程序声明式地添加事务支持,与使用 EJB 技术时的方式基本相同。要了解 IOC 容器、AOP 和 Spring 的更多内容,请参阅 参考资料

具体到业务

在本文的其余部分,所有的讨论都将基于一个实际的例子。起点是一个企业应用程序,要为它实现一个事务持久层。持久层是一个对象关系数据库,它包括像 UserUser GroupRolesContactInfo 这些熟悉的抽象。

在深入到数据库的要素——查询和事务管理——之前,需要建立它的基础:对象关系映射。我将用 Hibernate 设置它,并只使用一点 Spring。

用 Hibernate 进行 OR 映射

Hibernate 使用 XML ( *.hbm.xml) 文件将 Java 类映射到表,将 JavaBean 属性映射到数据库表。幸运的是,有一组 XDoclet标签支持 Hibernate 开发,这使得创建所需要的 *.hbm.xml文件更容易了。清单 1 中的代码将一个 Java 类映射到数据库表。关于 XDoclet 标签的更多内容,请参阅 参考资料

清单 1. 将 Java 类映射到 DB 表

[User.java]
 /** 
 * 
        @hibernate.class table="TBL_USER"
 * .. 
 * .. 
 * ... 
 */ 
 public class User { 
	 private Long id = new Long(-1); 
	 private String email; 
	 private String password; 
	
	 . 
	 . 
	 . 
	 /** 
	 * @return 
	 * 
        @hibernate.id column="PK_USER_ID" 
	 * 		 unsaved-value="-1" 
	 *              generator-class="native"
	 */ 
	 public Long getId() { 
		 return id; 
	 } 
	 ... 
	 /** 
	 * 
        @hibernate.property column="VC_EMAIL" 
	 * 			 type="string" 
	 * 			 update="false"
	 * 			 insert="true"
	 * 			 unique="true"
	 * 			 not-null="true"
	 * 			 length="82"
	 * @return 
	 */ 
	 public String getEmail() { 
		 return email; 
	 } 
	 /** 
	 * 
        @hibernate.property column="VC_PASSWORD" 
	 * 			 type="string" 
	 * 			 update="false"
	 * 			 insert="true"
	 * 			 unique="true"
	 * 			 not-null="true"
	 * 			 length="20"
	 * @return 
	 */ 
	 public String getPassword() { 
		 return password; 
	 } 
	 ... 
	 ... 
	 ... 
 }

可以看到, @hibernate.class table="TBL_USER"标签将 User 映射到 TBL_USER表。 @hibernate.property column="VC_PASSWORD" 将 JavaBean 属性 password映射到 VC_PASSWORD列。 @hibernate.id column="PK_USER_ID" 标签声明 id属性是主键,它将使用本机( generator-class="native")数据库机制生成键(例如,Oracle sequences 和 SQL Server Identity 键)。Hibernate 可以指定 generator-class="native" 以外的、其他可以想象的得到主键获得策略,不过我更愿意使用 native。 type length属性用于从 Hibernate *.hbm.xml OR 映射文件生成表。这些 final 属性是可选的,因为使用的可能不是 green-field 数据库。在这个例子中,已经有数据库了,所以不需要额外的属性。( green-field 应用程序是一个新的应用程序, green-field 数据是新应用程序的一个新数据库。不会经常开发一个全新的应用程序,不过偶尔有一两次也不错)。

看过了表如何映射到类以及列如何映射到 JavaBean 属性,该使用 Hibernate 在 OR 数据库中设置一些关系了。

设置对象关系

在本节中,我将只触及 Hibernate 提供的设置对象间关系的选项的一小部分。首先设置像 UserUser GroupRolesContactInfo这些类之间的关系。其中一些关系如图 1 所示,这是数据库的验证对象模型。

图 1. 关系的图示
关系的图示
关系的图示

如您所见,在上述抽象中存在各种各样的关系。 UserContactInfo有一对一关系。 ContactInfo 的生命周期与 User相同(用数据库的术语,UML 中的组成 aka级联删除)。如果删除 User,则相应的 ContactInfo也会删除。在 Users 与 Roles 之间存在多对多关系(即与独立生命周期相关联)。在 Groups 与 Users 之间存在一对多关系,因为组有许多用户。用户可以存在于组外,即是 aggregation 而不是 composition (用数据库的说法,在 Groups 和 Users之间没有级联删除关系)。此外, UserEmployee有子类关系,就是说, Employee的类型为 User。表 1 显示了如何用 XDoclet标签创建一些不同类型的对象关系。

表 1. 用 XDoclet 创建对象关系

关系Java/XDocletSQL DDL(由 Hibernate Schema Export 生成的 MySQL)
组包含用户

一对多

Aggregation

双向
(Group<-->Users)

[Group.java]
/**
*
* @return
*
* @hibernate.bag name="users"
* cascade="save-update"
* lazy="true"
* inverse="true"
*
* @hibernate.collection-key
* column="FK_GROUP_ID"
*
* @hibernate.collection-one-to-many
* class="net.sf.hibernateExamples.User"
*/
public List getUsers() {
return users;
}

[User.java]
/**
* @hibernate.many-to-one
* column="FK_GROUP_ID"
* class="net.sf.hibernateExamples.Group"
*/
public Group getGroup() {
return group;
}


create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)


create table TBL_GROUP (
PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT,
VC_DESCRIPTION VARCHAR(255),
VC_NAME VARCHAR(40) unique,
primary key (PK_GROUP_ID)
)

alter table TBL_USER add index (FK_GROUP_ID),
add constraint FK_111 foreign key (FK_GROUP_ID)
references TBL_GROUP (PK_GROUP_ID)

用户有联系信息

一对一

Composition
单向
(User-->ContactInfo)

[User.java]
/**
* @return
*
* @hibernate.one-to-one cascade="all"
*
*/
public ContactInfo getContactInfo() {
return contactInfo;
}

[ContactInfo.java]
(Nothing to see here. Unidirectional!)
create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)

create table TBL_CONTACT_INFO (
PK_CONTACT_INFO_ID BIGINT not null,
...
...
...
primary key (PK_CONTACT_INFO_ID)
)

用户与角色关联

多对多

Association

单向
(Users-->Roles)

[User.java]
/**
* @return
* @hibernate.bag
* table="TBL_JOIN_USER_ROLE"
* cascade="all"
* inverse="true"
*
* @hibernate.collection-key
* column="FK_USER_ID"
*
* @hibernate.collection-many-to-many
* class="net.sf.hibernateExamples.Role"
* column="FK_ROLE_ID"
*
*/
public List getRoles() {
return roles;
}

[Role.java]
Nothing to see here. Unidirectional!
create table TBL_ROLE (
PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT,
VC_DESCRIPTION VARCHAR(200),
VC_NAME VARCHAR(20),
primary key (PK_ROLE_ID)
)

create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)

create table TBL_JOIN_USER_ROLE (
FK_USER_ID BIGINT not null,
FK_ROLE_ID BIGINT not null
)

雇员是用户

Inheritance

用户

雇员

[User.java]
/**
* @hibernate.class table="TBL_USER"
* discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
...
...
...
*/
public class User {

[Employee.java]
/**
* @hibernate.subclass discriminator-value = "1"
*/
public class Employee extends User{

create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)

要了解在 Hibernate 中设置对象关系的更多内容,请参阅 参考资料

Hibernate 中的查询

Hibernate 有三种类型的查询:

  • Criteria, object composition
  • SQL
  • HQL

在下面的例子中将只使用 HQL。本节还要使用 Spring,用它的 AOP-driven HibernateTemplate 简化 Hibernate 会话的处理。在本节将开发一个 DAO(Data Access Object)。要了解更多关于 DAO 的内容,请参阅 参考资料

清单 2 展示了两个方法:一个使用 HQL 查询的组查询,另一个是后面接一个操作的组查询。注意在第二个方法中,Spring HibernateTemplate 是如何简化会话管理的。

清单 2. 使用查询

import net.sf.hibernate.HibernateException; 
import net.sf.hibernate.Session; 
import net.sf.hibernate.Query; 
import org.springframework.orm.hibernate.HibernateCallback; 
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
/** 
* @author Richard Hightower 
* ArcMind Inc. http://www.arc-mind.com 
*/ 
public class UserDAO extends HibernateDaoSupport{ 
  . 
  . 
  . 
  /** 
   * Demonstrates looking up a group with a HQL query 
   * @param email 
   * @return 
   */    
   public Group findGroupByName(String name) { 
     return (Group) getHibernateTemplate()
         .find("from Group g where g.name=?",name).get(0); 
   } 
   

  /** 
   * Demonstrates looking up a group and forcing it to 
   * populate users (relationship was lazy) 
   * @param email 
   * @return 
   */    
   public Group findPopulatedGroupByName(final String name) { 
     HibernateCallback callback = new HibernateCallback(){ 
      public Object doInHibernate(Session session) 
             throws HibernateException, SQLException { 
         Group group =null; 
         String query = "from Group g where g.name=?"; 
         Query queryObject = getHibernateTemplate()
	     .createQuery(session, query); 
         queryObject.setParameter(0, name); 
         group = (Group) queryObject.list().get(0); 
         group.getUsers().size();//force load 
         return group; 
         } 
        
      }; 
     
      return (Group) getHibernateTemplate().execute(callback); 
   } 
  . 
  . 
  . 
}

您可能会注意到第二个方法比第一个方法复杂得多,因为它强迫加载 users集合。因为 Group->Users 之间的关系设置为 lazy initialize(即表 2 中 lazy="true"),组对象需要一个活跃的会话以查询用户。在定义 GroupUsers 之间关系时设置这个属性为 lazy="false",则不需要第二个方法。在这种情况下,可能使用第一种方法 ( findGroupByName) 列出组,用第二种方法( findPopulatedGroupByName)查看组细节。

Spring IOC 和 Hibernate

使用 Spring 时,在 J2EE 容器内和容器外工作一样容易。比如在最近的项目中,我在 Eclipse 中,使用 HSQL 和本地数据库对使用 Hibernate 事务管理器的 Hypersonic SQL 数据库进行持久性单元测试。然后,在部署到 J2EE 服务器时,将持久层转换为使用 J2EE 数据源(通过 JNDI)、JTA 事务和使用 FireBird (一个开放源代码版本的 Interbase)。这是用 Spring 作为 IOC 容器完成的。

从清单 3 中可以看出,Spring 允许加入依赖性。注意清单中应用程序上下文文件是如何配置 dataSource的。 dataSource传递给 sessionFactorysessionFactory 传递给 UserDAO

清单 3. Spring IOC 和 Hibernate

 <beans> 
    <!-- Datasource that works in any application server 
       You could easily use J2EE data source instead if this were 
       running inside of a J2EE container. 
    --> 
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> 
       <property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
       <property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
       <property name="username"><value>root</value></property> 
       <property name="password"><value></value></property> 
    </bean> 
    <!-- Hibernate SessionFactory --> 
    <bean id="sessionFactory" 
       class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> 
       <property name="dataSource"><ref local="dataSource"/></property> 
      
       <!-- Must references all OR mapping files. --> 
       <property name="mappingResources"> 
          <list> 
              <value>net/sf/hibernateExamples/User.hbm.xml</value> 
              <value>net/sf/hibernateExamples/Group.hbm.xml</value> 
              <value>net/sf/hibernateExamples/Role.hbm.xml</value>               
              <value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value> 
          </list> 
       </property> 
      
       <!-- Set the type of database; changing this one property will 
          port this to Oracle, MS SQL etc. --> 
       <property name="hibernateProperties"> 
          <props> 
           <prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
          </props> 
       </property> 
    </bean> 
   
    <!-- Pass the session factory to our UserDAO --> 
    <bean id="userDAO" class="net.sf.hibernateExamples.UserDAO"> 
       <property name="sessionFactory"><ref local="sessionFactory"/></property>
    </bean> 
   
 </beans>

设置了 UserDAO后,下一步就是定义并使用更多的查询以展示可以完成的操作。Hibernate 可以用预定义查询将查询存储到源代码之外,如清单 4 所示。

清单 4. 预定义查询

[User.java]
/** 
* @author Richard Hightower 
* ArcMind Inc. http://www.arc-mind.com 
* @hibernate.class table="TBL_USER" discriminator-value="2" 
* @hibernate.discriminator column="USER_TYPE"
* 
* @hibernate.query name="AllUsers" query="from User user order by user.email asc"
* 
* @hibernate.query name="OverheadStaff" 
* query="from Employee employee join employee.group g 
* where g.name not in ('ENGINEERING','IT')"
* 
* @hibernate.query name="CriticalStaff" 
* query="from Employee employee join employee.group g 
* where g.name in ('ENGINEERING','IT')"
* 
* @hibernate.query name="GetUsersInAGroup" 
* query="select user from Group g join g.users user"
* 
* @hibernate.query name="GetUsersNotInAGroup" 
* query="select user from User user where user.group is null"
* 
* @hibernate.query name="UsersBySalaryGreaterThan" 
* query="from User user inner join user.contactInfo info where info.salary > ?1"
* 
* @hibernate.query name="UsersBySalaryBetween" 
* query="from User user join user.contactInfo info 
* where info.salary between ?1 AND ?2"
* 
* @hibernate.query name="UsersByLastNameLike" 
* query="from User user join user.contactInfo info where info.lastName like ?1"
* 
* @hibernate.query name="GetEmailsOfUsers" 
* query="select user.email from Group g join g.users as user where g.name = ?1"
* 
*/ 
public class User {
 . 
 . 
 .

上述代码定义了几个预定义查询。 预定义查询 是存储在 *.hbm.xml文件中的查询。在清单 5 中,可以看到如何执行预定义查询。

清单 5. 使用预定义查询

[UserDAO.java]
	 /** 
	 * Demonstrates a query that returns a String. 
	 */                
	 public String[] getUserEmailsInGroup(String groupName){ 
		 List emailList = 
		 getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers"); 
		 return (String []) 
			 emailList.toArray(new String[emailList.size()]); 
	 } 
	 /** 
	 * Demonstrates a query that returns a list of Users 
	 * 
	 * @return A list of emails of all of the users in the authentication system. 
	 * 
	 */                
	 public List getUsers(){ 
		 return getHibernateTemplate().findByNamedQuery("AllUsers"); 
	 } 
      /** 
	 * Demonstrates passing a single argument to a query. 
	 * 
	 * @return A list of UserValue objects. 
	 * 
	 */                    
	 public List getUsersBySalary(float salary){ 
	   return getHibernateTemplate() 
	       .findByNamedQuery("UsersBySalaryGreaterThan", 
	            new Float(salary)); 
	 } 
	 /** 
	 * Demonstrates passing multiple arguments to a query 
	 * 
	 * @return A list of UserValue objects. 
	 * 
	 */                    
	 public List getUsersBySalaryRange(float start, float stop){ 
                return getHibernateTemplate() 
		 .findByNamedQuery("UsersBySalaryBetween", 
			 new Object[] {new Float(start), new Float(stop)}); 
	 }

查询进行时,可以在持久层中加上最后一层:使用 Spring 的事务管理。

用 Spring 管理事务

Spring 可以声明式地管理事务。例如, UserDAO.addUser方法当前不是在单个事务中执行的。因此,组中的每一个用户都插入到自己的事务中,如清单 6 所示。

清单 6. 添加一组用户

[UserDAO.java]
 /** 
 * @param group 
 */ 
 public void addGroup(Group group) { 
	 getHibernateTemplate().save(group); 
	
 } 
        [UserDAOTest.java]
 public void testAddGroupOfUsers(){ 
	 Group group = new Group(); 
	
	 for (int index=0; index < 10; index++){ 
		 User user = new User(); 
		 user.setEmail("rick"+index+"@foobar.com" ); 
		 user.setPassword("foobar"); 
		 group.addUser(user); 	
	 } 
	
	 group.setName("testGroup"); 
	
	 userDAO.addGroup(group); 
	 assertNotNull(group.getId()); 
	
	 Group group2 = userDAO.findPopulatedGroupByName("testGroup"); 
	
	 assertEquals("testGroup",group2.getName()); 
	 assertEquals(10, group2.getUsers().size()); 
	 String email = ((User)group2.getUsers().get(0)).getEmail(); 
	 assertEquals("rick0@foobar.com", email); 
	
 }

不建议使用上述解决方案,因为每一个 User都要在自己的事务中插入到数据库中。如果出现问题,那么只能添加部分用户。如果希望保留 ACID 属性(即保证所有都发生或者所有都不发生),可以通过程序进行事务管理,但是它很快就会变得一团糟了。相反,应使用 Spring 的 AOP 来支持声明式的事务,如清单 7 所示。

清单 7. 声明式管理事务

[applicationContext.xml]
 <!-- Pass the session factory to our UserDAO --> 
   <bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl"> 
	 <property name="sessionFactory"><ref local="sessionFactory"/></property> 
   </bean> 
	
   <bean id="transactionManager" 
   	 class="org.springframework.orm.hibernate.HibernateTransactionManager"> 
      <property name="sessionFactory"><ref bean="sessionFactory"/></property> 
   </bean> 
   <bean id="userDAO"
     class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> 
	 <property name="transactionManager"><ref local="transactionManager"/></property> 
	 <property name="target"><ref local="userDAOTarget"/></property> 
	 <property name="transactionAttributes"> 
		 <props> 
			 <prop key="addGroup">PROPAGATION_REQUIRED</prop> 
		 </props> 
	 </property> 
    </bean>

注意在准备清单 7 的代码时,我重新改写了 UserDAO并提取了其接口。这个接口现在是 UserDAO,它的实现类是 UserDAOImpl。这样清单 7 中的事务代码就使用了带有事务属性 (PROPAGATION_REQUIRED) UserDAO.addGroup()方法。现在只要底层数据库支持,就可以在一个事务中添加所有用户。

结束语

在本文中,介绍了如何使用 Hibernate 和 Spring 实现一个事务持久层。Hibernate 是一种先进的 OR 映射工具,而 Spring 是一个 AOP 框架和 IOC 容器。这两种技术的综合使用,使得开发人员可以编写媲美数据库厂商的代码,它可以在 J2EE 容器中运行,也可以单独运行。使用了 DbUnit (JUnit 的扩展)构建和测试本文中例子的所有代码,虽然这不是讨论的重点。

要了解有关 AOP、IOC 容器和测试驱动开发的更多内容,请参阅 参考资料


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=53250
ArticleTitle=无需容器的对象关系映射
publish-date=04012004