10.4.4 处理用户竞价

当用户进行竞价时,系统不仅需要添加竞价记录,还需要向竞价用户发送通知邮件,通知该用户已经竞价成功。
下面是处理用户竞价的业务逻辑方法代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 增加新的竞价,并对竞价用户发邮件通知
* @param itemId 物品id;
* @param bid 竞价
* @param userId 竞价用户的ID
* @return 返回新增竞价记录的ID
*/
public int addBid(int itemId, Bid bid, Integer userId)
throws AuctionException
{
try
{
AuctionUser au = userDao.get(AuctionUser.class, userId);
Item item = itemDao.get(Item.class, itemId);
if (bid.getBidPrice() <= item.getMaxPrice())
{
return -1;
}
item.setMaxPrice(bid.getBidPrice());
itemDao.save(item);
// 设置Bid对象的属性
bid.setBidItem(item);
bid.setBidUser(au);
bid.setBidDate(new Date());
// 持久化Bid对象
bidDao.save(bid);
// 准备发送邮件
SimpleMailMessage msg = new SimpleMailMessage(this.message);
msg.setTo(au.getEmail());
msg.setText("Dear " + au.getUsername() + ", 谢谢你参与竞价,你的竞价的物品的是: "
+ item.getItemName());
mailSender.send(msg);
return bid.getId();
} catch (Exception ex)
{
ex.printStackTrace();
log.debug(ex.getMessage());
throw new AuctionException("处理用户竞价出现异常,请重试");
}
}

从该方法代码不难看出,方法的前半部分是添加竞价记录,并依赖于系统的DAO组件提供实现;而后半部分发送通知邮件,发送通知邮件依赖于Spring的邮件抽象层。
发送通知邮件在实际项目中是非常重要的事情,每当用户竞价成功后,系统可以发送通知邮件让用户确认,只有用户确认竞价后该竞价才会真正生效。
当使用Spring邮件抽象层时,需要使用Spring的如下两个工具类。

  • JavaMailSender:用于发送邮件。
  • SimpleMailMessage:表示邮件本身。

本系统并未发送MimeMessage信息,所以使用SimpleMailMessage已经足够。本系统把邮件的MailSenderSimleMailMessage都配置在Spring容器中,因此必须在业务逻辑组件的代码中增加这两个组件的setter 方法。下面是在业务逻辑组件中依赖注入邮件MailSenderSimleMailMessagesetter方法代码。

1
2
3
4
5
6
7
8
9
// 为业务逻辑组件注入两个邮件发送Bean的setter方法
public void setMailSender(MailSender mailSender)
{
this.mailSender = mailSender;
}
public void setMessage(SimpleMailMessage message)
{
this.message = message;
}

当然,还应该在Spring容器中配置如下两个关于邮件发送的Bean

  • mailSender:配置Spring发送邮件的MailSender Bean
  • mailMessage:配置所发送的邮件本身。

下面是配置这两个邮件发送Bean的配置片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 定义JavaMailSenderImpl,它用于发送邮件 指定发送邮件的SMTP服务器地址, 指定登录邮箱的用户名、密码 -->
<bean
id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl"
p:host="smtp.163.com"
p:username="spring_test"
p:password="123abc">
<property name="javaMailProperties">
<props>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.timeout">25000</prop>
</props>
</property>
</bean>

<!-- 定义SimpleMailMessage Bean,它代表了一份邮件 指定发件人地址,指定邮件标题 -->
<bean
id="mailMessage"
class="org.springframework.mail.SimpleMailMessage"
p:from="spring_test@163.com"
p:subject="竞价通知" />

从以上代码可以看出,本系统使用163的电子邮件系统发送通知邮件,因此要求运行系统时服务器可以正常访问163的电子邮件系统,否则系统将抛出异常。当然,如果是一个企业级的应用,则应该改为使用本企业的邮件系统。
因为163的邮件系统要求先登录邮箱,然后才可发送邮件,故配置MailSender时,指定了mail.smtp.auth属性的值为true,并提供登录邮箱所必需的用户名和密码。
提示:Spring的邮件抽象层完全支持发送带附件、HTML格式的邮件,如果需要发送这种”复杂”类型的邮件,则应该使用MimeMessage信息,而不是SimpleMailMessage信息。SimpleMailMessage信息只支持普通文本内容。如果读者希望了解关于Spring邮件抽象层的更多知识,则可以参考Spring官方手册。

10.4.2 依赖注入DAO组件

实现业务逻辑组件,就是为AuctionService 接口提供一个实现类,该实现类必须依赖于DAO组件,但这种依赖是接口层次的依赖,而不是类层次上的依赖。
因为业务逻辑组件必须依赖于5个DAO组件,而这5个DAO组件都依赖SpringIoC容器的注入,因此在AuctionServiceImpl中必须提供如下5个setter方法,这5个setter方法正是依赖注入5个DAO组件所必需的方法。
下面是在业务逻辑组件中依赖注入5个DAO组件的setter方法片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 以下是该业务逻辑组件所依赖的DAO组件
private AuctionUserDao userDao;
private BidDao bidDao;
private ItemDao itemDao;
private KindDao kindDao;
private StateDao stateDao;

// 为业务逻辑组件依赖注入DAO组件所需的setter方法
public void setUserDao(AuctionUserDao userDao)
{
this.userDao = userDao;
}
public void setBidDao(BidDao bidDao)
{
this.bidDao = bidDao;
}
public void setItemDao(ItemDao itemDao)
{
this.itemDao = itemDao;
}
public void setKindDao(KindDao kindDao)
{
this.kindDao = kindDao;
}
public void setStateDao(StateDao stateDao)
{
this.stateDao = stateDao;
}

一旦为该业务逻辑组件提供了这5个setter方法,当把业务逻辑组件部署在Spring容器中,并配置所依赖的DAO组件后,Spring容器就可以把所需DAO组件注入业务逻辑组件中。

10.4.3 业务逻辑组件的异常处理

Spring的异常处理哲学简化了异常的处理:所有的数据库访问异常都被包装成了Runtime异常,在DAO组件中无须显式捕捉异常,所有的异常都被推迟到业务逻辑组件中捕捉。
在业务逻辑组件中捕捉系统抛出的原始异常,这种原始异常不应该被客户端看到,甚至不应该在服务器端暴露出来。为了达到这个目的,可以使用log4j 的日志功能:系统使用log4j记录业务逻辑方法中原始的异常信息,然后再抛出自定义异常。下面是本系统中自定义异常类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.crazyit.auction.exception;

public class AuctionException extends Exception
{
private static final long serialVersionUID = 1L;
// 定义一个无参数的构造器
public AuctionException()
{
}
// 定义一个带message参数的构造参数
public AuctionException(String message)
{
super(message);
}
}

系统业务逻辑方法采用如下方式来处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
try
{
//完成业务逻辑
}
//捕捉异常
catch (Exception e)
{
//通过日志记录异常
log.debug(e.getMessage());
//抛出新异常
throw new AuctionException("底层业务异常,请重试");
}

业务层抛出的业务异常会传播到控制器层,而控制器则负责将该异常封装成JSON数据响应传给浏览器端的JS脚本。

10.4 业务逻辑层实现

本系统的规模不大,只涉及5个DAO组件,分别用于对5个持久化对象进行增、删、改、查操作。本系统使用一个业务逻辑对象即可封装5个DAO组件。

10.4.1 设计业务逻辑组件

业务逻辑组件同样采用面向接口编程的原则,让系统中的控制器不是依赖于业务逻辑组件的实现类,而是依赖于业务逻辑组件的接口类,从而降低了系统重构的代价。
该业务逻辑组件接口的类图如图10.9所示。

AuctionService 接口里定义了大量业务方法,这些业务方法的实现依赖于DAO 组件。为了达到高层次的解耦,这里推荐使用接口分离的规则,将业务逻辑组件分成接口和相应的实现类两个部分
AuctionServiceImpl 实现类实现了AuctionService 接口,并实现了该接口中的所有方法。此外,AuctionServiceImpl实现类中比接口中多了如下5个依赖注入的方法。

方法 描述
setUserDao(AuctionUserDao dao) 为业务逻辑组件依赖注入AuctionUserDao的方法。
setBidDao(BidDao dao) 为业务逻辑组件依赖注入BidDao的方法。
setItemDao(ItemDao dao) 为业务逻辑组件依赖注入ItemDao的方法。
setKindDao(KindDao dao) 为业务逻辑组件依赖注入KindDao的方法。
setStateDao(StateDao dao) 为业务逻辑组件依赖注入StateDao的方法。
该接口的作用同样是定义一种规范,规定该业务逻辑组件应该实现的方法。下面是该接口的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package org.crazyit.auction.service;

import java.util.List;

import org.crazyit.auction.business.BidBean;
import org.crazyit.auction.business.ItemBean;
import org.crazyit.auction.business.KindBean;
import org.crazyit.auction.domain.Bid;
import org.crazyit.auction.domain.Item;
import org.crazyit.auction.domain.Kind;
import org.crazyit.auction.exception.AuctionException;


public interface AuctionService
{
/**
* 根据赢取者查询物品
* @param winerId 赢取者的ID
* @return 赢取者获得的全部物品
*/
List<ItemBean> getItemByWiner(Integer winerId)
throws AuctionException;

/**
* 查询流拍的全部物品
* @return 全部流拍物品
*/
List<ItemBean> getFailItems()throws AuctionException;

/**
* 根据用户名,密码验证登录是否成功
* @param username 登录的用户名
* @param pass 登录的密码
* @return 登录成功返回用户ID,否则返回-1
*/
int validLogin(String username , String pass)
throws AuctionException;

/**
* 查询用户的全部出价
* @param userId 竞价用户的ID
* @return 用户的全部出价
*/
List<BidBean> getBidByUser(Integer userId)
throws AuctionException;

/**
* 根据用户查找目前仍在拍卖中的全部物品
* @param userId 所属者的ID
* @return 属于当前用户的、处于拍卖中的全部物品。
*/
List<ItemBean> getItemsByOwner(Integer userId)
throws AuctionException;

/**
* 查询全部种类
* @return 系统中全部全部种类
*/
List<KindBean> getAllKind() throws AuctionException;

/**
* 添加物品
* @param item 新增的物品
* @param avail 有效天数
* @param kindId 物品种类ID
* @param userId 添加者的ID
* @return 新增物品的主键
*/
int addItem(Item item, int avail , int kindId , Integer userId)
throws AuctionException;

/**
* 添加种类
* @param kind 新增的种类
* @return 新增种类的主键
*/
int addKind(Kind kind) throws AuctionException;

/**
* 根据产品分类,获取处于拍卖中的全部物品
* @param kindId 种类id;
* @return 该类的全部产品
*/
List<ItemBean> getItemsByKind(int kindId) throws AuctionException;

/**
* 根据种类id获取种类名
* @param kindId 种类id;
* @return 该种类的名称
*/
String getKind(int kindId) throws AuctionException;

/**
* 根据物品id,获取物品
* @param itemId 物品id;
* @return 指定id对应的物品
*/
ItemBean getItem(int itemId) throws AuctionException;

/**
* 增加新的竞价,并对竞价用户发邮件通知
* @param itemId 物品id;
* @param bid 竞价
* @param userId 竞价用户的ID
* @return 返回新增竞价记录的ID
*/
int addBid(int itemId , Bid bid ,Integer userId)
throws AuctionException;

/**
* 根据时间来修改物品的赢取者
*/
void updateWiner()throws AuctionException;
}

在该接口里定义了大量的业务逻辑方法,实际上,这些业务逻辑方法通常对应一次客户请求。这些业务逻辑方法当然不会被直接暴露出来,这些业务逻辑方法只是供前端MVC 控制器调用,而MVC 控制器并不直接与业务逻辑组件的实现类耦合,而仅仅只是依赖于系统的业务逻辑组件接口。当需要重构系统业务逻辑组件时,只要该组件的接口不变,则系统的功能不变,系统的控制器层也无须改变,从而把系统的业务逻辑层的改变阻止在该层以内,避免了向上扩散。

10.3.3 部署DAO组件

对于所有继承BaseDaoHibernate4DAO实现类必须为其提供SessionFactory的引用,SpringIoC容器可以将SessionFactory注入到DAO组件中。
下面是在本系统中部署DAO组件的配置代码,本系统以单独配置文件来部署DAO组件,这样可以将不同组件放在不同配置文件中分开管理,从而避免Spring 配置文件过于庞大。下面是部署DAO组件的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?xml version="1.0" encoding="utf-8"?>
<!-- Spring配置文件的根元素,并指定Schema信息 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<!-- 注意修改这里的数据库密码 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://localhost:3306/auction"
p:user="root"
p:password="root"
p:maxPoolSize="200"
p:minPoolSize="2"
p:initialPoolSize="2"
p:maxIdleTime="2000"
destroy-method="close"/>
<!-- 定义Hibernate的SessionFactory
并为它注入数据源,设置Hibernate配置属性等。-->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
p:dataSource-ref="dataSource">
<property name="annotatedClasses">
<list>
<value>org.crazyit.auction.domain.AuctionUser</value>
<value>org.crazyit.auction.domain.Bid</value>
<value>org.crazyit.auction.domain.Item</value>
<value>org.crazyit.auction.domain.Kind</value>
<value>org.crazyit.auction.domain.State</value>
</list>
</property>
<!-- 定义Hibernate的SessionFactory的属性 -->
<property name="hibernateProperties">
<props>
<!-- 指定数据库方言 -->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQL5InnoDBDialect</prop>
<!-- 显示Hibernate持久化操作所生成的SQL -->
<prop key="hibernate.show_sql">true</prop>
<!-- 将SQL脚本进行格式化后再输出 -->
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>

<!-- 配置daoTemplate,作为所有DAO组件的模板,
为DAO组件注入SessionFactory引用 -->
<!-- 1号代码 -->
<bean id="daoTemplate"
abstract="true"
p:sessionFactory-ref="sessionFactory" />
<!-- 配置stateDao组件 -->
<bean id="stateDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.StateDaoHibernate" />
<!-- 配置kindDao组件 -->
<bean id="kindDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.KindDaoHibernate" />
<!-- 配置auctionDao组件 -->
<bean id="auctionUserDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.AuctionUserDaoHibernate" />
<!-- 配置bidDao组件 -->
<bean id="bidDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.BidDaoHibernate" />
<!-- 配置itemDao组件 -->
<bean id="itemDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.ItemDaoHibernate" />
</beans>

这里在粗体字代码中配置了一个daoTemplate抽象Bean,它将作为系统中其他DAO组件的模板,这样就可将daoTemplate的配置属性传递给其他DAO Bean
为了让其他DAO组件获得daoTemplate 的配置属性,必须将其他DAO 组件配置成daoTemplate的子Bean,子Bean通过parent属性指定其父Bean,正如在上面的配置文件中,在每个DAO Bean的粗体字代码中都指定了parent="daoTemplate",这表明这些DAO组件将以daoTemplate作为模板。

10.3.2 实现DAO组件

为了实现DAO模式,系统至少需要具有如下三个部分:

  • DAO接口
  • DAO接口的实现类
  • DAO工厂

对于采用Spring框架的应用而言,无须额外提供DAO工厂,因为Spring容器本身就是DAO工厂。此外,开发者需要提供DAO接口和DAO实现类。每个DAO组件都应该提供标准的新增加载更新删除等方法,此外还需提供数量不等的查询方法
如下是AuctionUserDao接口的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.crazyit.auction.dao;

import org.crazyit.auction.domain.AuctionUser;
import org.crazyit.common.dao.BaseDao;

public interface AuctionUserDao extends BaseDao<AuctionUser> {
/**
* 根据用户名,密码查找用户
*
* @param username 查询所需的用户名
* @param pass 查询所需的密码
* @return 指定用户名、密码对应的用户
*/
AuctionUser findByNameAndPass(String username, String pass);

/**
* 根据物品id、出价查询用户
*
* @param itemId 物品id;
* @param price 出价的价格
* @return 指定物品、指定竞价对应的用户
*/
AuctionUser findByItemAndPrice(Integer itemId, Double price);
}

从表面上看,在该AuctionUserDao 接口中只定义了2 个方法,但由于该接口继承了BaseDao<AuctionUser>,因此该接口其实也包含了增加、修改,根据主键加载、删除等通用的DAO 方法。在该接口中额外定义的findUserByNameAndPass()方法,可根据用户名、密码查询AuctionUser,由于本系统在映射AuctionUserusername属性时指定了unique="true",因此根据usernamepass查询时不会返回List,最多只会返回一个AuctionUser实例。
定义了AuctionUserDao接口之后,下面就可以为该接口提供实现类了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package org.crazyit.auction.dao.impl;

import java.util.List;
import org.crazyit.auction.dao.AuctionUserDao;
import org.crazyit.auction.domain.AuctionUser;
import org.crazyit.common.dao.impl.BaseDaoHibernate4;

public class AuctionUserDaoHibernate extends BaseDaoHibernate4<AuctionUser>
implements
AuctionUserDao
{

/**
* 根据用户名,密码查找用户
*
* @param username
* 查询所需的用户名
* @param pass
* 查询所需的密码
* @return 指定用户名、密码对应的用户
*/
public AuctionUser findByNameAndPass(String username, String pass)
{
// 执行HQL查询
// 1号方法
List<AuctionUser> ul = (List<AuctionUser>) find(
"from AuctionUser au where au.username=?0 and au.userpass=?1",
username, pass);
// 返回查询得到的第一个AuctionUser对象
if (ul != null && ul.size() == 1)
{
return (AuctionUser) ul.get(0);
}
return null;
}

/**
* 根据物品id、出价查询用户
*
* @param itemId
* 物品id;
* @param price
* 竞价的价格
* @return 指定物品、指定竞价对应的用户
*/
public AuctionUser findByItemAndPrice(Integer itemId, Double price)
{
// 执行HQL查询
List<AuctionUser> userList = (List<AuctionUser>) find(
"select user from AuctionUser user inner join user.bids bid"
+ " where bid.bidItem.id=?0 and bid.bidPrice=?1",
itemId, price);
// 返回查询得到的第一个Bid对象关联的AuctionUser对象
if (userList != null && userList.size() == 1)
{
return userList.get(0);
}
return null;
}
}

AuctionUserDaoHibernate类中的方法稍稍复杂一些,该方法用于根据用户名、密码查找用户。
ItemDaoAuctionUserDao稍微复杂一点,下面是ItemDao接口的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.crazyit.auction.dao;

import java.util.List;
import org.crazyit.auction.domain.Item;
import org.crazyit.common.dao.BaseDao;

public interface ItemDao extends BaseDao<Item>
{
/**
* 根据产品分类,获取当前拍卖的全部商品
*
* @param kindId
* 种类id;
* @return 该类的全部产品
*/
List<Item> findByKind(Integer kindId);

/**
* 根据所有者查找处于拍卖中的物品
*
* @param useId
* 所有者Id;
* @return 指定用户处于拍卖中的全部物品
*/
List<Item> findByOwner(Integer userId);

/**
* 根据赢取者查找物品
*
* @param userId
* 赢取者Id;
* @return 指定用户赢取的全部物品
*/
List<Item> findByWiner(Integer userId);

/**
* 根据物品状态查找物品
*
* @param stateId
* 状态Id;
* @return 该状态下的全部物品
*/
List<Item> findByState(Integer stateId);
}

同样,让ItemDaoHibernate继承BaseDaoHibernate4就能用简单的代码来实现该DAO组件的全部方法了,下面是ItemDaoHibernate类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package org.crazyit.auction.dao.impl;

import java.util.List;
import org.crazyit.auction.dao.ItemDao;
import org.crazyit.auction.domain.Item;
import org.crazyit.common.dao.impl.BaseDaoHibernate4;

public class ItemDaoHibernate extends BaseDaoHibernate4<Item> implements ItemDao
{
/**
* 根据产品分类,获取当前拍卖的全部商品
*
* @param kindId
* 种类id;
* @return 该类的全部产品
*/
public List<Item> findByKind(Integer kindId)
{
return find("from Item as i where i.kind.id=?0 and i.itemState.id=1",
kindId);
}

/**
* 根据所有者查找处于拍卖中的物品
*
* @param useId
* 所有者Id;
* @return 指定用户处于拍卖中的全部物品
*/
public List<Item> findByOwner(Integer userId)
{
return (List<Item>) find(
"from Item as i where i.owner.id=?0 and i.itemState.id=1",
userId);
}

/**
* 根据赢取者查找物品
*
* @param userId
* 赢取者Id;
* @return 指定用户赢取的全部物品
*/
public List<Item> findByWiner(Integer userId)
{
return find(
"from Item as i where i.winer.id =?0" + " and i.itemState.id=2",
userId);
}

/**
* 根据物品状态查找物品
*
* @param stateId
* 状态Id;
* @return 该状态下的全部物品
*/
public List<Item> findByState(Integer stateId)
{
return find("from Item as i where i.itemState.id = ?0", stateId);
}
}

AuctionUserDaoHiberante类相似,ItemDaoHibernate类也非常简单,几乎所有方法都只要一行代码即可实现。
借助于Spring+Hibernate的简化结构,开发者可以非常简便地实现所有DAO组件。系统中的KindDaoBidDaoStateDao类都非常简单,故这里不再给出它们的实现。

10.3 DAO层实现

本系统的后台完全采用轻量级Java EE应用的架构,系统持久层访问使用DAO组件完成。DAO组件抽象出底层的数据访问,业务逻辑组件无须理会数据库访问的细节,只需专注于业务逻辑的实现即可。DAO 将数据访问集中在独立的一层,所有的数据访问都由DAO对象完成,从而使系统具有更好的可维护性。
DAO 组件还有助于提升系统的可移植性。独立的DAO层使得系统能在不同的数据库之间轻易切换,底层的数据库实现对于业务逻辑组件完全透明,移植数据库仅仅影响DAO层,切换不同的数据库不会影响业务逻辑组件,因此提高了系统的可移植性。
前面介绍的BaseDao接口、BaseDaoHibernate4接口可以极大地简化DAO组件的开发,因此本系统的DAO组件同样会继承BaseDaoBaseDaoHibernate4接口。

10.3.1 DAO的基础配置

对于BaseDaoHibernate4需要容器注入一个SessionFactory引用,该类也为依赖注入提供了setSessionFactory()方法。BaseDaoHibernate4 基类一旦获得SessionFactory 的引用,就可以完成大部分通用的增、删、改、查操作。
Spring为整合Hibernate提供了LocalSessionFactoryBean 类,这样可以将HibernateSessionFactory纳入其IoC容器内。在使用LocalSessionFactoryBean配置SessionFactory之前,必须为其提供对应的数据源。SessionFactory的相关配置片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?xml version="1.0" encoding="utf-8"?>
<!-- Spring配置文件的根元素,并指定Schema信息 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 定义数据源Bean,使用C3P0数据源实现 -->
<!-- 注意修改这里的数据库密码 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://localhost:3306/auction"
p:user="root"
p:password="root"
p:maxPoolSize="200"
p:minPoolSize="2"
p:initialPoolSize="2"
p:maxIdleTime="2000"
destroy-method="close"/>
<!-- 定义Hibernate的SessionFactory
并为它注入数据源,设置Hibernate配置属性等。-->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
p:dataSource-ref="dataSource">
<property name="annotatedClasses">
<list>
<value>org.crazyit.auction.domain.AuctionUser</value>
<value>org.crazyit.auction.domain.Bid</value>
<value>org.crazyit.auction.domain.Item</value>
<value>org.crazyit.auction.domain.Kind</value>
<value>org.crazyit.auction.domain.State</value>
</list>
</property>
<!-- 定义Hibernate的SessionFactory的属性 -->
<property name="hibernateProperties">
<props>
<!-- 指定数据库方言 -->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQL5InnoDBDialect</prop>
<!-- 显示Hibernate持久化操作所生成的SQL -->
<prop key="hibernate.show_sql">true</prop>
<!-- 将SQL脚本进行格式化后再输出 -->
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>

<!-- 配置daoTemplate,作为所有DAO组件的模板,
为DAO组件注入SessionFactory引用 -->
<bean id="daoTemplate"
abstract="true"
p:sessionFactory-ref="sessionFactory" />
<!-- 配置stateDao组件 -->
<bean id="stateDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.StateDaoHibernate" />
<!-- 配置kindDao组件 -->
<bean id="kindDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.KindDaoHibernate" />
<!-- 配置auctionDao组件 -->
<bean id="auctionUserDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.AuctionUserDaoHibernate" />
<!-- 配置bidDao组件 -->
<bean id="bidDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.BidDaoHibernate" />
<!-- 配置itemDao组件 -->
<bean id="itemDao"
parent="daoTemplate"
class="org.crazyit.auction.dao.impl.ItemDaoHibernate" />
</beans>

可以将Hibernate属性直接放在LocalSessionFactoryBean内配置,也可以放在hibernate.cfg.xml文件中配置。

10.2.2 实现Domain Object

Domain Object之间存在的关联关系,由图10.8可以清楚地看到,其中AuctionUserItem两个持久化类之间的关系尤为复杂,它们之间存在两种1对N的关系,分别是所属赢取两种关系。
下面是AuctionUser类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package org.crazyit.auction.domain;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="auction_user")
public class AuctionUser
{
// 标识属性
@Id
@Column(name="user_id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
// 用户名成员变量
private String username;
// 密码成员变量
private String userpass;
// 电子邮件成员变量
private String email;

// 根据属主关联的物品实体
@OneToMany(targetEntity=Item.class ,
mappedBy="owner")
private Set<Item> itemsByOwner = new HashSet<>();
// 根据赢取者关联的物品实体
@OneToMany(targetEntity=Item.class ,
mappedBy="winer")
private Set<Item> itemsByWiner = new HashSet<>();

// 该用户所参与的全部竞价
@OneToMany(targetEntity=Bid.class ,
mappedBy="bidUser")
private Set<Bid> bids = new HashSet<>();

// 无参数的构造器
public AuctionUser()
{
}
// 初始化全部成员变量的构造器
public AuctionUser(Integer id , String username
, String userpass , String email)
{
this.id = id;
this.username = username;
this.userpass = userpass;
this.email = email;
}

// id的setter和getter方法
public void setId(Integer id)
{
this.id = id;
}
public Integer getId()
{
return this.id;
}

// username的setter和getter方法
public void setUsername(String username)
{
this.username = username;
}
public String getUsername()
{
return this.username;
}

// userpass的setter和getter方法
public void setUserpass(String userpass)
{
this.userpass = userpass;
}
public String getUserpass()
{
return this.userpass;
}

// email的setter和getter方法
public void setEmail(String email)
{
this.email = email;
}
public String getEmail()
{
return this.email;
}

// itemsByOwner的setter和getter方法
public void setItemsByOwner(Set<Item> itemsByOwner)
{
this.itemsByOwner = itemsByOwner;
}
public Set<Item> getItemsByOwner()
{
return this.itemsByOwner;
}

// itemsByWiner的setter和getter方法
public void setItemsByWiner(Set<Item> itemsByWiner)
{
this.itemsByWiner = itemsByWiner;
}
public Set<Item> getItemsByWiner()
{
return this.itemsByWiner;
}

// bids的setter和getter方法
public void setBids(Set<Bid> bids)
{
this.bids = bids;
}
public Set<Bid> getBids()
{
return this.bids;
}
}

AuctionUser 类里保留了两个Set<Item>属性:itemsByOwneritemsByWiner,其中itemsByOwner用于访问属于该用户的全部拍卖物品,而itemsByWiner用于访问该用户赢取的全部拍卖物品,这两个属性的关联实体都是Item。此外,AuctionUser类里还有一个bids属性,该属性用于访问该用户参与的全部竞价记录。
本示例没有使用映射文件来管理Hibernate实体,而是采用Annotation来管理持久化实体。在该程序中,在定义AuctionUser类时使用了@Entity修饰,该Annotation可将该类映射成持久化实体。除此之外,AuctionUser实体和3个关联实体存在1对N的关系,因此需要在AuctionUser类的itemsByOwneritemsByWinerbids属性上使用@OneToMany修饰。
本系统将把所有1对N关联都映射成双向关联,因此我们为3个@OneToMany元素都指定了mappedBy属性,这表明AuctionUser实体(1的一端)不控制关联关系。根据Hibernate的建议:对于双向1对N关联,不要让1的一端控制关系,而应该让N的一端控制关联关系,这样可以保证更好的性能。
Item类除了和AuctionUser之间存在两种N对1的关联关系外,还和Bid之间存在1对N的关联关系,和Kind之间存在N对1的关联关系,和State之间存在N对1的关联关系。下面是Item类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package org.crazyit.auction.domain;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="item")
public class Item
{
// 标识属性
@Id
@Column(name="item_id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
// 物品Remark
@Column(name="item_remark")
private String itemRemark;
// 物品名称
@Column(name="item_name")
private String itemName;
// 物品描述
@Column(name="item_desc")
private String itemDesc;
// 物品添加时间
private Date addtime;
// 物品结束拍卖时间
private Date endtime;
// 物品的起拍价
@Column(name="init_price")
private double initPrice;
// 物品的最高价
@Column(name="max_price")
private double maxPrice;

// 该物品的所有者
@ManyToOne(targetEntity=AuctionUser.class)
@JoinColumn(name="owner_id", nullable=false)
private AuctionUser owner;
// 该物品的赢取者
@ManyToOne(targetEntity=AuctionUser.class)
@JoinColumn(name="winer_id", nullable=true)
private AuctionUser winer;

// 该物品所属的种类
@ManyToOne(targetEntity=Kind.class)
@JoinColumn(name="kind_id", nullable=false)
private Kind kind;
// 该物品所处的状态
@ManyToOne(targetEntity=State.class)
@JoinColumn(name="state_id", nullable=false)
private State itemState;

// 该物品对应的全部竞价记录
@OneToMany(targetEntity=Bid.class ,
mappedBy="bidItem")
private Set<Bid> bids = new HashSet<Bid>();

// 无参数的构造器
public Item()
{
}
// 初始化全部成员变量的构造器
public Item(Integer id , String itemRemark , String itemName ,
String itemDesc , Date addtime , Date endtime ,
double initPrice , double maxPrice , AuctionUser owner)
{
this.id = id;
this.itemRemark = itemRemark;
this.itemName = itemName;
this.itemDesc = itemDesc;
this.addtime = addtime;
this.endtime = endtime;
this.initPrice = initPrice;
this.maxPrice = maxPrice;
this.owner = owner;
}

// id的setter和getter方法
public void setId(Integer id)
{
this.id = id;
}
public Integer getId()
{
return this.id;
}

// itemRemark的setter和getter方法
public void setItemRemark(String itemRemark)
{
this.itemRemark = itemRemark;
}
public String getItemRemark()
{
return this.itemRemark;
}

// itemName的setter和getter方法
public void setItemName(String itemName)
{
this.itemName = itemName;
}
public String getItemName()
{
return this.itemName;
}

// itemDesc的setter和getter方法
public void setItemDesc(String itemDesc)
{
this.itemDesc = itemDesc;
}
public String getItemDesc()
{
return this.itemDesc;
}

// addtime的setter和getter方法
public void setAddtime(Date addtime)
{
this.addtime = addtime;
}
public Date getAddtime()
{
return this.addtime;
}

// endtime的setter和getter方法
public void setEndtime(Date endtime)
{
this.endtime = endtime;
}
public Date getEndtime()
{
return this.endtime;
}

// initPrice的setter和getter方法
public void setInitPrice(double initPrice)
{
this.initPrice = initPrice;
}
public double getInitPrice()
{
return this.initPrice;
}

// maxPrice的setter和getter方法
public void setMaxPrice(double maxPrice)
{
this.maxPrice = maxPrice;
}
public double getMaxPrice()
{
return this.maxPrice;
}

// owner的setter和getter方法
public void setOwner(AuctionUser owner)
{
this.owner = owner;
}
public AuctionUser getOwner()
{
return this.owner;
}

// kind的setter和getter方法
public void setKind(Kind kind)
{
this.kind = kind;
}
public Kind getKind()
{
return this.kind;
}

// winer的setter和getter方法
public void setWiner(AuctionUser winer)
{
this.winer = winer;
}
public AuctionUser getWiner()
{
return this.winer;
}

// itemState的setter和getter方法
public void setItemState(State itemState)
{
this.itemState = itemState;
}
public State getItemState()
{
return this.itemState;
}

// bids的setter和getter方法
public void setBids(Set<Bid> bids)
{
this.bids = bids;
}
public Set<Bid> getBids()
{
return this.bids;
}
}

这里的5行粗体字代码为Item类映射了它关联的4个关联实体,其中

  • owner引用该物品的所属者,
  • winer 引用该物品的赢取者,这两个属性都引用到AuctionUser 实体。

Item 类和AuctionUserKindState之间存在N对1的关联,因此需要使用@ManyToOne来修饰这些属性;
Item类和Bid之间存在1对N的关联,因此需要使用@OneToMany来修饰该属性。

@ManyToOnefetch属性默认FetchType.EAGER,该属性将会取消延迟加载。对于N对1的关联映射,N的一端的实体只有一个关联实体,如果取消了延迟加载,那么系统加载N的一端的实体时,1的一端的实体同时也会被加载—这不会有太大的问题,因为只是额外多加载了一条记录。
@ManyToOne中取消延迟加载不会有太大的性能下降。但对于@OneToMany等修饰集合的Annotation则尽量不要取消延迟加载。一旦为修饰集合属性的Annotation取消延迟加载,则意味着加载主表记录时,引用该记录的所有从表记录也会同时被加载,这会产生一个问题:我们无法预料有多少条从表记录引用该主表记录,同时加载这些从表记录可能引起巨大的性能下降。

本系统中还有BidStateKind三个实体,但这三个实体都比ItemAuctionUser实体更简单。理解了ItemAuctionUser的映射关系,自然也就掌握了BidStateKind实体的实现方法,故此处不再赘述。

5.2 Hibernate入门

Hibernate 的用法非常简单,只要在Java项目中引入Hibernate 框架,就能以面向对象的方式操作关系数据库

5.2.2 Hibernate的数据库操作

在所有的ORM 框架中有一个非常重要的媒介:PO对象(持久化对象)。通过持久化对象可对数据执行增、删、改的操作,也就是以面向对象的方式操作数据库。
应用程序只需创建、修改、删除持久化对象即可,Hibernate 则负责把这种操作转换为对指定数据表的操作。
Hibernate完全采用普通的Java对象作为持久化对象使用,看下面的POJO(普通的、传统的Java 对象类。

5.1.4 Hibernate概述

  • Hibernate 是一个面向Java环境的对象/关系数据库映射工具,用于把面向对象模型表示的对象映射到基于SQL 的关系模型的数据结构中。
  • Hibernate 能消除那些针对特定数据库厂商的SQL代码,并且把结果集从表格式的形式转换成值对象的形式
  • Hibernate不仅仅管理Java类到数据库表的映射(包括Java数据类型到SQL数据类型的映射),还提供数据查询获取数据的方法,可以大幅度减少开发时人工使用SQLJDBC处理数据的时间。

Hibernate 能在众多的ORM 框架中脱颖而出,因为Hibernate 与其他ORM 框架对比具有如下优势。

  1. 开源和免费的License ,方便需要时研究源代码,改写源代码,进行功能定制。
  2. 轻量级封装,避免引入过多复杂的问题,调试容易,减轻程序员负担。
  3. 有可扩展性, API 开放。 功能不够用的时候,自己编码进行扩展。
  4. 开发者活跃,产品有稳定的发展保障。