开发者学堂课程【高校精品课-上海交通大学 -互联网应用开发技术:JPA 6】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/76/detail/15759
JPA 6
内容介绍:
一、知识点回顾
二、举例说明
三、三种策略的特点
四、问题解答
五、结语
一、知识点回顾
上节课讲解了有关访问关系型数据库的第二部分,了解了基本概念之后进行类和类之间的关联。
前面所讲到的继承关系共有三种方案,第一种是每类只包含自己的属性,person里面有三个属性,而citizen和resident里只包含自己的属性,继成了person类之后又扩展出来的特有属性。因此需要在副类表里有区分这一行记录是父类还是某子类的字段,在应试的时候把它叫做discriminator(区分符)。
第二种方案依旧是三个表,在每一张表里存储它从父类继承来的属性以及扩展的属性,这样就不需要单独的列去描述这一行记录属于什么,它在哪个表里就表明它归属于哪一类数据。这样的设计与第一种相比,具有一定的缺点,如果多增加一列性别,在第一种策略里只需要更改表其他两个表不需要改动,但是在第二种策略里面三张表都要加字段,即第二种方式的可扩展性明显不如第一种。
当然第一种方式也具有一定的缺点,如果想要得到citizen的所有信息,在第一张表里拿到Id信息后之要到另外一张表做一次关联,找到具有同样的Id身份扩展出的属性后才能拿到所有的字段。
第三种策略只有一张表,既不用做连接操作,又不需要改动若干张表。同样的是具有discriminator列,以及类的区分。这种策略是把所有的属性全部拼接在一起,因此会看到有一些地方为空,即可以允许一些列为空,从语义上来它的约束不如前面的。
在前面两种策略里可以约定passport不能为空,而在第三种案里没有办法进行这样的约定。
无论怎样映射这三个表,它们在数据库里只是表的结构不一样,但在类代码上全部都是下图所表示的样子。
二、举例说明
前面还给大家演示了一个例子,用一张表进行映射。
1.用一张表进行映射
DISC表示区分列,含有不同的飞机,包括空客、波音、伊尔等,它们都会记录机型以及制造商的信息。空客表示大型飞机强调容量,所以会扩展容量,波音强调舒适性,而伊尔什么都没有强调。从继承的角度来看plane仍然使用的是Entity,前面课程中讲到无论是spring JPA还是Hibernate的实现,其实都是Java企业版的JPA的规范,它们两个的代码是几乎一样的,因为它们用的是Java企业版JPA,只有少量扩展出来的使用了其他程序,例如在spring JPA里使用了Json打破循环。
spring JPA和Hibernate的代码看起来很像就是因为它们都是使用Java企业版JPA来写代码。这个例子仍然使用Java企业版的JPA来规定annotation,无论使用spring JPA实现还是通过Hibernate实现,基本的用法是一样的。
首先表明实体,然后映射数据库里的Plane表。它的继承策略共有三种,这里使用的是单表继承,代码中的discriminator对应表里的disc列,它的类型是string类型。
对于plane而言它的值一列是零,另外一列必须要有值,不允许为空,凡是为零的就表示它为plane。Annotation是在主件上靠自动生成策略生成的,Airbus和波音从类的代码上来说,它们的扩展都是plane,其他的内容不用进行书写,因为已经从父类里继承下来了,单表集成也在映射plane。在discriminator这一列上空客的取值是1,波音的取值是2。抛开annotation来说,Java代码本身没有什么特别之处。书写Servlet完成处理,Survlet就是当用户输入一个制造商参数时,通过public的session创建一个新的Airbus、波音以及伊尔飞机数据,并将它们存到数据库里,存储完毕后需要做一次查询。HQL和circle很像,但HQL里面出现的全部是类和对象,它的意思就是从所有的飞机里面寻找它的制造。相当于冒号表示的参数需要从外界设入进去。设置参数manu,表示从请求拿出来的参数manufacture。执行完一个query后可以把某一个制造商制造的所有飞机全部列出来,找出来这样的飞机之后就得到了飞机的集合,将每一个元素都拿出来遍历,得到里面些信息并进行输出。前端的页面会写出相应的一句话让用户选择。
具体代码为:
l
Plane.java
@Entity
@Table(name = "plane")
@lnheritance(strategy = InheritanceType.SINGLE_ TABLE)
@DiscriminatorColumn(name="disc",discriminatorType=DiscriminatorType.STRING)
@DiscriminatorOptions(force=true)
@DiscriminatorValue(value = "O"")
public class Plane {
private Long id;
private String type;
private String manufacturer;
public Plane() {
}
@
I
d
@GeneratedValue(generator = "increment")
@GenericGenerator(name = "increment",
strategy = "increment")
public Long get
I
d() {
return id;
}
private void set
I
d(Long id) {
this.id = id;
}
public String getType(){
return type;
}
public void setType(String type) {
this.type = type;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer){
this.manufacturer = manufacturer;
}
}
l
Airbus.java
@Entity
@DiscriminatorValue(yalue = "1")
public class Airbus extends Plane{
private String capacity;
public Airbus() {}
public String getCapacity() { return capacity; }
public void setCapacity(String capacity) { this.capacity = capacity; }
}
l
Boeing.java
@Entity
@DiscriminatorValue(value = "2")
public class Boeing extends Plane
{private String comfort,
public Boeing() {}
public String getComfort() { return comfort; }
public void
g
etComfort(String comfort) { this.comfort = comfort; }
}
实现效果:
在某种型号下输入正确就会把相应的飞机找出。这个例子的重点在于,无论输入伊尔还是波音、空客,都可以在plane里找到,并且能够实现在输入波音的时候找到波音的飞机,在输入airbus的时候找到空客的飞机。
输入airbus即可找到空客的飞机。由于每次都会插入一个空客、波音、伊尔的飞机,所以数量也在不断增加,即每次插入三个飞机。如果选择的不是airbus而是波音,就会把所有波音的飞机查出。同样道理如果输入伊尔,就会把所有的伊尔飞机查询出来。这个例子说明如果要查询一个飞机,实际上是在三种类里完成查询的,包括plane以及前面的两个子类。因为波音和airbus从继承的角度来看,它们也属于都是plane,所以在查飞机的时候,输入波音和airbus会查出所有类的子类,子类的子类会被天然继承,也会进行查询。故无论是伊尔还是波音、airbus都会查出。
2.通过两张表进行映射
使用join table进行演示。每一张表的类是一样的,如果进行子类数据的查询,就必须要实现连接。左侧定义了cats,表里含有ID、生日、颜色、性别、体重的信息,右侧定义了家养的宠物猫domestic cats,宠物猫的表里含有名字,比如哆啦a梦。Cat和name组合起来表示主件,Cat是外键关联在Cat表的主件。在写类的时候,以cat类为例。包括主件、Birthday、color、sex、weight,家养的宠物猫用两张表映射domesticCat表并多继承一项内容——名字,因此会有getSet。这个例子的整体操作代码为:
DomesticCat domesticCat = new DomesticCat();
Date birthday = new Date();
domesticCat.setld(Long.valueOf(3));
domesticCat.setBirthday(birthday);
domesticCat.setColor("blue");
domesticCat.setName("Doraemon");
domesticCat.setSex("male");
domesticCat.setWeight(20);
session.save(domesticC
a
t);
List cats = session.createQuery("from Cat").list();
for (int i = O; i< cats.size(); i++){
Cat theCat = (Cat)cats.get(i);
out.println("id: " +theCat.getld() + "<br>" +
"birthday: " + theCat.getBirthday() + "<br>" +
"color: " + theCat.getColor() + "<br>" +
"weight: " + theCat.getWeight() + "<br>");
if (theCat instanceof DomesticCat) {
DomesticCat aCat = (DomesticCat)theCat;
out.println("name: " + aCat.getName()+ "<br>");
}
out.println("<br>");
}
session.getTransaction().commit();
创建DomesticCat,注意它是一个子类,然后设计它的ID、birthday、color、name、sex、weight,然后进行存贮,除了name之外的所有字段都存在了cat这张表里,name会存到DomesticCat这张表里。查询出所有的猫可以找到哆啦a梦,尽管它的数据一部分位于Cat中一部分位于DomesticCat里,但是依旧可以找出,所以在查询时全部改成Cat的字段,然后判断是不是DomesticCat,如果是再输出它的名字。
运行show cats的例子,show cats只有一个query,点击即可把哆啦a梦找出来。可以了解一下哆啦A梦在数据库里的存储,在cat这张表里看到2002年2月29号出生的蓝色公猫,体重为20斤,在domesticCat表里只存储了name哆啦a梦,而它的输出包含了ID、birthday、color以及name一起输出。即在查询一个cat时,可以把哆啦A梦一起找到。这就是第二种方法——关联操作(join连接操作),使用的是join table策略,它的代码在cat里映射相应的表,基层策略使用的是join,其他并没有什么特别之处;在domestic里由于需要扩展,所以也必然使用join,它映射的就是domesticCat。回顾Cat和domesticCat的代码,分别如下所示:
Cat.
Java
@Id
GeneratedValue(generator = "increment")
GenericGenerator(name = "increment", strategy = "increment")
public Long getld() {
return id;
}
public void setld(Long id)
{this.id = id;
}
public Date getBirthday()
{return birthday;
}
public void setBirthday(Date birthday){
this.birthday = birthday;
}
public String getColor() {
return color;
}
public void setColor(String color){
this.color = color;
}
public String getSex(){
return sex;
}
public void setSex(String sex){
this.sex = sex;
}
public int getWeight(){
return weight;
}
public void setWeight(int weight){
this.weight = weight;
}
DomesticCat
.Java
@Entity
Table(name = "domestic cat")
public class DomesticCat extends Cat {
private String name;
public DomesticCat() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
hibernate.cfg.xml
<!-- Names the annotated entity class -->
<mapping class="org.reins.orm.Entity.Cat" />
<mapping class="org.reins.orm.Entity.DomesticCat" />
Cat比较常规没有什么特别之处,DomesticCat抛开上面的两个注释也是一个普通的Java类,也没有什么特别之处,但是这种映射方案可以体现出它的优势在于创建了一个DomesticCat类型的对象,经过Hibernate进行处理的时候把一部分数据存储在了Cat里,一部分数据存储在了DomesticCat里,除name之外所有的数据都在Cat里,只有name存储在DomesticCat里。
第二种策略的代码和第一种策略飞机的代码从servlet里访问数据库的角度来看,没有注意这些数据是如何存储的,只知道需要创建airbus、波音、Plane,然后输入save进行存储即可;第二种策略的Cat也是一样,创建一个DomesticCat,设置save进行存储。从Java代码的角度去看,两者的差距很小,唯一不一样的地方在于annotation,使用的集成策略不一样。
3.一个类一张表
这种策略表示如果要映射一张表,会把它从父类继承来的所有属性以及扩展的属性全部存入一张表。
比如fruit水果的属性包括形状、口味、颜色,苹果特殊一些还另设置了weight属性,想要了解苹果的重量。苹果是fruit的一种,它所有的属性理应存在一张表里,而不是将除了weight之外的属性放在父类中,weight单独存放一张表格。它在映射fruit时实行的策略是一个类一张表,在子类里指明它映射的表。同样的道理后续在处理fruit的时候,创建一个Apple设置了相应的属性之后进行存储,仍然需要查询父类,因为Apple是fruit的子类。从面向对象设计的角度来说,一个子类的属性可以被当做父类去操作判断,所以Apple就是一个fruit。因此from fruit表示获取所有fruit对象,可以把apple找到并输出。fruit的有些属性可以直接调用,判断是否为apple,如果是则输出weight。具体代码为:
FruitServlet.java
Apple apple = new Apple();
apple.setld(new Long(5));
apple.setShape("round");
apple.setFlavor("so-so");
apple.setColor("yellow");
apple.setWeight(9);
session.save(apple);
List fruits = session.createQuery("from Fruit").list();
session.getTransaction().commit();
for (int i = 0; i < fruits.size(); i++)
{Fruit theFruit = (Fruit)fruits.get(i);
out.println("id: " + theFruit.getld()+ "<br>" +
"shape: " + theFruit.getShape() + "<br>" +
"color: " + theFruit.getColor() + "<br>" +
"flavor: " + theFruit.getFlavor()+ "<br>");
if (theFruit instanceof Apple) {
Apple aFruit =(Apple)theFruit;
out.println("weight: " + aFruit.getWeight() + "<br>");
}
out.println("<br>");}
运行后可以将苹果的信息输出。数据库里没有插入任何fruit,fruit这张表仍然为空,Apple里找到了三条记录。
三、三种策略的特点
1.三种策略从Java代码来看具有共性。
无论是哪种策略对应的例子(猫、飞机和水果的例子),代码都并不知道数据库是如何存储的,只能看到子类或父类对象;当对父类进行查找的时候,所有满足条件的子类也可以被加载出来,即把表里存储子类的记录抽取出来创建成自类对象。而像它到底是在一张表、两张表还是要通过join操作才能得到所有属性的类似问题全部被屏蔽掉,在代码里无法看出,这就是进行Hibernate映射的好处。
上面的三个例子合起来可以称作集承策略的例子,再一次体会到了对象和关系之间不是一一对应的。讲解这三个例子的原因只是因为在一个工程里运行比较方便,按道理来说同一例子写不同代码(annotation)最终的运行效果是一样的。无论在关系库中怎样存储对象,对象这一端的效果就是一个类有两个子类,而在关系层数据库里有三种不同的策略去存储。这三种策略各有各的优势和缺点,主要取决于应用场景更注重策略的哪一点优点与缺点。它们在Java端的代码是一样的,对象和类之间绝对不是一一对应的。如果一个类映射出一张表,两张表完全一样,基本是join table的属性,排斥了另外两个策略。
2.规律
OR映射还是非常灵活的,在讲继承之前提到两个对象在映射的时候方向和关系层数据库是不一样的。在关系层数据库只能是多的这一方引用1的这一方,多的这一方具有外键可以引用,反过来则不可以,并且一个多对多关系必须拆成两个一对多关系;但是在对象可以直接映射,它的方向可以颠倒,甚至可以直接映射多对多,所以会出现四个表被映射成table和in两个类。通过annotation方式实现了对象的属性和关系层数据库表里字段名字间的结果,一个类通过table可以映射到不同的表上,再通过配置文件把数据源的位置(MySQL数据库还是Oracle数据库)以及连接数据库的驱动程序、三种不同的集成关系确定出来。对象和关系层数据库间的差异非常大,这是因为在面向对象的世界里以面向对象的方式考虑问题,而关系层数据库受制于二维表格之间的关系,以关系层数据库的方式考虑问题,所以它们在进行实体的建模时,模型肯定会存在巨大的差异。JPA规范(spring JPA和Hibernate)要做的事情就是允许两者按照各自的思维设计模型,JPA从中完成映射实现转换。
OR映射的实质就是在看待事物不同的对象和关系层之间完成映射。可以简单理解为一张表是一个类,一条记录是一个对象,但实际上它们两者完全不一样,它们是可以解耦的,可以不受任何约束的设计两端,然后通过OR映射工具以及各种annotation的组合实现它们之间的映射,这是OR映射非常关键的思想。没有必要考虑首先进行关系层的输出还是首先考虑对象,两者本来就可以安装各自的思维考虑,中间的OR工具完成好映射关系即可。
四、问题解答
后端的应用做好后只要1个,现在的前端是react。下节课会讲解react如何与后端进行交互,之后的课程会讲解微信、ios以及安卓的客户端,但是不会直接讲解原生的Java或servlet、object是如何开发的,还是以框架进行讲解运用,比如JS或者dart的框架。这些框架的后台仅有一项,前端会有不同的工程,因此后台的项目非常重要,必须要建设完善。之前讲解的所有例子都通过两种方式创建,一种是创建新的Mavan工程,需要选择出一个架构类型,这里选择的开发WebApp。
可以发现建设出来的工程和演示项目类似,含有一个pom文件,另外main文件夹里含有内容,其他文件夹可能为空,需要从pom文件里引入需要用到的内容。若不了解自己需要哪些内容,可以通过思考得出,例如hibernate需要添加Hibernate的依赖,可以在mvnrepository网站上搜索如何写入Hibernate。
可以搜出许多hibernate的相关写法,每个写法基本上都有对应的解释,可以通过解释选择适合的写法,目前最常用的就是hibernate core。将添加hibernate core进去之后还要注意一点,课程提供的文件里缺少一个动作没做,运行时如果报错需要铭记:这种工程运行起来之后在File里具有project structure(由于Windows版本的不同,名称上可能有所差异),需要在project settings的modules选择目录是什么,比如Java目录的蓝色文件夹表示原文件,点击resources后系统就明确了在打包时要把目录中的内容打包至发布的包中,才能够部署到contact中。
上面的两个动作以及Mavan的创建,系统默认选择的Java原值都比较低,可以自行修改选择。选好之后在resource里放置了一些配置文件,比如hibernate的配置文件,还有一些SQL脚本,也可以不放,因为SQL脚本只用于在自己机器上运行时建设hibernate,至此环境工程就具备了,接下来Java组织包的流程在上节课中进行了讲解,Java中分为很多包,每一层都会做相应的事情。
目前的内容都比较简单,具有基本实体和一些前端思维就可以操作,当然只针对当前阶段后续深入阶段需要不断地改写。还需要增添配置运行的工具,选择TomCat并建立一个本地的TomCat,建好之后在Mavan运行就可以使用了,这是一种拿自己的TomCat用Mavan运行的方式;还有一种是直接使用SpringPut,在Spring initializer的下一步选择需要用到的工具,这些工具是在网上查询出来的,因此会比较慢。
设置好自己的信息,这一步可以随意设置,关键在于明确所需要的工具,例如开发web,需要用到Spring web,其他的也要依次筛查,例如SQL中需要用到Spring Data JPA、MySQL Driver以及NoSQL中未来会开发的Spring Data MangoDB,其他类型也是类似需要逐个筛查。工程建好之后其运行比较简单,建立一个 demo进行举例讲解,建设成功之后不用再进行配置即可运行,因为它自带了ablication。选好工具之后并没有下载依赖包,点击import引进包开始下载,全部下载后会显示出来,与前面所讲内容一致,包含Java、resource等,若对目录不满意,可以自行设置,决定原代码、resource等包的位置,设置完成后才可以在将来打包至附属包中。目前来说可以使用这两种方式进行设置,但是不限制开发工具,如果使用其他开发工具配置方法可能就有所不同,但本质上都是Mavan的工程,通过Mavan管理工程并在里面添加需要的依赖包。
个人倾向于使用自建Mavan的方式,使用Spring put也是可以的,本质上并无太大差异。目前讲解的都是有关后端的内容,所写的网页只用于展示所写后端的效果,因此也需要进行设置,选择了ArtiFacts所部署的内容之后可以看到webapp文件夹中会出现一个蓝点,在部署时就包括这个文件夹中的内容,所以刚才的页面才可以被访问,实际上未来的形态逐渐向取消这些内容的趋势发展即后台,前台就是写的react或者是view界面,两个工程在运行的时候会出现跨域问题,这个问题之前没有详细讲解,具体的资料可以在14课专门来解决跨域问题,部分同学对前端理解有一定的问题,之前的设想是先将后端的问题处理完毕再讲解前后端互通的课程,这次作业由于还未讲解可以不用完成前后端互通的部分,将后端部分完成即可,如果前端接近后端了,即如果前端修改过可以把前后端一起上交。
真正的前后端互通属于两端跨域问题,跨域需要通过一定的工具进行处理,这会在后续的课程提到。开发后端的工程就是建设空的webapp的内容工程,或者用springput选择所需内容,还有一种在spring网站设置的方式,spring网站里具有一个有关spring initializer的专属页面,同样可以选择所需工具,通过增加依赖实现,例如web或其他的内容,全部点击选择后点击生成,即可生成工程,把该工程下载下来后可以得到与第二种方式创建的一样的工程,使用spring initializer创建和Mavan创建的工程是一样。
同学们可能对作业程度有疑问,将来的前端可以有若干种,无论是react还是其他的一些工具,后端只要一个。所以作业程度要求是:将数据库设计完善,对它增删改查的动作应通过JPA实现,作业数据库的最小级应该是这样的:Book用于存放所有的书,User用于存放所有用户,Orders来存放所有的订单,orders应该与user关联,即将订单与下单者相匹配,orders表示具体的订单情况,比如A这本书两本,B这本书三本,A书的价格是元,B书的价格是五元。每一行对应的就是ordersitem,item要分别与books和orders关联,比如一张订单的第一行表示A书两本十元钱,B书是3本5元钱,第二张订单可以为任意的书籍,但至少要含有四张表,这个过程发生在MySQL中。
由于需要配合张老师的讲课内容,本课程还会讲解NoSQL、mangoDB,上节课提到一部分数据是在mangoDB里抽取的,mangoDB的数据体现在设计中,包括两种方案,一部分同学倾向于把user图片存至mangoDB中,使用base64将网络上传的图片输出为字符串(base64的字符串);还有一部分同学倾向于存储书本的详细介绍封面。
无论何种方式都有一部分数据存储在mangoDB中,类似于上节课讲解的person例子,person里面有一部分数据来自于关系层数据库,一部分来自于mangoDB,这一部分是在未来学习的。现在至少要将表设计完善可以映射出实体类,构建出相应层,service可以简单一点,在未来还能够不断的扩展。
目前能实现单表的操作即可,所以后端的工程未来是完整的要实现同时操作,而刚才提出的是最小级数据库,有部分同学提出还可以扩展一张管理员表,user与管理员的权限是不一样的,还有部分提出不应该是管理员表格,而应该是权限集,这个集合对应user里面的role字段,体现管理员具有的权限以及一般用户具有的权限。扩展取决于大家自己的设计,有些同学认为购物车也应该是一张表,也应存储在数据库里;有的同学认为购物车和order的效果是一样的,两者只是状态不同,使用字段进行区分即可,比如1代表order,2代表cart……因为讲解的只是最小级数据库,可以根据自己的设计进行不断的扩展,可设计出更为复杂的数据库。
希望数据库可以映射,产生DAO层后在上面进行访问时最好完全是面向对象,不要再看到具体对数据库的操作了,希望以这种方式来实现工程。
五、结语
无论后端怎么开发,后端会处理数据库里的数据,前端可以看到页面react、微信小程序App、ios和安卓上的APP,按道理ios上的App应该由Swift或者objectc开发,安卓由Java开发,希望可以利用之前已经比较熟悉的react或view,因此在课程中花费了大量时间讲解JSA框架,比如cordorar。
春节前夕学院组织了一些学院毕业生进行座谈,提到flutter这一新语言,不管怎样前端这一系列的内容最终只使用一个后台,如果开发两个后台则会失败,因为它们全部为同一客户端,没有必要开发两个后台。
所以后台的应用必须要开发完善,用以向所有的前端包括页面端、微信端、手机端做支撑,后端所需要达到的程度就是能够足以支撑起来所有的前端。