开发者学堂课程【高校精品课-上海交通大学 -互联网应用开发技术:JPA 5】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/76/detail/15758
JPA 5
内容介绍:
一、Overview
二、spring 的 JPA
三、接口编程和分层多的原因
四、继承如何建模
一、Overview
上节课提到 Hibemate,Hibemate 有个很重要的东西。第一个是提供gpa 接口,另外第二个是提供 Hibemate 本地原生的接口。
具体意思上节课提到过,现在可以对照 persistence 的 JPA,会发现实际上 Oracle 作为 Java 的拥有者。Java 的企业版里提出了一个标准就是 JPA,在 JPA 的规范里定义了各种各样的 entition,比如刚才提到的entity。所以 Hibemate 必须支持它,另外其自己发挥也可以去用,但是在刚才的例子里基本上没有用,使用的都是 Java企业版里面定义的 JPA 的接口。
二、spring 的 JPA
再来看spring,spring的 JPA 里面这两个类就是刚才说的,无论是 entity、table、还是这里看到的 ID 、column、generatedValue、Basic 等等这些东西都是 Java 的 JPA 规范里的。
所以所谓 spring 的 JPA 是指 spring 提供了一个符合 JPA 规范的一种实现,那所以JPA 的 API 里面提的这些 entition 必须是一定的批次。
这样可以看到 spring 的 JPA 和 Hibemate 的 JPA 例子里面写的东西没什么差别,基本上写法都是一样的,因为本来是使用的就是 JPA 的entition ,包括后面 ManyToMany JoinTable中写法都是相同的。
所以区别其实在于实现,而实现对用户是透明的,这里只是对着entition 去编程。存在可能例如 spring 的实现和 entition 实现存在差异。
例如 eager 和 lazy 的循环加载上,其二者实现是存在差异的,这是第一点需要注意的地方。所以刚才讲述的内容,不是 Hibemate 独有的,其实是对着 JAVA 的 JPA 的标准在进行编程。entition 是 JAVA 的 JPA 中的代码。
接下来查看代码,可以看到下图前面两个(table、jsonignoreproperties)是刚才提到的标准的 JAVA 企业版里带的东西。
后面只是意思使用 Spring 的 JPA 的实现,而不是 Hibemate 的实现。
如下图所示,这两个东西放置在这里,也可以看到标准化的好处是什么:只要熟悉一个,并且都遵循标准,那编写代码实际是一样的。
三、接口编程和分层多的原因
1.DAO & DAO Implementation
观察可以发现在 Dao 这一层是接口实现类,在 repository是接口实现类自动生成。
那么在这个实现类里用到了接口的实现类,但是跟实现类不绑定。其需要创建一个接口类型的对象,实际是实现了这个接口的类对象,至于到底是谁还需要寻找。这里 Dao 用到 repository 是通过这种途径。
2. Services & Services Implementation
再看 service 用到 Dao 的时候还是这种状态,实现了这个接口的类,创建对象,赋给 eventDao,这里并未提到是否为 Dao 的实现类,如果此时在整个工程里全包扫描协助寻找,找到 Dao 的实现类,并将其注入其中。
3.Controllers
service 用 Dao 是这种方式,Control 用 service 也是这种方式,这里不再详细说明,现在问题是为什么需要使用这种方式,这就叫做针对接口编程(design by control)。control 在软件中的含义就是接口。就例如同学之间分工合作完成项目,肯定需要三方间两两先定接口,定好接口不移动后再去实现。
4.针对接口编程和分层多的原因
中间道理的含义在于例如这里需要使用 service,那 service 实现类就写作 EventServicelmpl 这个类,但如果之后觉得 EventServicelmpl 这个类写的不好,需要写真正的 event service的时间类,也就是真时间类。那如果在controllers 的代码里,直接写其时间里某一个,那岂不是说明目前这段代码需要更改,那么是否可以不更改,就是只对接口编程。
至于具体是哪个实现类的对象,创建出来赋给了 event service,那靠spring 组装。
那如果将来把 true event service implementation输入到包里,或者比如把它替换掉,其扫描结果就是 true EventServicelmpl。如果不打包这个,那扫描到就是另外一个。
所以不管使用哪一个,可以看到EventService eventservice代码不进行修改,这里本来就没提到和时间类绑定,只针对接口编程,无论是哪个实现类,是service implementation 还是 true service information,这两个都实现 eventservice 接口,所以在 C++里可以认为其是它的子类,因为 C++里面没有纯接口概念,所以它是可以扩展的多个类,可以认为它跟子类一样。所以无论是 service implementation 还是 true service information,它们都是 even service 类型的对象,所以这里编程时只针对于接口编程,每一层都这样。每一层都和具体的实现类是结偶的,这样如果替换实现类,上一层的代码就不用动,就依赖于接口的这层代码不用动,这就是要做针对接口编程的原因。所以如果直接写这是 evens service implementation 类型的对象,就不用 Auto wired 的注释,其已经直接到这个类了,但也意味着和这个类关系已经绑定,如果未来需要更换类,就需要修改代码。肯定更希望代码尽量容易维护,所以针对接口编程。
另外为什么需要分多层,解释首先大层是有 controller 层,有 dao层,dao 和 daoimpl 是值刚才看到的全是定义接口,然后底下定义类,只是这将类封装到了这里,本质上它俩是一层,实体是一层,然后 repository 是一层,service 是一层,Service的实现跟道和道的实现是相同的道理,本质上是一层。
实际上分了这么多层,那它们各自的作用是什么:
1. entity 的作用就是在映射数据库里的数据,是非常明确的作用,但是其做映射需要的操作是通过 repository 这一层去实现,原因是jparepository已经协助实现了,不用再重新做起了,例如像 getone 这类,就不用自己编写,使用它的实现就可以完成。所以可以理解为对数据库的操作。
2. Dao 还是在对数据库操作,直接调底下 repository 的方法就过去了,写的代码都显得多余,那 Dao的作用是什么,其实 Dao 这一层是在对数据进行组装,但是数据未必都是通过 ORM 映射得到的,也就是说 repository 实际上是在执行 ORM 映射的操作,如果有部分数据不是通过 ORM 映射得到的应该如何处理?这时就应该在 Dao 里面去做实现,比如有部分数据在 mongoDB 里,这部分 repository 是自己集成入的 JPA 的,它只能拐弯映射这部分,那 mongoDB 里就要在 Dao 里去处理。
3. Service 这一层是在说需要去处理数据,去调用 event 到方法得到数据,然后进行处理,再将其写回去。Service 主要是面向应用,再按照应用的逻辑去组装 service,可能会去调 person 和 event 的 Dao 去实现人加入实践的服务,所以其可能会使用多个 Dao 来实现后台逻辑,其在改变 person 和 event 的状态。
4. Controller 在响应前端发过来的这些请求,然后转发给后台的service,不把 service 的代码直接写到 controller 里面去的原因就是将业务逻辑和页面跳转的关系结合在一起。前面 controller 应该只是在转发,接收请求之后,转发给后台的 service 去处理,那这些 service 在调后台的都要去做整个数据状态的修改。将 service 和 controller 合并是可以的,合并到一起就写一层,就算将所有的层都合并到一起,相当于仅仅只留一层,再加上实体类都可以,上节课模拟的 Service 操作实体类就是这样的。
只是做这种分层的设计之后,各自代码就会比较好维护,比如说有请求来,需要发给某个 service 去处理,至于此 service 的逻辑可以不管了,将来可以在 service 里改写其逻辑,这部分代码就不进行改动了,只需要管理前端请求时转发的目标是谁,具体转发给它,只需要不要影响这里的代码就可以。
如果原来访问 person 的 Dao,然后再访问 event 的 Dao 是这样状态的转换。现在逻辑要发生变化,不管怎么发生变化,最好不要对controller 产生影响,如果把这两层合起来,就是这层不能独立出来,只要稍微有点修改,就要修改其代码,就是这样的结构。当然后面概念,会反复给大家提醒。
现在的问题是在 Dao 里真有两部分代码吗?其实刚才讲了一个在后面在讲到 mango DB 的代码,可以先给大家看一下,就会明白为什么需要 Dao 这一层。下图看到 person 这个类,因为这里基本上是用一个表从头做到尾的,使用 spring JPA ,可以看到就是刚才的类,所有东西都是一样,只是在后面添加了一个人的头像 icon,还有 Transient,如果有时间的话等会会再次提到。
这里加 Transient 就表示这个属性不归 JPA 管。icon 的定义它来自于 mango DB 里面有一个像表的东西叫 collection,来自于person icon,里面就存了一张图片。然后把图片转成字符串(iconBase64)然后进行存储。然后看 person 的 repository 是刚才说的,通过 spring 的 JPA 生成的,里面没有任何代码,但是这个 icon 不通过 spring 的 JPA,而是通过mango repository 的。但其实没有编写任何代码,就靠它来协助实现。
然后关键在 Dao 这一层看到有一部分数据来自于person repository,有一部分来自于person icon repository,在 find one 这里可以看到,做了一次组合,而不是直接访问,就直接就掉到底下的 person 实现就成功了。这里是将 person 里面调用 get one 把这个人取出来,也就是取出了它在数据库里存储的东西,然后用这个 person icon repository从 mango DB 里取出来这个人的头像。这时看头像是否取到,如果取到就把其设置到 person 的 icon 属性里。
刚才提到 person 的 icon 不归 Hibematet 管,那需要把它设置进去,所以在这里只要从 Hibematet 找出来的 icon,就把 icon 设到这个 person 里去,如果没找到,icon 就是空。
返回 person 再看到上面才说的是 service 这一层,它只管调到的 find one。这时就看到分层结构的好处在这里:Find one 这个动作实际上是在通过两个 repository 实现的,但是怕封装完之后, service 体会不到这点,直接调 find one 就得到了,就把这种复杂性通过分层的方式给解决掉了。那这就是刚才说数据确实可能一部分
在数据库里,一部分在关系数据库里,一部分在网络地址里的例子。这个例子跑起来,如下方截图,这里储存了三个人,刘备、孙权和曹操。
在这地方如果需要是获取谁就进行点击,这部分数据是来自于关系型数据库,底下的是来自于 mango DB,但是一到前排是看不到的,其实到 service 这一层就已经屏蔽了差异,这就是为什么要分这么多层,每一层都有作用,然后让每一层之间互相结合。刚才提问到,至少 Dao 层或者 repository 这层要分开,通过这个例子能看到,service 为什么在不在 controller 里直接写进去,controller 里面实际上应该只写接受什么样的请求,然后把请求调某一个 service里头写,这样 service 的未来比如说不是通过 Dao 得到,或者说通过 Dao 得到之后还要对对象做一些复杂的判断。这些逻辑就全部都封装起来,其不应该出现在这个 controller 里,所以这就是分层结构的含义,就是这张图里的结构的含义,当然可以对它裁剪,例如就要将 service 放到 controller 里是可以进行的,但是刚才在编程时看到 service 如果要放到 controller 里,controller 的代码会显得非常多,太好维护了。还有是其实有一些助手类,可以再增加一个包,比如说 utility 这样的包,接收或者做一些辅助的功能,这些辅助功能全部放到这里,这就是助教做这样包的意义,总的来说,现在需要清楚的就是应该是个分层结构,Java 所谓的分层就通过包来实现,如果注意,可能会发现刚提到的话还有一点差异。
就是写的时候分成这么多,那每一层里都会有一些相应的实现,那么如果把 service 实现搬到 service 里也可以,就是不要让它跟service 是平行的。service 里面有一个子包叫 service 实现的包也可以,所以这是灵活的,不需要一成不变。但是需要通过分层,总的来说,代码这种组织的方式比较好,然后实际上是通过包的方式在做分层的设计,重要的是不要拘泥包的样子,其实需要搞清楚分层的架构,分层的架构不靠包来实现,大家看到实际上是在 even controller 里使用了 service 的实现,service 里又使用了 Dao,Dao又使用 repository ,repository 最后操作的结构,这才是在做分层包,分层包只是代码组织的方式,即使把它都组织到一个包里,它仍然是个分层结构。因为其调用的方式就是这样一层一层下来的。今天当然是大家第一次接触这样的分层架构,可能觉得很难理解,或者很难掌握,其实写程序就跟写作文或写诗一样,先需要有模板照着抄。写的、照的差不多了,就会有自己的体会,大家按照架构去看代码组织怎么样,并将其跑起来,自己在上面改一改,就能体会到里面这样组织的好处。但是包的组织形式和分层其实不是一个概念,包组织形式就是 controller、Dao 的实现类等等。分层是代码里体现的,刚才的调用的关系,几层是这样出来的,所以通过 spring JPA 例子,想强调两点:
第一点是 spring JPA 和 Hibemate 没有本质区别,因为其大量使用的是标准的Java JPA 的东西。
第二点要强调的就是现在写代码时,分层架构非常重要,大家在写后台的代码时,现在就应该将其变成分层的架构。现在讲的所有东西全部都集中在是应该怎么做映射,然后对它做一些简单的操作。
现在讲一讲开始讲的坑,坑是面向对象的,这个 or 映射里面对象和关系之间有一些东西是不太匹配的,现在要继续讲讲集成是如何建模的。如果大家对刚才的 spring 的 JPA 和 Hibemate 的 JPA 之间的类相似和不同的关系有问题,可以先讨论一下,或者说大家如果有其他问题可以提出来讨论。
提出了问题:event service 和 event Dao 的区别是什么,Dao 这一层目的还是对 entity 操作,实际是一对一的,就是 person是对 antidote 操作,实际是面向数据访问,而 service 实际是业务逻辑,比如 service 里有下订单动作,那这里一定会调用 user的 Dao 观察用户身份,然后调用 order 的 Dao 把 order 给写进去,它会涉及到多个的 Dao 的操作来改变系统的状态,它是面向应用逻辑的,所以有这样的差异。
四、继承如何建模
继承怎么去建模,就是其应该怎么做映射的,继承就是在类的继承里是有概念,在表里没有表继承的另外表的概念,那怎么做映射,在继承里只有一种方法就是子类继承父类,但是把这样的继承关系存储到数据库里实际上有三种策略。
这三种策略下面是各举了一个例子,如果跑过代码的话,就知道其有三种不同策略可以实现,但从对象的角度去观察其实是相同的。但在关于数据库里有三种不同的存法。
举个简单例子,这里存在 person 的类,里面有姓名和年龄,Person有两个子类,一个子类为 citizen(公民),公民就有护照,也就是扩展出来的属性。另一个子类为 resident (居民),这不是公民,而是外国定居的,所以其国籍是什么?国家是哪个?然后持有的签证类型是什么?
那这在面向对象的事件里,做此计算很容易理解,就是子类继承父类之后,会继承父类里所有的属性,另外可以扩展属性。那这样的三个类,它们表示的数据在存入数据库时是几张表?不管是几张表,这里会采取一定测量往里存,刚才提到三种方案,这三种方案各自的优缺点是什么?那不管怎么存,在面向对象的世界里看到就是这样。下面看看如何存。
1. joined table
第一种方式:三个类映射出三张表 Person、citizen 和 resident。
这种方式的名字叫 joined table 意味着想要拿到一个人的所有属性,必须要做连接的操作。比如说 John 是 citizen,并且这里有年龄,然后有姓名,这是 person 里自带的,然后将扩展出来的存到citizen 里,有外界 person ID 去关联它,然后 person ID =1这个人与年龄为19岁的小孩 john, person ID =1有 passport Number,再看 name 是 mike 的这个人,同样这个是 person ID =2对应上面person ID =2,这是居民,除了姓名和年龄之外,还有扩展出来的国籍和签证类型。
第三个人就是副类的 person,其信息就只存在上方的表里,所以意思是对于类的结构,就创建三张不同的表,而且每一张表只包含类特有的东西,所以 person 就只有 person 的东西,然后citizen 就只有它扩展出来的citizen的的这个护照号,而居民就只有它自己扩展出来的国籍和签证的类型。
那么在 person 里,要添多加一列,这一列叫它区分符,要去区分一下这个人到底是citizen、resident 还是 person,这样就知道要取这个人的完整信息时,肯定是先拿着 IP 到 person 里,然后根据 type 就知道需要到哪个表里寻找其特有的东西,所以这叫 join table,是这样的一种实践方案。
这种实践方案的优势是:和数据库、类的映射方案完全一样,类的继承方案完全一样,只在数据库的角度看有什么好处?比如未来的person 除了性别和年龄以外,再增加一个性别,可以很容易实现这个逻辑。怎么实现呢?就在这张 person 表里加一个字段就可以,很容易扩展,完全不影响另外两张表。就算是父类也不会影响到底下两张表。如果要删除例如年龄,也是直接把上面删掉就行,底下的表不受影响。
缺点是:任何人想得到它完整的信息(除了 person,只是子类的),那就需要做一次连接操作,这是它的缺点。
2.table per class
第二种方式:Person 只存person,Resident 只存 resident。也就是例如像 resident,如果一个人是居民,就一定会存在 resident 这张表里存其所有信息,Citizen 也是一样。所以看到还是刚才的三个人,现在存在三张表里,每一张表里面有其完整的信息。
好处是:不用做任何的连接操作,就能获取这个人的完整信息,这就是优点。
缺点是:如果现在已知存在 price ID=3的人,现在需要知道他在哪张表里,这时必须到三张表里寻找,才能知道它的位置。比如说 ID =1,这种情况就不知道到底在 person、Resident还是在Citizen的哪张表里,可能都得去寻找才能找到;另外一个缺点,如果现在需要加入sex 这个字段,三张表都需要添加 sex 的字段。
3.single table
还有一种方式就是只存一张表,然后这张表里的字段包含父类里的所有字段,以及所有的子类扩展出来的所有的字段。然后如果是 person,只存前三个字段;如果 Citizen,存前三个字段加上自己扩展出来的字段;如果是Resident,就存前三个字段加扩展出来的两个字段。
好处是:不需要做多个表的扫描,就能找到某一个人,然后之后需要添加一列,也不太麻烦。
缺点是:如果一个人只要是公民,必须有 passport number。在前两个表里面都可以做到,需要让 passport number 这一列不能为空,可是第三种方案里就无法实现,因为对于 resident 和 person 没有passport number 这样就不能允许它为空。这里只能改变一下,例如约定一下,对于 resident 和 person 去放一个特殊的值(-1)。但这就不如前两个方便,靠数据库来保证它不为空。然后由于在这里会并入很多字段,所以它的字段的数量会远远多于前两个方案。
三种方案都有优点和缺点,所以才会存在三种方案,但这只是理论,实操时会怎样进行呢?这里举了三个例子:
第一个可以把其打开查看代码,代码很有意思,就是实际上数据库映射出来的不同,但是代码本身只是 analation 不同,其写法是一样的。例如这里有飞机,飞机定义了飞机类,然后飞机类存在 airbus,有 boeing,有 il的。
这里定义了一个飞机类,定义了一个 airbus的子类,一个 boeing 的子类。
那么 airbus 强调要装的人多,比如380要装500多,所以它有一个扩展出来的属性叫 capacity,而 airbus强调要舒适;比如说 boeing,像787梦想飞机有舒适度,这是它扩展出来的属性,而 il 的778它没什么特别的强调的东西,所以其就是普通的飞机。
所以传递数据库就是这样的结构,这里观察其写的类。首先是父类飞机类是 person,它在映射 plane 这张表,因为现在只有一张表(plane)。继承策略使用的时单表方式,也是刚才看到的第三种方式。刚刚的第三种存在一个细节被遗漏,就是它也有 type 来描述这个人的身份,那么这在映射的就叫做 discriminator,也就是区分的一列,区分符的那一列对应的叫做 disc,在数据库里也存在。它的取值是string 类型。
那其区分这里看前面的区分是 citizen、resident 还是 person,每一种类型可能都需要附一个类,所以在这如果就这一列的值就是0.,强制要求其必须存在。
这就是 il 飞机这一列为0,plane 本身比较简单,有 ID、type、 manufacturer,因为 ID 是自动的,然后对旁边两个存在 get that。本身代码并不复杂,复杂在前面 attention 这里。
再看Airbus,airbus 也是实体类,那其扩展 plane,然后其区分这一列的值就是1,这就是刚才看到的 Airbus 类,然后其强调的cape city,所以其扩展出来了 cape city 的东西,最后就get set,再就没有任何其他的东西了。
要注意,所有的类必须要有无参数的播报器,然后才能去用,跟它类似的内容就是播音,播音在这一列取值是2,然后就结束。
这是就是映射方案。然后编写 webservlet,并且在 plane 这个地方等着,后面提到 manufacturer 是前端输入的参数,把参数取出来,然后在 hibernate 上开 construction,然后创建一个新的 Airbus对象,设置进去,airbus.settype 是父类带的属性,airbus.set capacit个属性是它自己的扩展的属性,将这些进行存储;同样再创建一个播音类型的对象,前面是父类的,后面是其自己带的属性,然后也进行存储;再创建 il,也就是普通的飞机,这是manufacturer和 type 是78算进去,因为这就是普通飞机,也没有其他途径,所以也进行存储。
三个都存储完毕,然后这时就会存入到上面的表里,存成刚才的数据。后面执行 query:从飞机(plane)的类里面去找飞机的 manufacture就是输入的参数,这个参数设置进去,也就是刚才从前端来的参数。
如果输入它是 il,那就等于 il,如果输入是 Airbus,那就是 Airbus,如果输入是 being,那它就为 being,但问题是 Airbus 和 being 都是 plane 的子类,但在这个语句可以查询出来,因为虽然这里是子类,但也是父类类型的对象。
所以如果输入 airbus 或者 being,就可以把相应的 Airbus 或者 being 的飞机找出来,这是在这个类以及它所有的子类里操作,去查 manufacture 输入的参数,这个例子可以看一下。
同样的道理这里把映射也就是 circle 的脚本也发送给大家了,可以在本地跑出来。
这是刚才写的几个类:这是 plane 和之前写的代码,和上节课显示的 person Event 没有什么差异,也就是 Java 代码本身没什么差异,差异主要在 entity、inheritance,还有discriminator 的相应处理。
这是播音的,看到 discrimination 的取值是2,也就是代码里写的有点差异,然后这是播音的值为2,然后是空客,之后这的值为1。然后看到的这是 Soviet,其访问创建一个 Airbus 类型、being 类型、plane 类型,当在进行存储时,这是三种不同类型的对象都存到了 planes 的那张表里,当要去获取这个对象时,是在 plane 以及它所有的子类里进行搜索,所以这就是其比较有趣的地方。
就是一旦存在错误可以协助自动纠错,执行完成之后可以选择这三个东西如下图所示。
运行 Airbus,点击 query,就会找出所有 Airbus 的东西。如果要寻找的是 il 的,那它就会把所有的 il 飞机找出来,这是因为这里插入了很多遍,所以会找好几个。这个例子的神奇之处在于它有读写,也就是在搜索时,其搜索的是 plane 以及它所有的子类。所以这三个都可以搜出来,在这三个的范围里搜,如果写的三个不同的类的对象,那储存的时候在调 save 的时候,都存入了 plane 这一张表,这是第一种策略。另外一种策略,下节课再讲。这里可以交流一下不懂的问题。
这里提出问题是 Dao 的作用,这里有 event,Dao 是在说对其做增删改差的动作。然后为什么从这个 JPA 里获取 repository 能对它做增删改差,为什么还需要 Dao?
如果是 event 的属性不全是从 repository 中得到的,那不需要在 Dao 里面做组装吗?在Dao 这一层无论是person、repository还是person icon repository 只能处理一部分数据,但是这两部分数据合起来才是一个完整的人,那这个逻辑就应该在到这一层去处理。
否则没有其他地方可以处理这个东西,如果组装出完整的部分,因为它的数据一部分在关系数据库里,一部分在 mango DB 里,靠系统直接生成一个,然后从两个里去拿是无法完成的,只能自己去实现此逻辑。
很多人已经从这里直接继承了,所以存在很多的方法,就算没有也可以在里面添加一些。但是它存在前提是它是在操作关系性数据库,这里还有一部分数据是在非关系性数据库里,所以必须是在 repository 外面写,如果从 JPA 这个 repository 里得到的,在这里再去写非关系的数据库就显得很累赘,至少职责方面分配不清楚。而这两边各自能处理一部分的逻辑、数据,它处理不了完整的数据,所以需要在到这一层来做汇聚。