简介
在前面的一篇文章里我介绍了JPA的一些基本配置和几个典型的映射关系实现。经过最近一段时间的应用和学习,对于常用的集中操作和配置也有了一定的了解。在这里一并做一个总结。
基本操作
基本的操作无非就是增删查改这么几个。我们结合一个示例来看看每个具体的操作过程。首先我们定义两个对象实体,分别为PersonInformation和Address。假设PersonInformation和Address是一对多的关系,他们的具体定义实现如下:
package model; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Lob; import javax.persistence.OneToMany; import javax.persistence.Table; @Entity @Table(name="person_information") public class PersonInformation { @Id @Column(name = "person_id", unique = true, nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "name", nullable = false) private String name; @Column(name = "age", nullable = false) private int age; @Column(name = "marriage_status") @Enumerated(EnumType.STRING) private MarriageStatus marriageStatus; @Lob @Column(name = "self_description", length = 512) private String selfDescription; @OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY) @JoinColumn(name = "person_id") private List<Address> addresses; }Address的实现如下:
package model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="address") public class Address { @Id @Column(name = "id", unique = true, nullable = false) @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(name = "country") private String country; @Column(name = "province") private String province; @Column(name = "city") private String city; @Column(name = "street") private String street; @Column(name = "building") private String building; @Column(name = "room") private String room; }这些代码里忽略了对元素的get, set访问操作方法。详细的实现可以看后面附件里的代码。上述的代码里还专门针对enum类型,长的text类型的映射定义。上面定义生成的表结构如下图:
在这个定义的基础上,我们先来看增加一个元素的代码如何实现。
增
假定我们要保存一个完整的PersonInformation对象到数据库里,它有一个对应的Address列表,可以对应一个或多个Address对象。我们希望最理想的情况就是定义好PersonInformation对象和对应的Address对象之后,能够自动将两者都映射到数据库中。
下面是实现增加一个对应到数据库里的代码片段:
PersonInformationService service = new PersonInformationService(em); PersonInformation person = new PersonInformation(); person.setAge(20); person.setMarriageStatus(MarriageStatus.single); person.setName("frank"); person.setSelfDescription("Funny"); Address address = new Address(); address.setCountry("China"); address.setProvince("Beijing"); address.setCity("Beijing"); address.setStreet("ShangDi"); address.setBuilding("Ring"); address.setRoom("302"); List<Address> list = new ArrayList<Address>(); list.add(address); person.setAddresses(list); service.createPersonInformation(person);我们将实现添加元素的功能封装在PersonInformationService类里,该类里createPersonInformation方法的实现如下:
public void createPersonInformation(PersonInformation person) { if(person == null) throw new IllegalArgumentException("Invalid entity argument."); EntityTransaction transaction = em.getTransaction(); transaction.begin(); em.persist(person); transaction.commit(); }我们通过启动一个transaction,然后将更新提交到数据库保证更新的完整性。
删
对应删除元素的操作有几种,基本的是根据一个指定的primary key来删除对应的元素,一个典型的实现如下:
public void removePersonInformation(long id) { PersonInformation person = getSinglePersonInformationById(id); EntityTransaction transaction = em.getTransaction(); transaction.begin(); em.remove(person); transaction.commit(); }
如果我们仔细考虑这个示例的话,会发现一个比较有意思的地方。我们指定的是删除PersonInformation对象,实际上它是关联了Address对象的,如果我们查数据库会发现PersonInformation对象被删除的同时Address对象也被删除了。这是因为在前面的定义里我们有如下部分:
@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY) @JoinColumn(name = "person_id") private List<Address> addresses;
我们这里的CascadeType.ALL指定了级联的读取和删除操作。
查
JPA对查询的操作支持也有好几种方式,一种最简单的就是根据primary key查找对应的元素,它的典型实现如下:
public PersonInformation getSinglePersonInformationById(long id) { return em.find(PersonInformation.class, id); }
还有一种查询的方式是通过类似于SQL的脚本方式,我们称之为JQL。比如说我们想要查找年龄大于24岁而且已婚的person信息,一种实现如下:
public List<PersonInformation> getMarriedPersons() { List<PersonInformation> result = em.createQuery( "select p from PersonInformation p where p.age > 24 " + "and p.marriageStatus = model.MarriageStatus.married").getResultList(); return result; }
改
我们修改很多的数据需要更新会考虑到update一些数据,并将他们反应到数据库中,它的实现则比较简单。比如说我们需要修改marriageStatus状态信息,则实现如下:
public void updateMarriageStatus(long id, MarriageStatus status) { EntityTransaction transaction = em.getTransaction(); transaction.begin(); PersonInformation person = getSinglePersonInformationById(id); person.setMarriageStatus(status); em.merge(person); transaction.commit(); }
很多其他状态数据的修改我们都可以采用类似的方式。
综合看前面几种数据访问的代码,我们还发现一个比较有意思的地方。就是凡是牵涉到修改数据的地方,比如增加数据,删除数据或者修改数据的时候,我们倾向于使用transaction。采用事物的方式可以保证我们数据更新的原子性和一致性。
常用配置
除了前面的几种基本操作,有一些配置对于我们来说也很重要,比如我们希望能看到jpa生成的sql代码,以方便后续进行性能调优。有的时候我们也会想,以前用jdbc写代码的时候,为了提升性能,会考虑使用连接池,那么jpa里有没有支持呢?这几点我们都一一道来。
自动生成表
我们知道jpa的一个重要的好处就是它可以自动生成我们定义的表格。我们将定义的对象实体和数据库的表做好了映射,所以它能够做到这一点。这样有一个好处就是减少了我们和纯SQL语句打交道的机会。在我们前面的一些示例里我们可能已经看到了,我们在配置文件里只需要指定连接的数据库名字和用户名密码,然后其他都不需要考虑。如果我们运行程序的时候,甚至都没有建表,可以程序也可以很好的运行,而且数据库里表也就很神奇的给建立起来了。其实这个功能的实现主要在于如下的配置:
<property name="eclipselink.ddl-generation" value="create-tables" /> <property name="eclipselink.ddl-generation.output-mode" value="database" />
我们可以尝试将原来的数据库删除了,然后只是建一个库而不建对应的表。然后运行程序之后再去看结果。
生成SQL语句
在eclipselink里,生成sql语句其实比较简单。就好比我们输出日志一样,通过查看jpa生成的sql我们可以看到它运行的一些机制。
要启用这个特性,我们只需要在配置文件persistence.xml文件里加入如下部分就可以了:
<property name="eclipselink.logging.level.sql" value="FINE"/> <property name="eclipselink.logging.parameters" value="true"/>
这两个property项放在properties属性内。
线程池
以前分析过线程池的一些简单实现。当时用jdbc访问数据库的时候,考虑到每个具体的连接都由连接池管理的话可以充分利用数据库的资源,也能够提升系统的性能。在最初实现一个简单的jpa示例之后我就想到,如果jpa也能支持线程池的话那就完美了。其实它的配置也比较简单:
<property name="eclipselink.connection-pool.default.initial" value="1" /> <property name="eclipselink.connection-pool.default.min" value="64" /> <property name="eclipselink.connection-pool.default.max" value="64" />
这个典型的配置里,我们默认的线程池初始连接数量为1,在一些情况下当请求增加的时候,它的最大值可以增加到64。这里也可以将最小的线程数量设置为64。这些我们都可以根据需要来进行调整。
cache相关
和任何一个系统访问操作相关的地方,我们都很关注性能。cache就是一个非常重要的提升手段。现在想起来,它用的好可以很好的节省时间,用的不好也会带来很多不必要的麻烦。之所以有这部分的感想主要是在于以前经历的一个项目中碰到过一个设计不合理的问题。
在系统里,如果我们希望通过JPA作为系统的数据访问层,那么它将作为系统访问数据库统一的接口。对于所有通过它来访问数据库的操作,它的支持都很好。在我们系统的一个案例里,却存在一种绕过JPA通过另外一种方式访问数据库的情况。这样在两个进程访问同一个数据库时,如果一个进程修改了数据库的内容,另外一个进程假定是使用JPA访问的话,它一般是不知道的。这样就可能有这么一个情况,假设有数据A,它原来的内容是"abc",但是另外一个进程将它修改成"def"后原来采用JPA访问的进程读到的还是老的"abc"。这是什么原因呢?就因为JPA里默认是支持cache的。所以对于通过JPA来访问的进程来说它们没有修改任何数据,JPA也不会认为它的cache失效了。为了解决这个问题,只能禁用JPA的cache,保证每次访问都去数据库里取最新的数据。这种配置结果如下:
<property name="eclipselink.cache.shared.default" value="false"/> <property name="eclipselink.cache.size.default" value="0"/> <property name="eclipselink.cache.type.default" value="None"/> <property name="eclipselink.query-results-cache" value="false"/>
这里主要禁用的cache有共享的部分以及将查询的结果cache禁用。
总的来说,禁用cache对于系统的性能来说有非常大的影响。在这个案例里作出这样的选择其根源在于没有采用统一的数据访问方式。这种不合理的方式也让我们更深刻的理解为什么系统要定义一个统一的数据访问层而不是随意多个。
总结
JPA它本身是java orm实现的一个规范,除了我们常听到的hibernate,还有eclipselink, openjpa等实现。因为有了同样的一个规范,他们之间可以实现很好的兼容。对于数据库来说这些最常见的增删查改等操作是最基础的。除了这些以外,我们还需要考虑到系统的性能,一些情况下的跟踪调试等等。这些就牵涉到线程池、cache和输出sql语句的跟踪等方面。这里举的例子都是采用eclipselink,采用其他的实现也都有类似的功能。
参考材料
http://stackoverflow.com/questions/2809275/disable-caching-in-jpa-eclipselink
http://stackoverflow.com/questions/12926319/how-to-disable-cache-in-eclipselink