基本环境准备

    我们先来看看一个具体的JPA工程示例。要运行这个示例,我们需要如下的类库和软件安装配置好:

    类库: EclipseLink, mysql-connector-j

    数据库: Mysql

    开发环境:eclipse

    因为JPA是一个公开的规范,所以有不同的实现。我们需要一个JPA的类库。这里我们引用了如下类库:EclipseLink  (可以让eclipse 帮我们下载去)。

    在下载EclipseLink包后,解压。我们需要将里面jlib目录下的eclipselink.jar以及jlib/jpa目录里的java.persistence.x.jar引入到工程中就可以。

后面我们定义代码里的映射关系对应的底层实现就是由这两个包实现。 既然要实现ORM,肯定少不了数据库。这里我们用了比较传统的mysql数据库。所以少不了也需要引用mysql的jdbc驱动:mysql-connector-javax.jar。

    准备好了这些之后,我们创建一个JPAProject的java工程。整体的项目结构如下图:


有映射关系的mysql数据表 映射关系表是什么_数据库

    这里为了方便工程引用类库,首先将这些库文件拷贝到工程下面的一个lib目录里,再将这些库引入到工程中。

persistence.xml

    在前面配置好基本的类库之后我们还有一个需要关心的就是,既然我们是ORM,就有一个要映射到数据库的地方。这里是怎么映射到数据库的呢?具体又映射到哪个数据库呢?这些都是在persistence.xml文件定义的。我们在创建好工程之后的src目录里建立一个META-INF的目录,然后在这个目录里建立一个persistence.xml文件。程序连接数据库和对象映射的配置信息就是通过读取这个目录下的文件来实现的。我们来看一个persistence.xml的示例:

1. <?xml versinotallow="1.0" encoding="UTF-8" ?>
2. <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3. xsi:schemaLocatinotallow="http://java.sun.com/xml/ns/persistence   
4.     http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"  
5. versinotallow="2.0" xmlns="http://java.sun.com/xml/ns/persistence">
6. <persistence-unit name="EmployeeService" transaction-type="RESOURCE_LOCAL">
7. <class>model.PersonInformation</class>
8. <properties>
9. <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
10. <property name="javax.persistence.jdbc.url"
11. value="jdbc:mysql://localhost:3306/simpleDb" />
12. <property name="javax.persistence.jdbc.user" value="root" />
13. <property name="javax.persistence.jdbc.password" value="root" />
14.   
15. <!-- EclipseLink should create the database schema automatically -->
16. <property name="eclipselink.ddl-generation" value="create-tables" />
17. <property name="eclipselink.ddl-generation.output-mode"
18. value="database" />
19. </properties>
20. </persistence-unit>
21. </persistence>

    这里都是xml的配置项,看起来也很容易懂。在一个<persistence-unit>的单元里定义了这个unit的名字以及详细的数据库连接驱动,数据库用户名,密码。前面<class>里面定义的是需要映射到数据库的具体实体类。比如model.Employee就对应于我们在package model里定义的Employee类。因为我们要连的数据库是mysql,这里的javax.persistence.jdbc.driver值被设为com.mysql.jdbc.Driver。而我们具体要连接的数据库名字在javax.persistence.jdbc.url对应的值里面定义了,为simpleDb。为了后面实际程序运行的时候能够读写这个库,我们需要事先在数据库里创建simpleDb。

    OK,有了这些基本的设置。我们就能连上数据库进行实际的操作了。

实体对象定义

    现在,我们需要定义一个可以具体实例化到数据库里的对象。我们定义一个PersonInformation的类如下:

1. package
2.   
3. import
4. import
5.   
6. @Entity
7. public class
8. @Id
9. private int
10. private
11. private int
12.       
13. public int
14. return
15.     }  
16. public void setId(int
17. this.id = id;  
18.     }  
19. public
20. return
21.     }  
22. public void
23. this.name = name;  
24.     }  
25. public int
26. return
27.     }  
28. public void setAge(int
29. this.age = age;  
30.     }  
31. }


    这里只是很简单的一个entity bean对象定义。我们针对每个可以操作的对象定义了get, set方法。这里比较有意思的地方是我们用了两个annotation,一个是@Entity,一个是@Id。这里的@Entity表示一个可以序列化映射的的对象。如果我们希望这个对象被映射到数据库中的某个表,则必须要加上这个annotation。而@Id则表示对应表的主键。我们建一个表要求有对应的主键。这里指定id为主键。如果我们不指定主键的话则运行的时候会出错。有兴趣的可以尝试一下。

    有了这些定义之后,我们就可以来使用他们了。这里是通过他们访问数据库的代码:


1. package
2.   
3. import
4. import
5. import
6.   
7. import
8.   
9. public class
10. private static final String PERSISTENCE_UNIT_NAME = "EmployeeService";  
11. private static
12.       
13. public static void
14.         factory = Persistence.createEntityManagerFactory(  
15.                 PERSISTENCE_UNIT_NAME);  
16.         EntityManager em = factory.createEntityManager();  
17.         em.getTransaction().begin();  
18. new
19. 1);  
20. 30);  
21. "fred");  
22.         em.persist(person);  
23.         em.getTransaction().commit();  
24.         em.close();  
25.     }  
26. }


    第一个需要创建的对象是EntityManagerFactory, 这里通过Persistence.createEntityManagerFactory方法。而这个"EmployeeService"是哪里来的呢?我们看前面的persistence.xml文件,那里有<persistence-unit name="EmployeeService" transaction-type="RESOURCE_LOCAL">这个定义。EmployeeService就是对应到这里一个persistence-unit的名字。他们必须一致。实际上,如果我们需要访问多个库的话,在配置文件里也可以定义多个persistence-unit。有了这个factory之后我们再创建一个EntityManager对象。

    为了使得对象的创建成为一个事务来提交,我们通过em.getTransaction().begin(); em.getTransaction().commit();这两个方法来完成整个数据插入的过程。

    现在运行程序,我们再去查看数据库的话,则会发现如下的信息:


有映射关系的mysql数据表 映射关系表是什么_bc_02

    回顾一下我们前面定义的PersonInformation对象的信息,我们定义的字段都是小写格式的。映射到数据库里包括表名和表里面字段名都是大写格式的。前面我们指定了id字段为主键之后,对应的表里这个字段就有了primary key not null的限制。

修改

    从前面的运行结果,我们可以看到一些实体对象和数据库表之间的映射关系。可是在实际情况中,有时候我们希望数据库表名或者里面的字段名能够更加可以定制化一点。不是简单的将对象里的字段变成大写的。这样可以增加一点可读性,那我们该怎么修改呢?

    很显然,我们需要指定对象应该对应哪个表名以及每个属性应该对应什么名字。这是修改后的代码:

 

1. package
2.   
3. import
4. import
5. import
6. import
7. import
8. import
9.   
10. @Entity
11. @Table(name="PersonInformation")  
12. public class
13. @Id
14. @GeneratedValue(strategy = GenerationType.IDENTITY)  
15. @Column(name="id")  
16. private int
17.       
18. @Column(name="name")  
19. private
20.       
21. @Column(name="age")  
22. private int
23.       
24. public int
25. return
26.     }  
27. public void setId(int
28. this.id = id;  
29.     }  
30. public
31. return
32.     }  
33. public void
34. this.name = name;  
35.     }  
36. public int
37. return
38.     }  
39. public void setAge(int
40. this.age = age;  
41.     }  
42. }

    和前面的代码比起来,我们这里增加了几个描述属性。一个是@Table,这里通过它来设定对应的数据库表名字是什么。@Column这个用来设定对应的数据库字段名。还有一个比较有意思的地方就是我们在id字段增加了@GeneratedValue(strategy = GenerationType.IDENTITY)这个描述属性。它表示什么意思呢?它表示这个主键的值可以自动来生成,而后面的GenerationType.IDENTITY表明它的生成方式是自动增长,类似于auto increment.

    针对前面修改之后我们再来看数据库的运行结果:


有映射关系的mysql数据表 映射关系表是什么_java_03

    这样,经过前面这些步骤的描述,我们已经能够完成一个简单的JPA示例了。也因此知道了将对象如何映射到数据库表中。

各种关系映射

    前面的示例是简单的将一个实体对象映射到数据库中。在很多实际的情况下,我们并不是简单的一个对象映射,往往对象和对象之间还存在着一定的关联关系。我们在做ORM的时候,也需要针对不同的情况进行考虑。这个就牵涉到数据库中间表或者实体对象之间的关系。总的来说,实体对象之间的关系无非为以下几种:一对一,一对多,多对多。而对于多对一的情况来说,从另外一个角度来看也是一种一对多的关系。

一对一

    我们都知道,对于不管是哪种对应的关系,实际上他们在对应到数据库的实现里,我们是可以有几种不同的映射实现方式的。我们以一对一的关系开始。假设我们有一个Employee的实体对象。它同时有一个属性是Address。这个Address和它是一对一的映射关系。那么在数据库的实现里,我们该怎么考虑呢?

    我们实际上有几种实现方式。一种很简单,既然他们反正是一对一的关系,我们完全可以把他们当成一个表来使。也就是说这个Address虽然是Employee的属性,但是他们所有的属性放到一个表里是完全没问题的。还有一种就是我们可以定义两个分离的表,他们之间通过外键关联。当然,除了这种,我们也可以定义一个单独的映射表来保存他们之间的映射关系。

    现在我们来看看以上各种方式的实现:

合并成一个表

    我们定义的Employee对象如下:

1. package
2.   
3. import
4. import
5. import
6. import
7.   
8. @Entity
9. @Table(name="Employee")  
10. public class
11. @Id
12. private int
13. private
14. private long
15.       
16. @Embedded
17. private
18.       
19. public
20. return
21.     }  
22.   
23. public void
24. this.address = address;  
25.     }  
26.   
27. public
28.       
29. public Employee(int id) { this.id = id; }  
30. // ...  
31. }


    这里出于篇幅的限制,省略了那些属性的get, set方法。

 

Address类的定义实现如下:

1. package
2.   
3. import
4. import
5. import
6. import
7.   
8. @Embeddable
9. public class
10. private
11. private
12. private
13.       
14. @Column(name="Zip_Code")  
15. private
16. //...   
17. }


    我们来看代码里加入的标注。这里Employee有一个Address的属性。我们在Employee里对这个属性增加了@Embedded标注。而定义Address的类里增加了@Embeddable标注。其中@Embedded表示Address元素被嵌入到Employee表中间。为了能够嵌入到Employee表中,我们还需要将Address属性增加@Embeddable,表示它是能够被嵌入到其他对象里面的。

    我们运行如下的代码:

1. package
2.   
3. import
4. import
5. import
6. import
7.   
8. import
9. import
10.   
11. public class
12.   
13. public static void
14.           
15.         EntityManagerFactory entityManagerFactory =    
16. "EmployeeService");  
17.         EntityManager em = entityManagerFactory.createEntityManager();  
18.         EntityTransaction userTransaction = em.getTransaction();  
19.           
20.         userTransaction.begin();  
21.           
22. new
23. "frank");  
24. 2000);  
25.           
26. new
27. "Beijing");  
28. "BJ");  
29. "Shuangying");  
30. "100000");  
31.         employee.setAddress(address);  
32.         em.persist(employee);  
33.         userTransaction.commit();  
34.         em.close();  
35.         entityManagerFactory.close();  
36.     }  
37. }

 


    我们现在来看数据库的结果:


有映射关系的mysql数据表 映射关系表是什么_数据库_04

 

外键约束

    在采用这种外键约束的时候我们需要首先考虑一下。根据我们定义的逻辑关系,可以认为是Employee里有Address这么一个字段。那么就相当于Employee里要引用到Address的信息。而按照外键的定义,是一个表引用另外一个表的主键。那么,我们就需要给Address定义一个主键,同时我们也要标注一下在Employee里引用的Address字段该是什么名字。我们定义的Employee如下:


有映射关系的mysql数据表 映射关系表是什么_bc_05

1. package
2.   
3. import
4. import
5. import
6. import
7. import
8. import
9. import
10. import
11.   
12. @Entity
13. @Table(name="Employee")  
14. public class
15. @Id
16. @GeneratedValue(strategy = GenerationType.IDENTITY)  
17. private int
18. private
19. private long
20.       
21.       
22. @OneToOne(cascade=CascadeType.ALL)  
23. @JoinColumn(name="address_id")  
24. private
25.       
26. public
27. return
28.     }  
29.   
30. public void
31. this.address = address;  
32.     }  
33.   
34. public
35.       
36. public Employee(int id) { this.id = id; }  
37.   
38. // ...
39. }

    这里,我们增加了一个标注@OneToOne(cascade=CascadeType.ALL)和@JoinColumn(name="address_id")。@OneToOne表示他们是一对一的关系,同时cascade表示他们的级联关系。如果我们仔细观察一下的话会发现前面应用代码里有一个比较有趣的地方,我们em.persist()只是保存了employee对象。而对应的Address对象只是设定为employee对象的一个属性。我们希望是employee对象被保存到数据库里的时候address对象也自动保存进去。那么我们就需要设定这个cascade的级联访问属性。否则我们就需要显式的利用em.persist()来保存address对象。这也就是为什么我们要用一个cascade的属性。

  @JoinColumn里面指定了Employee表里引用到Address时关联的名字是什么。

    和前面的定义比较起来,Address的定义如要增加了一个id:

1. @Id
2. @GeneratedValue(strategy = GenerationType.IDENTITY)  
3. private int


    在我们运行程序前需要将Address加入到persistence.xml文件里。因为我们将它映射到一个单独的表里。我们再运行前面的程序,发现生成如下的表结构:


有映射关系的mysql数据表 映射关系表是什么_java_06

 

单独的映射表

    采用这种映射方式的实现很简单,只需要在Employee里面做一点如下的修改:


有映射关系的mysql数据表 映射关系表是什么_bc_05

1. @OneToOne(cascade=CascadeType.ALL)  
2. @JoinTable(name="employee_address",joinColumns=@JoinColumn(name="address_id"),inverseJoinColumns=@JoinColumn(name="employee_id"))  
3. private


 其他地方都不需要改变。这里的@JoinTable描述了关联表的名字,他们相互关联的时候里面包含的字段。一个为address_id,一个为employee_id。

    运行结果如下图:


有映射关系的mysql数据表 映射关系表是什么_java_08

一对多

    讨论完了前面的一对一映射,我们再来看看一对多的关系。其实和前面一对一的关系很类似,我们可以猜想得到,既然前面描述一对一的关系有@OneToOne,我们这里应该有@OneToMany的映射关系。而且比较有意思的是,在JPA里,除了一对多的关系,也存在着一个多对一的关系描述。那么,在哪些情况下该使用一对多来描述,哪些情况下用多对一来描述呢?我觉得这主要还是取决于我们设计的对象逻辑关系。通过对象的逻辑关系来设置不同的选择。

    我们来看一个示例。

Many to one

     假定我们有一个Employee类,然后还有一个Department类。对于每个Employee来说,他属于且仅属于一个Department。这样对于Employee和Department来说,他们就构成一个多对一的关系。而从Department的角度来说,他们则是一个一对多的关系。假定我们要求Employee对象有一个方法来取得他所在的Department。这样从面向对象的角度来说,我们可能希望Employee有一个指向Department的引用。于是按照这种思路,我们设计Employee的代码如下:


有映射关系的mysql数据表 映射关系表是什么_bc_05

1. package
2.   
3. import
4. import
5. import
6. import
7. import
8. import
9. import
10. import
11.   
12. @Entity
13. @Table(name="Employee")  
14. public class
15. @Id
16. @GeneratedValue(strategy = GenerationType.IDENTITY)  
17. private int
18. private
19. private long
20.       
21. @ManyToOne(cascade=CascadeType.ALL)  
22. @JoinColumn(name="Dept_Id")  
23. private
24.   
25. public
26.       
27. public Employee(int id) { this.id = id; }  
28. // ...  
29. }


    这里我们省略了对所有属性get, set方法。这里用了一个@ManyToOne的标注。并设定了映射的名字为“Dept_Id”。

    而Department的定义则如下:


有映射关系的mysql数据表 映射关系表是什么_bc_05

1. package
2.   
3. import
4. import
5. import
6. import
7. import
8. import
9.   
10. @Entity
11. @Table(name="Department")  
12. public class
13. @Id
14. @Column(name="Dept_Id")  
15. @GeneratedValue(strategy = GenerationType.IDENTITY)  
16. private long
17.       
18. @Column(name="name")  
19. private
20. // ...
21. }


    这个类的定义则没有什么特殊的。和前面定义的差不多。

    运行完之后的数据库情况如下:


有映射关系的mysql数据表 映射关系表是什么_有映射关系的mysql数据表_11

 

One to many 

    还是前面那个示例,不过这次我们换一个角度来考虑一下。前面是我们希望Employee有一个取得所在Department的属性的方式。这里我们希望从Department对象得到里面所有的员工。从面向对象的角度来说,我们可以在Department里面定义一个集合类来保存一系列的Employee对象。那么,这里两个类的定义形式则如下:

Employee:


有映射关系的mysql数据表 映射关系表是什么_bc_05

1. package
2.   
3. import
4. import
5. import
6. import
7. import
8.   
9. @Entity
10. @Table(name="Employee")  
11. public class
12. @Id
13. @GeneratedValue(strategy = GenerationType.IDENTITY)  
14. private int
15. private
16. private long
17.   
18. public
19.       
20. public Employee(int id) { this.id = id; }  
21. // ...
22. }


    Employee的定义基本上不需要做什么特殊的关系映射定义。

Department:

1. package
2.   
3. import
4.   
5. import
6. import
7. import
8. import
9. import
10. import
11. import
12. import
13. import
14. import
15.   
16. @Entity
17. @Table(name="Department")  
18. public class
19. @Id
20. @Column(name="Dept_Id")  
21. @GeneratedValue(strategy = GenerationType.IDENTITY)  
22. private long
23.       
24. @Column(name="name")  
25. private
26.       
27. @OneToMany(cascade=CascadeType.ALL,fetch=FetchType.LAZY)  
28. @JoinColumn(name="Dept_Id")  
29. private
30. // ...
31. }

    这里一个比较有意思的地方是在定义了employees的地方加了一个@OneToMany的标注。它同时也通过@JoinColumn说明了关联的项。

    这里我们运行的程序稍微修改了一下,代码如下:


有映射关系的mysql数据表 映射关系表是什么_bc_05

1. package
2.   
3. import
4. import
5.   
6. import
7. import
8. import
9. import
10.   
11. import
12. import
13.   
14. public class
15.   
16. public static void
17.           
18.         EntityManagerFactory entityManagerFactory =    
19. "EmployeeService");  
20.         EntityManager em = entityManagerFactory.createEntityManager();  
21.         EntityTransaction userTransaction = em.getTransaction();  
22.           
23.         userTransaction.begin();  
24.           
25. new
26. new
27. "Information");  
28.   
29. new
30.           
31. "frank");  
32. 2000);  
33.         employees.add(employee);  
34.           
35. new
36. "fred");  
37. 3000);  
38.         employees.add(employee);  
39.         dept.setEmployees(employees);  
40.   
41.         em.persist(dept);  
42.         userTransaction.commit();  
43.         em.close();  
44.         entityManagerFactory.close();  
45.     }  
46. }


    我们写入了一个Employee的集合。所以最后运行结果生成的表和结果如下: 


有映射关系的mysql数据表 映射关系表是什么_bc_14

    和我们前面一对一映射的关系类似。我们也可以将一对多或者多对一的关系用一个中间关系表来保存。在这里实现就很简单,之需要将Department里面的@JoinColumn去掉就可以了。因为在JPA里,如果我们设定了两个对象之间的一对多属性关系,它会默认生成一个中间表,并且表名以两个表的名字加一个下划线拼接起来。

多对多

    对于多对多的场景,看起来它会显得更复杂一些。实际上则未必。因为当我们典型的针对这种关系来设计数据库表的时候,肯定会考虑将他们拆分出一个中间的映射表来。这样的话,基本上要实现这样关系的映射,肯定就只有拆分映射表这么一条路。这里定义了另外一个示例。假定Student和Department是一个多对多的关系。

这里,我们定义了Student:

1. package
2.   
3. import
4. import
5.   
6. import
7. import
8. import
9. import
10. import
11. import
12. import
13.   
14. @Entity
15. public class
16. @Id @GeneratedValue(strategy=GenerationType.IDENTITY)  
17. private int
18. private
19.   
20. @ManyToMany
21. @JoinTable(name="Student_Dept",   
22. @JoinColumn(name="Stut_ID"),  
23. @JoinColumn(name="DEPT_ID"))    
24. private
25. public
26. new
27. }  
28.   
29. public void
30. if
31.         getDepartments().add(department);  
32.     }  
33. if (!department.getStudents().contains(this)) {  
34. this);  
35.     }  
36.   }  
37.   
38. public
39. return "\n\nID:" + id + "\nName:"
40.   }  
41. // ...
42. }

 


    这里省略了元素的get, set方法。为了实现对象的双向关联,这里定义的addDepartment需要将自己加入到目标Department对象的列表里。另外,我们在对多的关系部分添加了一个@ManyToMany的标注,同时也指定@JoinTable的属性。

    而对于Department:

1. package
2.   
3. import
4. import
5.   
6. import
7. import
8. import
9. import
10. import
11.   
12. @Entity
13. public class
14. @Id @GeneratedValue(strategy=GenerationType.IDENTITY)  
15. private int
16. private
17.   
18. @ManyToMany(mappedBy="departments")  
19. private
20.       
21. public
22. new
23.     }  
24.       
25. public void
26. if
27.           getStudents().add(student);  
28.       }  
29. if (!student.getDepartments().contains(this)) {  
30. this);  
31.       }  
32.     }  
33.   
34. public
35. return "Department id: "
36. ", name: "
37.     }  
38. // ...
39. }


    这里引用Student集合的地方也用了一个@ManyToMany的标注,同时,里面还设置了一个mappedBy属性。这个属性有什么用呢?它是指定我们这个students的集合是映射到Student类里面的departments列表。这样后面生成的对象才找到对应的映射一方。还有一个就是,既然是多对多的映射,在Student里面定义了jointable的属性,这和我们在Department里面定义有什么差别呢?

    这个问题在于我们想定义这个关系的拥有方。我们把jointable的定义放在哪个类这个类就成为了拥有方。那么这个拥有方来绑定和解除他们的关系。另外一方则不能用来绑定和解除他们的关系。

    我们调用的代码实现如下:

1. package
2.   
3. import
4.   
5. import
6. import
7. import
8. import
9.   
10. import
11. import
12.   
13. public class
14. static EntityManagerFactory emf = Persistence.createEntityManagerFactory("EmployeeService");  
15. static
16.   
17. public static void main(String[] a) throws
18.     em.getTransaction().begin();  
19.       
20.       
21. new
22. "Joe");  
23.     em.persist(student);  
24.       
25. new
26. "Joe");  
27.     em.persist(student1);  
28.       
29. new
30. "dept name");  
31.     dept.addStudent(student);  
32.   
33.     dept.addStudent(student1);  
34.     em.persist(dept);  
35.      
36.     em.flush();  
37.   
38. "SELECT e FROM Student e");  
39.     List<Student> list = (List<Student>) query.getResultList();  
40.     System.out.println(list);  
41.       
42. "SELECT d FROM Department d");  
43.     List<Department> dList = (List<Department>) query.getResultList();  
44.     System.out.println(dList);  
45.       
46.     em.getTransaction().commit();  
47.     em.close();  
48.     emf.close();  
49.       
50.   }  
51. }


    这里,我们添加了两个Student对象到一个Department对象中。他们运行后的数据表结构如下:


有映射关系的mysql数据表 映射关系表是什么_数据库_15

    多对多关系对应的对象关系还有一个比较麻烦的地方在于。一旦建立了双方的对象关系之后,就基本上形成了一个对象之间的循环引用。从面向对象的角度来说这不是一个合适的设计。另外,从内存管理的角度来看,这样也容易导致内存泄漏。因为我们一不小心就容易导致一些引用的对象没有被释放。在将这些关系映射到不同对象的时候,一定要非常的慎重。

总结

    以前使用一些语言和开发框架的时候用到ORM。这里针对一个最基本的JPA示例一并探讨了几种典型的对象映射关系在JPA中的实现。有了这些对象关系的定义作为基础我们可以定义很多面向对象的方法而不用采取直接拼接sql字符串的方式与数据库交互。由于要针对这些关系一一讨论,本文的篇幅就会显得长一些。

参考材料

pro jpa2

http://www.java2s.com/Tutorial/Java/0355__JPA/ManyToManyJoinTableJoinInverseJoinColumn.htm