ORM in JaveEE——JPA

什么是ORM

  ORM,即Object Relation Mapping,对象关系映射。我们一般用数据库作数据的持久化存储,通过针对表,属性等进行各种sql的操作,来进行增删改查。但作为程序员,在面向对象思想大行其道的今天,我们更倾向于处理对象,而不是直接处理数据库里面的关系表。我们希望在程序的业务逻辑中,一切皆是对象,而不是表与表的关系,表之间的属性。于是一套从表到对象的映射技术,便应运而生。目前,很多框架都可以实现从关系型数据库到对象的映射,例如Hibernate,EclipseLink等。只需要编写一些配置文件,便可以轻松实现。

什么是JPA

  由于市面上有很多框架都可以实现ORM,我们使用不同的框架,就需要一套不一样的配置文件,一套不一样的代码。换言之,我们的工程与这些框架是紧耦合的。如果我们想更换这些框架,那么改动就非常大了。JPA就实现了一套统一的接口标准,让我们可以自由选择不同供应商的不同ORM框架,实现与这些第三方框架的解耦合。并且,JPA广泛使用注解,代码简单,开发十分方便。

JPA与RESTful API

  前面我们提到RESTful API使用JAX-RS实现的。RESTful API与JPA没有直接的关系,但我们知道通过RESTful API我们实际上是把对对象的增删改查映射到HTTP方法上。而JPA相当于是处理下一层的工作,把对数据库表的增删改查映射到对象上。这样,经过JAX-RS到JPA再到数据库这么三层的处理,我们便可以利用RESTful API来实现我们的业务逻辑。

BCE模型

BCE模型,即Boundary-Controll-Entity,是Web应用系统的一种十分常见的结构。Boundary,即系统之间交互的边界,一般指一些涉及对Web应用的数据的输入输出的类,例如RESTful API直接访问的JAX-RS的Resource类。Control,一般是完成一些复杂的计算与业务逻辑的处理,是从Entity到Boundary的中间层。Entity,就是数据库表直接ORM转化的实体对象类。下面就从BCE模型入手,按Entity,Control,Boundary的顺序,围绕实例介绍一下JPA标准以及其与JAX-RS的结合。

Entity

  先来看看JPA的Entity类是什么样的:

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
@Entity
@Table(name = "commentary", catalog = "weibo", schema = "public")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "Commentary.findAll", query = "SELECT c FROM Commentary c"),
@NamedQuery(name = "Commentary.findById", query = "SELECT c FROM Commentary c WHERE c.id = :id"),
@NamedQuery(name = "Commentary.findByWord", query = "SELECT c FROM Commentary c WHERE c.word = :word"),
@NamedQuery(name = "Commentary.findByCreateDate", query = "SELECT c FROM Commentary c WHERE c.createDate = :createDate"),
@NamedQuery(name = "Commentary.findByIsDeleted", query = "SELECT c FROM Commentary c WHERE c.isDeleted = :isDeleted")})
public class Commentary implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Long id;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 140)
@Column(name = "word")
private String word;
@Column(name = "create_date")
@Temporal(TemporalType.TIMESTAMP)
private Date createDate;
@Column(name = "is_deleted")
private Boolean isDeleted;
@JoinColumn(name = "reply_subscriber_id", referencedColumnName = "id")
@ManyToOne
private Subscriber replySubscriberId;
@JoinColumn(name = "subscriber_id", referencedColumnName = "id")
@ManyToOne
private Subscriber subscriberId;
@JoinColumn(name = "message_id", referencedColumnName = "id")
@ManyToOne
private Message messageId;
public Commentary() {
}
public Commentary(Long id) {
this.id = id;
}
public Commentary(Long id, String word) {
this.id = id;
this.word = word;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Boolean getIsDeleted() {
return isDeleted;
}
public void setIsDeleted(Boolean isDeleted) {
this.isDeleted = isDeleted;
}
public Subscriber getReplySubscriberId() {
return replySubscriberId;
}
public void setReplySubscriberId(Subscriber replySubscriberId) {
this.replySubscriberId = replySubscriberId;
}
public Subscriber getSubscriberId() {
return subscriberId;
}
public void setSubscriberId(Subscriber subscriberId) {
this.subscriberId = subscriberId;
}
public Message getMessageId() {
return messageId;
}
public void setMessageId(Message messageId) {
this.messageId = messageId;
}
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Commentary)) {
return false;
}
Commentary other = (Commentary) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
@Override
public String toString() {
return "sampleweb.message.entity.Commentary[ id=" + id + " ]";
}
}

  简单分析一下,@Entity标注说明该类是JPA的实体类。@Table标注说明了该类对应的数据库,schema以及表名,由此使Entity与数据库的表建立一一对应的关系。

  @Id标注说明了主键,@Column说明了表的列属性。@Basic(optional = false)与@NotNull都是表示该属性对应到数据库的列不能为空。@Size一般说明字符串类型的长度范围,对应数据库里的char或varchar等类型。@GeneratedValue(strategy = GenerationType.IDENTITY)说明主键的生成策略采用了数据库自身的自增长主键。

  @ManyToOne和@OneToMany说明了该实体对应的表与实体对应的表的外键引用关系,通过这些关系可以比较方便的实现Entity的连接。类似的还有@OneToOne和@ManyToMany。

  然后便是对每一个属性的getter和setter方法。Entity类必须含有一个空的构造方法。

  该类实现了Serializable接口,并且打上了@XmlRootElement标注,表明该实体可以被序列化,直接作为数据进行输入输出。这时候它既属于Entity也属于Boundary,没有经过数据的再封装。只有需要不同的Entity的数据组合,或者需要多次查询多个实体的数据,则需要在Boundary下再定义一个专门用于数据传输的POJO类。

  我们只要有一个大概的认识,了解一下实体是怎么与数据库的表映射在一起的。这里面广泛使用了注解,代码很少,配置简单。其实Entity类基本不用自己写的,IDE里只要连接了数据库,通常可以直接从数据库一键生成Entity类,根据数据库的结构自动地配置好所有标注与变量,我们只需把重点放在Boundary与Control部分即可。

  这里有一点需要注意一下,从数据库的表一键生成Entity类时,我们需要指定一个数据库的表,作为Entity的基础,并且会自动把与其相关联的有外键引用关系的表一起生成为其他Entity。但是,其外键的引用关系只能是同一个schema里的,如果是不同schema之间的外键引用,则无法做到ORM。反正我的试验下是这样的,如果有正确的方法实现不同schema之间的ORM,希望不吝赐教~

Control

  控制层是JPA的关键与重点,我们的绝大部分业务逻辑都集中在控制层内。下面我们从EJB,EntityManager以及Query API三个方面来介绍一下Control层。

EJB

  EJB,即EnterPrise JaveBean,是分离视图层与数据库访问的一层技术,是Control的核心,是数据库接口与业务逻辑的封装体。它有自己的生命周期管理机制,是一套跨application server的独立组件。它可以是有状态和无状态的。无状态的EJB说明其每次调用都是无记忆无状态的,是没有关联的。一般应尽量使用无状态的EJB,其能够具备最好的线程安全性与并发性。而且,我们绝大多数的应用场景用无状态的EJB便能满足,特别是基于http的RESTful API的实现。下面重点介绍一下无状态的EJB,即stateless EJB。

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
@Stateless
@RolesAllowed({"operator"})
public class MessageService {
@PersistenceContext(unitName = "com.hudoumiao_sampleweb_war_1.0-SNAPSHOTPU")
private EntityManager em;
public List<Object[]> findDangerousMessages(int page,int perPage){
String sql = "(select message1.word,message1.comment_count,message1.transmission_count,message1.id,message1.publish_date," +
"message2.word,message2.comment_count,message2.transmission_count,message2.id,message2.publish_date," +
"subscriber1.username,subscriber2.username,subscriber1.id,subscriber2.id,manager.dangerous_message.managed_date," +
"manager.action.action_name,account.auth_user.name from manager.dangerous_message left outer join " +
"message message1 on(manager.dangerous_message.message_id = message1.id) left outer join account.auth_user " +
"on(manager.dangerous_message.manager_id = account.auth_user.id) left outer join manager.action " +
"on(manager.dangerous_message.action_id = manager.action.id) left outer join message message2 on(message1.reference_id = message2.id)" +
"inner join subscriber subscriber1 on (message1.subscriber_id = subscriber1.id) left outer join subscriber subscriber2 on (message2.subscriber_id = subscriber2.id) " +
"where message1.is_deleted = false and message1.is_blocked = false and message1.is_removed = false)" +
"order by 2 desc,5 desc " +
"limit " + perPage + "offset " + (page - 1) * 10;
return em.createNativeQuery(sql).getResultList();
}
public long countDangerousMessage(){
String sql = "select count(manager.dangerous_message.id) from manager.dangerous_message left outer join message "
+ "on(manager.dangerous_message.message_id = message.id) where message.is_deleted = false and "
+ "message.is_blocked = false and message.is_removed = false";
return (Long)em.createNativeQuery(sql).getSingleResult();
}
public long countSolvedMessages(){
String sql = "select count(manager.dangerous_message.id) from manager.dangerous_message left outer join message "
+ "on(manager.dangerous_message.message_id = message.id) where message.is_deleted = false and "
+ "(message.is_blocked = true or message.is_removed = true)";
return (Long)em.createNativeQuery(sql).getSingleResult();
}
public List<Object[]> findSolvedMessages(int page,int perPage){
String sql = "(select message1.word,message1.comment_count,message1.transmission_count,message1.id,message1.publish_date," +
"message2.word,message2.comment_count,message2.transmission_count,message2.id,message2.publish_date," +
"subscriber1.username,subscriber2.username,subscriber1.id,subscriber2.id,message1.is_removed,message1.is_blocked,manager.dangerous_message.managed_date," +
"manager.action.action_name,account.auth_user.name from manager.dangerous_message left outer join " +
"message message1 on(manager.dangerous_message.message_id = message1.id) left outer join account.auth_user " +
"on(manager.dangerous_message.manager_id = account.auth_user.id) left outer join manager.action " +
"on(manager.dangerous_message.action_id = manager.action.id) left outer join message message2 on(message1.reference_id = message2.id)" +
"inner join subscriber subscriber1 on (message1.subscriber_id = subscriber1.id) left outer join subscriber subscriber2 on (message2.subscriber_id = subscriber2.id) " +
"where message1.is_removed = true and message1.is_deleted = false)" +
"union" +
"(select message1.word,message1.comment_count,message1.transmission_count,message1.id,message1.publish_date," +
"message2.word,message2.comment_count,message2.transmission_count,message2.id,message2.publish_date," +
"subscriber1.username,subscriber2.username,subscriber1.id,subscriber2.id,message1.is_removed,message1.is_blocked,manager.dangerous_message.managed_date," +
"manager.action.action_name,account.auth_user.name from manager.dangerous_message left outer join " +
"message message1 on(manager.dangerous_message.message_id = message1.id) left outer join account.auth_user " +
"on(manager.dangerous_message.manager_id = account.auth_user.id) left outer join manager.action " +
"on(manager.dangerous_message.action_id = manager.action.id) left outer join message message2 on(message1.reference_id = message2.id)" +
"inner join subscriber subscriber1 on (message1.subscriber_id = subscriber1.id) left outer join subscriber subscriber2 on (message2.subscriber_id = subscriber2.id) " +
"where message1.is_blocked = true and message1.is_deleted = false)" +
"order by 2 desc,5 desc " +
"limit " + perPage + "offset " + (page - 1) * 10;
return em.createNativeQuery(sql).getResultList();
}
public List<SumItem> sumMessage(){
String sql = "select count(to_char(publish_date,'yyyy-mm-dd')),to_char(publish_date,'yyyy-mm-dd') from message " +
"where publish_date > current_date - interval '9 day' " +
"group by to_char(publish_date,'yyyy-mm-dd') order by to_char(publish_date,'yyyy-mm-dd')";
List<Object[]> results = em.createNativeQuery(sql).getResultList();
List<SumItem> messageSum = new ArrayList();
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, -9);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String start = sdf.format(cal.getTime());
for(int i = 0;i < 10;i++){
SumItem sumItem = new SumItem();
sumItem.setCount(0);
sumItem.setDate(sdf.format(cal.getTime()));
messageSum.add(sumItem);
cal.add(Calendar.DAY_OF_MONTH, 1);
}
if(!results.isEmpty()){
for(Object[] result : results){
String end = (String)result[1];
try{
int interval = (int)((sdf.parse(end).getTime() - sdf.parse(start).getTime()) / (1000 * 60 * 60 * 24));
messageSum.get(interval).setCount(((Long)result[0]).intValue());
}catch(ParseException e){
e.printStackTrace();
}
}
}
return messageSum;
}
public List<SumItem> sumComment(){
String sql = "select count(to_char(create_date,'yyyy-mm-dd')),to_char(create_date,'yyyy-mm-dd') from commentary " +
"where create_date > current_date - interval '9 day' " +
"group by to_char(create_date,'yyyy-mm-dd') order by to_char(create_date,'yyyy-mm-dd')";
List<Object[]> results = em.createNativeQuery(sql).getResultList();
List<SumItem> commentSum = new ArrayList();
Calendar cal = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
cal.add(Calendar.DAY_OF_MONTH, -9);
String start = sdf.format(cal.getTime());
for(int i = 0;i < 10;i++){
SumItem sumItem = new SumItem();
sumItem.setCount(0);
sumItem.setDate(sdf.format(cal.getTime()));
commentSum.add(sumItem);
cal.add(Calendar.DAY_OF_MONTH, 1);
}
if(!results.isEmpty()){
for(Object[] result : results){
String end = (String)result[1];
try{
int interval = (int)((sdf.parse(end).getTime() - sdf.parse(start).getTime()) / (1000 * 60 * 60 * 24));
commentSum.get(interval).setCount(((Long)result[0]).intValue());
}catch(ParseException e){
e.printStackTrace();
}
}
}
return commentSum;
}
}
1
2
3
4
5
6
7
8
9
10
11
@Stateless
@RolesAllowed({"operator"})
@Path("message")
@Tracked
public class MessageFacadeREST extends AbstractFacade<Message> {
@PersistenceContext(unitName = "com.hudoumiao_sampleweb_war_1.0-SNAPSHOTPU")
private EntityManager em;
@EJB
MessageService messageService;
@EJB
CommentService commentService;

  第一段代码是EJB的定义类,@stateless标注说明是无状态的EJB。其必须含有无参的构造函数,这里用默认的无参构造函数。我们可以先忽略类里各方法的实现细节,但不难看出,每个方法都是对数据库的操作,以及获取数据后的一些处理。这就是我们的数据库访问与业务逻辑,全部封装在EJB里。

  第二段代码是注入EJB到类变量里。这里的MessageFacadeRest是JAX-RS的Resource类,我们需要借助EJB来处理业务逻辑,然后返回处理过后的数据到Resource类的相应方法内以供RESTful API访问。@EJB就表示EJB实体对象。

  我们每调用一次EJB的实体对象,都会经历一次EJB的生命周期,下面简单阐述一下其生命周期的流程。我们所用的application server container,例如GlassFish,Tomcat等,都会维护一个EJB pool,类似数据库连接池或者线程池,都是预先存放好一定数量的EJB实体对象,需要调用的时候从中抽取,调用完毕再由池回收。这整个过程application server container都会帮我们管理。

  1. 首先,container会根据EJB的无参构造函数创建EJB。
  2. EJB里的资源会根据注解注入,例如第一段代码的@EntityManager,这是数据库连接资源,后面再详细介绍。
  3. 建立EJB池,把建立好的EJB放入池中。
  4. 如果客户端有请求访问EJB,如果池中有空闲的EJB则会抽取出来给客户端使用,如果没有空闲的EJB,则会创建更多的EJB实体。
  5. 执行EJB业务逻辑。
  6. 业务逻辑执行完毕后,EJB归还池中。
  7. 根据需要移除EJB实体。

  EJB很好的封装了业务逻辑与数据库访问,把数据库与视图分离,且EJB pool很好地处理了线程安全与并发的问题。

EntityManager

  JPA关键在于实现ORM,前面我们介绍了Entity,但单靠创建Entity,我们是无法实现ORM的。要实现ORM,我们需要一个Persistence Unit,即持久化单元。我们依靠Persistence Unit去建立数据库连接。通过Persistent Unit,我们可以注入EntityManager,进而实现数据库业务逻辑的增删改查。

  我们需要现在相应的第三方ORM框架的配置文件persistence.xml里定义Persistence Unit:

1
2
3
4
5
6
7
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="com.hudoumiao_sampleweb_war_1.0-SNAPSHOTPU" transaction-type="JTA">
<jta-data-source>jdbc/weibo</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties/>
</persistence-unit>
</persistence>

  比较重要的是name属性,这个属性是我们在EJB里引用persistence unit所需要的。上面介绍EJB代码里已有相关示例。当我们要把项目部署到application server container里,我们需要使用transaction-type=”JTA”。在这种transaction-type下,我们不需要罗列需要引入的entity class,只需在jta-data-source里指定数据库就行。并且数据库的详细配置我们不需要再persistence.xml里写,只需在application server container的配置文件里配置好就行。例如,如果用GlassFish作为application server container,则配置文件glassfish-resources.xml如下:

1
2
3
4
5
6
7
8
9
10
<jdbc-connection-pool allow-non-component-callers="false" associate-with-thread="false" connection-creation-retry-attempts="0" connection-creation-retry-interval-in-seconds="10" connection-leak-reclaim="false" connection-leak-timeout-in-seconds="0" connection-validation-method="auto-commit" datasource-classname="org.postgresql.ds.PGSimpleDataSource" fail-all-connections="false" idle-timeout-in-seconds="300" is-connection-validation-required="false" is-isolation-level-guaranteed="true" lazy-connection-association="false" lazy-connection-enlistment="false" match-connections="false" max-connection-usage-count="0" max-pool-size="32" max-wait-time-in-millis="60000" name="post-gre-sql_weibo_postgresPool" non-transactional-connections="false" pool-resize-quantity="2" res-type="javax.sql.DataSource" statement-timeout-in-seconds="-1" steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
<property name="serverName" value="localhost"/>
<property name="portNumber" value="5432"/>
<property name="databaseName" value="weibo"/>
<property name="User" value="postgres"/>
<property name="Password" value="gdzqzxwjs95"/>
<property name="URL" value="jdbc:postgresql://localhost:5432/weibo"/>
<property name="driverClass" value="org.postgresql.Driver"/>
</jdbc-connection-pool>
<jdbc-resource enabled="true" jndi-name="jdbc/weibo" object-type="user" pool-name="post-gre-sql_weibo_postgresPool"/>

  上面的定义的jndi-name就是persistence.xml引用的数据库凭据。

  持久化单元(persist unit)就是关于一组Entity的命名配置。持久化单元是一个静态概念。

  持久化上下文(Persist Context)就是一个受管的Entity实例的集合。每一个持久化上下文都关联一个持久化单元,持久化上下文不可能脱离持久化单元独立存在。持久化上下文是一个动态概念。

  尽管持久化上下文非常重要,但是开发者不直接与之打交道,持久化上下文在程序中是透明的,我们通过EntityManager间接管理它。

  利用EntityManager我们便能够完成数据库的增删改查。例如:

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
public abstract class AbstractFacade<T> {
private Class<T> entityClass;
public AbstractFacade(Class<T> entityClass) {
this.entityClass = entityClass;
}
protected abstract EntityManager getEntityManager();
public void create(T entity) {
getEntityManager().persist(entity);
}
public void edit(T entity) {
getEntityManager().merge(entity);
}
public void remove(T entity) {
getEntityManager().remove(getEntityManager().merge(entity));
}
public T find(Object id) {
return getEntityManager().find(entityClass, id);
}
public List<T> findAll() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
return getEntityManager().createQuery(cq).getResultList();
}
public List<T> findRange(int[] range) {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
cq.select(cq.from(entityClass));
javax.persistence.Query q = getEntityManager().createQuery(cq);
q.setMaxResults(range[1] - range[0] + 1);
q.setFirstResult(range[0]);
return q.getResultList();
}
public int count() {
javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
javax.persistence.criteria.Root<T> rt = cq.from(entityClass);
cq.select(getEntityManager().getCriteriaBuilder().count(rt));
javax.persistence.Query q = getEntityManager().createQuery(cq);
return ((Long) q.getSingleResult()).intValue();
}
}

  这是一个通用的jax-rs资源类模板,通过继承这一个类,我们能够实现最基本的增删改查,分别对应EntityManager.persist(),EntityManager.remove(),EntityManager.merge(),EntityManager.find()。其中find方法一般只能根据id查找单个实体,如果需要做更复杂的查询还需要用到Query API,后面一节会详细介绍。这里,我们先简单介绍一下EntityManager的状态。

  EntityManager管理的实体有4个状态,new,managed,detached,removed。

  当我们通过构造函数新创建一个entity对象时,它属于new状态,此时它的数据与数据库是不同步的,是不在persistence context里的。当我们调用persist方法后,它将获得自己的主键,与数据库同步,转化为managed状态,进入persistence context。

  如果我们remove某个处于merged状态的entity,则它将变为removed状态。凡是被查找出来的实体,其状态都为managed,其变化与数据库同步,处于persistence context。

  所有处于detached状态的entity都游离于persistence context之外。如果我们对某个处于detached状态的实体调用merge方法,则会变成managed状态,其实体里的数据将同步到数据库。

  只要实体与数据库同步,我们便真正地实现了ORM。

Query API
基本语法

  我们知道,EntityManager的find方法只能查询某id的一个实体,如果要做复杂的查询则必须用到JPA的Query API。下面先以一段代码来认识一下Query API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public List<Object[]> findDangerousMessages(int page,int perPage){
String sql = "(select message1.word,message1.comment_count,message1.transmission_count,message1.id,message1.publish_date," +
"message2.word,message2.comment_count,message2.transmission_count,message2.id,message2.publish_date," +
"subscriber1.username,subscriber2.username,subscriber1.id,subscriber2.id,manager.dangerous_message.managed_date," +
"manager.action.action_name,account.auth_user.name from manager.dangerous_message left outer join " +
"message message1 on(manager.dangerous_message.message_id = message1.id) left outer join account.auth_user " +
"on(manager.dangerous_message.manager_id = account.auth_user.id) left outer join manager.action " +
"on(manager.dangerous_message.action_id = manager.action.id) left outer join message message2 on(message1.reference_id = message2.id)" +
"inner join subscriber subscriber1 on (message1.subscriber_id = subscriber1.id) left outer join subscriber subscriber2 on (message2.subscriber_id = subscriber2.id) " +
"where message1.is_deleted = false and message1.is_blocked = false and message1.is_removed = false)" +
"order by 2 desc,5 desc " +
"limit " + perPage + "offset " + (page - 1) * 10;
return em.createNativeQuery(sql).getResultList();
}
public long countDangerousMessage(){
String sql = "select count(manager.dangerous_message.id) from manager.dangerous_message left outer join message "
+ "on(manager.dangerous_message.message_id = message.id) where message.is_deleted = false and "
+ "message.is_blocked = false and message.is_removed = false";
return (Long)em.createNativeQuery(sql).getSingleResult();
}

  由上面代码看到,我们定义了一段sql的字符串,然后调用EntityManager的createNativeQuery方法,便可以进行数据库查询。如果要返回多个属性或多条记录,可以用getResultList方法来返回;如果只返回一个属性值,可以用getSingleResult方法。这是最基本的Query API与数据库访问方法,非常简单,只要有sql与数据库基础便可以轻松开发。

  除了用传统的sql语言作查询外,Query API还有一种面向对象的数据库查询语言JPQL,其语法与sql很相似,它的查询是针对ORM后的Entity class的。以字符串形式存储相应的jpql语句,然后调用createQuery方法进行查询,仍然可以用getResultList以及getSingleResult的方法返回结果。

  Query API的更多方法以及JPQL的具体语法可以自行查阅资料,网上资源也很多,这里就不展开赘述了。在开发的时候查查就行,用多了就熟练了,就像sql语言一样。一般大部分sql操作都能用JPQL实现。

  我们还能用@NameQuery的形式预先定义好查询语句,例如:

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Table(name = "dangerous_message", catalog = "weibo", schema = "manager")
@XmlRootElement
@NamedQueries({
@NamedQuery(name = "DangerousMessage.findAll", query = "SELECT d FROM DangerousMessage d"),
@NamedQuery(name = "DangerousMessage.findById", query = "SELECT d FROM DangerousMessage d WHERE d.id = :id"),
@NamedQuery(name = "DangerousMessage.findByMessageId", query = "SELECT d FROM DangerousMessage d WHERE d.messageId = :messageId"),
@NamedQuery(name = "DangerousMessage.findByManagerId", query = "SELECT d FROM DangerousMessage d WHERE d.managerId = :managerId"),
@NamedQuery(name = "DangerousMessage.findByManagedDate", query = "SELECT d FROM DangerousMessage d WHERE d.managedDate = :managedDate")})
public class DangerousMessage implements Serializable {
public class Message implements Serializable {

  上面就是用jpql语法预先定义好一些常用的查询语句。类似的,用sql也可以预先定义查询,此时的标注是@NameNativeQuery。如果在类名前用@NameQuery注解定义好一些查询语句,在代码里需要进行查询时候只需要用name作标识就可以,例如:

1
DangerousMessage dangerousMessage = (DangerousMessage)em.createNamedQuery("DangerousMessage.findByMessageId").setParameter("messageId",id).getSingleResult();

查询方式的选择

  总而言之,我们可以选择sql语句或者jpql语句作为Query API的参数进行数据库查询;确定了语言后我们还可以选择是用标准的查询形式还是预先定义的查询。如果进行复杂的数据库查询,或者一些jpql无法实现的查询,我们优先选择用sql语言实现。而在一般情况下,我们推荐使用JPQL,因为它更贴近面向对象的思想,且其实现与底层的数据库是无关的。换言之,我们可以用一套jpql语法来处理不同数据库的查询工作,如果更换数据库,我们的代码仍然能正常运行,而不用像sql语言一样每个数据库都有自己的一套语法。

  如果查询语句是动态变化,不固定的,那么只能用标准的查询方式。否则,推荐采用预先定义的查询。预先定义的查询语句存放在固定的地方,并且有更好的性能。只要进行了一次预定义查询,后面进行的查询就不用再编译,而是运行之前编译好的缓存的查询语句。

Boundary

  JAX-RS的Resource组件类通常便是我们所说的Boudary。我们在Resource里调用Control层的EJB来进行业务逻辑的运算与数据库访问,然后在Boudary层组装查询的结果。如果查询结果正好是Entity对象,则直接将其序列化传输就行;如果需要另外组合,则定义一个POJO类做为数据传输类(DTO),将其序列化传输。下面是微博评论系统的结构设计:

BCE模型结构

  类名里含DTO的都是新定义的作数据传输的POJO类,MessageFacadeREST是Resource组件类,它继承了AbstractFacaceREST。MessageService是EJB类,封装了大部分的数据库访问与业务逻辑。Entity包里都是ORM后的实体对象。

  其实,MessageFacadeREST也是EJB实体,因为重组数据也是业务逻辑之一。所以,EJB不一定只属于Control层,Boundary层也是需要的,只是我们为了介绍方便放在Control层里讲述而已。Control层的EJB主要用于数据库访问与一些业务逻辑的运算,我们希望尽可能把JPA标准里绝大多数的业务逻辑都放到Control层内。Boundary层的EJB对象则主要处理数据的再封装,以及一些JAX-RS里对于http response的header与body的一些处理。

总结

  这一次主要以BCE模型结构入手,讲述了JPA标准的基本知识,以及其与JAX-RS是怎么联合起来实现一套RESTful API的后台系统的。如果业务逻辑比较简单,我们也可以不严格按照BCE模型,省略了Control层,而把所有业务逻辑以及数据库访问都放在Boundary层的EJB内。如果系统较大,建议尽量用BCE的结构。