开发者学堂课程【高校精品课-上海交通大学 -互联网应用开发技术:JAP4】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/76/detail/15757
JAP4
内容介绍:
一、处理上节课的疑问
二、标识符
三、标识符生成器
四、References
五、一组相互关联的数据库对象
六、存储
一、处理上节课的疑问
上节课提到了解耦,从最前面的例子可以看到通过Column对EVENT和DATE做了解耦,title也是一样的加上Column就可以解耦。第二个问题是问什么一端用的集合类是set,而另外一端用的是list,实际上从名字可以看出这是集合类,Java里的集合类有很多种,只要是集合类都可以,都用list或者是都用set都可以,用其的也可以只要是集合类都行。
Java的集合类是比较复杂的话题不展开去讲,总的来说使用的某一种集合就可以不一定必须是list或者是set,两边都写成一样的也可以,只是给出的例子中碰巧是两种不一样的。只是存储的方式不同,list是像数组一样存放,set是用哈希表的方式存储,只是存储的方式不同,都实现了扩展自collection抽象类,都可以使用没有规定必须使用哪一种。
主键比较重要,比较简单的是有Entity、annotation、id,复杂的是之前看到的有一个表Table annotation,后面的是说TBL_FLIGHT映射哪张表,这是之前看到的,schema=AIR_COMMAND是在映射哪个数据库,如果安装了MySQL打开里面的一个客户端里面就会显示要选择一个schema数据库,下面是uniqueConstraints唯一约束会告知对应的是什么名字,对应的列是什么,
即{”comp_prefix”,”flight_number})),意思是在表里面比如主键的名字的唯一性或者是不是主键的列上约束columnNames的取值是唯一的,除了主键其的是不能约束是唯一的,如果想要加约束不能保证自动是唯一的,可以在上面加约束。
可以加Table表来描述在做类的时候的一些细节,即使是写的schema=AIR_COMMAND,uniqueConstraints=@UniqueConstraint(name=flight_number”,{”comp_prefix”,”flight_number}))也是不全的,需要查Table,table里面的东西非常多有的东西可能会用不到。
总的来说想要控制映射的细节可以查table相关的描述,同样如果想要在列上去查可以加column对应的是数据库中的哪个列,可以看到number对应的是flight_number,comp相关线对应的是compagnyprefix实现了解耦,顺便回答了title能否解耦的问题,加了table和column的目的是告知hibernate映射了详细的控制即如何去映射,而且table和column两个标签可以让类和表之间完全解耦。
有一个解耦可能没有意识到即直接在Java代码里写SQL会有什么问题,上节课提到过会有方言的问题,不同的数据库写出来的SQL可能会不一样,但是现在在写实体类在进行操作时数据库和SQL都看不到,方言是需要靠hibernate解决。
所以在hibernate的配置文件中在写方言是什么,会将对对象的操作写成方言之后发给数据库。
暗含的是与数据库管理系统解耦不管后面是MySQL还是orrecall不需要去管,原因是hibernate会帮助翻译成方言SQL的语句,现在是全部面向对象。
如果现在不与数据库管理系统绑定也就是不和URL绑定即具体和哪个库是不绑定的,类和库和表的结构、位置、类型等都绑定,所以解耦是比较好的,所有的事情靠annotation以声明的方式在做,但是public class Flight implements Serializable等代码是不需要改的,改动量集中在几个annotation里面,变小代码的复制性就会提高,这是or映射的一个精髓。
二、标识符
ID最常用的是之前讲的例子中看到的。Id不想管,如果id是自己维护非常麻烦,不能保证写进去的id是唯一的,如果交给数据库数据库通过递增主键帮忙生成是很好的,整出型的递增主键是数据库自动生成主键的策略之一,也可以不使用这种办法,但是不管怎样数据库中提供了多种策略选择一种就可以,不管哪种只需要选定在person里面人为设置id是什么,当将其属性都设置成功,存储之后在数据库中自动生成id可以在插入到数据库之后通过getid的方法将自动个生成的主键取出,所以主键一般会用自动生成的策略。主键碰到了组合主键,组合主键不可避免比如前面看到的四张表中组合主键不可避免,首先PERSON_EVENT是一个组合主键,其次PERSON_EMAIL也是一个组合主键,从数据库的设计角度来看组合主键很难避免。
如果在PERSON_EVENT中多加一个ID作为主键,EVENTS_ID和PERSON_ID都是外键也可以。
这里涉及数据库的冗余度问题即存储的数据冗余性是多少,数据库符合第几范式即数据库中冗余的数据要少一些。冗余数据是什么,举例如果要存person的email,一个人的email有很多,可以设计一个表格每一列分别为id、email的id、姓名、年龄以及email,如果一个人有三条email则在表中有三条记录。Id都是一,email分别为1、2、3,名字为Tom,年龄是10岁,email分别为1@...、2@...、3@...。这样存储可以发现是数据库的一种存储方式,但是姓名和年龄两列是冗余数据。在进行数据库设计时有这样的方案,这时数据库会带冗余,根据冗余的数量多少或者是性质可以发现遵循什么样的范式。可以想想组合主键好不好是不可避免,假设需要用到组合主键,比如一个人的id想用first name和last name名和姓表示,作为唯一标识一个人的主键,即在user里面想要将first name和last name单独提取定义userid的类,在user类里面有一个userid类型的属性叫做id和一个integer age。
即有一张表表中有三个字段,分别为first name、age、last name,想要将first name和last name取出映射成userid的类,user里面包含userid id和integer age,userid加一个Embeddedld叫做嵌入式id,下面加Embeddable是一个可嵌入的,Embeddedld和Embeddable的写法有一些区别表达的含义不同。Userid里面的first对应的是数据库表里面的fld_firstname,即在表格中叫做fld_firstname。可以发现一张表映射出了两个类,但是Embeddable不能单独用,单独用没有什么用处,是完整的user的一部分,但是Embeddable可以被复用,假设之前设的表格表示的是老师,再设一张表表示学生,学生的表中有一个字段表示学生的surname,后面的字段表示family name、学号、成绩等等。
如果是用surname和family name定义作为主键,这时可以定义student的类里面嵌入userid,同样用AttributeOverride说明,Userid里面的first对应的是surname一列,userid的嵌入式主键类可能被多个类复用,是一种比较灵活的方式。现在做的系统可能不太复杂,但是也可以进行尝试。
Userid主键类就可以得到复用,只有firstname和lastname只是显得弱,但是如果将set first name进行一些处理first name必须为全字母,必须首字母大写其的不允许插入,user就会有用。从中可以看出类的设计原则是复用,主键写的不错在user、student、teacher中都可以用,单独将Embeddable的代码抽取出来,在不同的环境中用的时候对应的列的代码不一样需要使用column=@column(name=“fld_firstname”)方法映射,这样就没有重复代码。在组合主键是往下面推的更深一层会发现如果碰到组合主键,可以扣出来专门变为一个可嵌入的主键,主键类专门应用。在这里便又又回到了之前提到的一个大坑即对象和关系之间的不匹配。
可以发现user的两个类之间也是不匹配的,但是在主动维持,将一张user表映射成了两个类,其中一个类是为了将来做复用,完全是在面向对象编程的方式考虑问题,所以与关系数据库不太一样。
刚才是一种方案,userid应用的好处是userid可以被当做一个属性嵌入一个更复杂的id,可以看到customerid里面有userid和String customerNumber,于是在Customer中有customerid还是Embeddedid,相当于将userid嵌入了一个customerid,扩展了属性整个作为customer的id,列不同可以设立,比如在customerid里面的属性userid里面的first name和last name,Customer表中的名字不一样仍然可以进行处理,或者将例子更为复杂一些,有一个user的表还有一个Customer的表,user表中是first name和last name等东西,Customer表中会引用user同时会引用出来Customerid,在映射的时候映射出Customer类里面嵌了一个userid,因为只存扩展的Customerid,有关user的属性存在user里。一个Customer完整的信息是自己的属性加上关联过来的user的属性,两部分合起来是一个完整的Customer,Customer与user是一对一的关系,Customerid有一部分来自user,userid映射到user表格中列的名字不是first name和last name。Customer里面有first name和last name映射user的first name和last name,但是名字不太一样在Customer中叫userfirstname_fk和userlastname_fk进行映射,这样会显得更复杂,也许现在会用不上。
Customer除了像之前的映射方法之外,还可以进行映射user,写法与之前不同,主键一部分来自user里面的first name和last name,外键与user进行一对一关联。
还有一部分是自己扩展的customerNumber,例子比较复杂可以不关注细节。当使用组合主键的时候会发现,组合主键在一个类里面可能是多个属性,于是在多个属性上都可以加id,hibernate便会知道两个属性上都加了id,两个属性合起来是当前的主键,这便是customerid的一种写法,与之前的写法不同,在新的写法中有customer number和user,二者合起来用的是主键类。
主键类有三种写法,实现的方式不同,总的来说是要用组合主键表示数据,可以发现组合主键比较麻烦,所以建议尽量回避组合主键的问题,迫不得已需要去用的时候再去使用,否则在前面四张表的例子中单独加一列做主键是基于数据库递增的是可以的,person作为外键,这样做映射会简单一些不存在提到的复杂的情况。
三、标识符生成器
主键需要交给数据库生成,基于数据库自增键的存储过程生成,除此之外还有其的策略,hibernate里面会带几个策略,也可以不选之前看到的increament,但是这种细节会涉及到数据库里面的事件因此不会详细讲,
会生成GeneratedValue(Strategy=GenerationType.IDENTITY的策略。如果站在MySQL的角度认为数据库的主键怎样自动帮用户生成,基于整数自增的事情是如何实现的。在运行刚刚给出的例子在工程中运行起来,可能会注意到里面的例子递增到了68就是event,但其实删除过表中的记录,将表中的记录删除没有从1来即使将68前面的都删除,也是从69开始,站在数据库中的角度,一个数据库中的表帮自动生成键时,会有人说扫描event表看里面最大的主键之后在的基础上加一,这种是可以的但是会显得效率比较低,第二不去扫描所有的主键count看一下有多少条记录再加一是不可以的,比如插入的是1、2、3,count3结果将1删除,count2插入3会与之前的3重复。接下来讲一个思路,虽然不是所有的都按照此思路设计,但基本原理上是一样的。
设计一张表,这张表是用户不能操作的是系统的一张表,在里面存两个字段一个是tablename一个是可用的id,比如tablename下面是events,可用的id下面存68,当有用户要插入event时去查找设计的表格中的记录,可以知道主键应该变为69,表格中会记住能用的主键是谁,所以将68删掉信息没有办法记录,只会记录当前可用的下一个主键是谁。主键在不断递增删除的是回不来的,这是一种递增的主键。
UUID是是一个32位的16进制的数,使用的原因是可以保证多线程写入数据库的时候唯一的,32位是IP加端口和JVM即写数据库进程的id再加上时间和随机数。
IP加端口是一段,JVM是一段,时间是一段,随机数是一段,共四段,8位随机数,除非在同一个Java虚拟机里面同一个时间内生成了两个随机数相同的对象,否则绝对是不一样的。
UUID的好处是可以保证主键绝对为一,缺点是很长检索能力比较低。还有其的生成策略,基于整数和UUID是最主要的两个,所以在数据库中可以看到选择哪个,通常情况下会选基于整数的方式,原因是UUID方法太长,再讲的例子中不会有很多数据,太长进行索引会浪费空间浪费效率,所以都是基于整数递增。
除非碰到复杂应用,会有很多人写数据库防止出现主键重合的情况,可以使用UUID策略。但是抛开具体的策略来看实际上hibernate想要告知主键的生成是比较复杂的事情想要通过Java代码进行处理是不太现实的,或者是不明智的,应该靠数据库的帮助操作,提供的策略会告知不需要管主键,在生成对象的时候直接将其的属性填好塞进去数据库帮助生成主键,所以会有不同的策略。
四、References
接下来是看到的hibernate的基本的样子,通过person和event两个类互相之间的关联,四张表的映射在告知hibernate的基本用法,,但是现在所有的操作都集中在mapping上告知如何做映射,讲清楚了对象映射需要注意的一些问题还有一些问题没有讲到所以需要继续讲,这是关于hibernate自身的用法。
五、一组相互关联的数据库对象
接下来学习在spring中是如何做的,用spring实现和之前的event和person之间类似的代码,学习Spring的JPA是如何实现的和hibernate是类似的,在未来进行大作业时可能在Spring的JPA和hibernate当中做一个选择,但这两个也不是唯一选择,如果之前用过ibatis或者是mybatis也可以选择这两个,基本道理是一致的。
接下来看spring的JPA是如何做映射的,还是之前的例子四个表格,结构就不再去说,要去用两个类,spring的配置有很多种方式,也可以写config类,让spring加载。
也可以在application.properties写的内容和
hibernate.config.sml里面的内容类似,需要连哪个数据库、用户名是什么、密码是什么以及驱动类是什么,大体上和hibernate的内容是一样的,这些东西都在告诉spring数据库在哪里应该怎样连。
底下要去看event类和person类是如何映射的,大部分内容都很像比如前面要有一个entity,一个table映射的是哪张表、映射的哪一个库,再下面的JsonlgnoreProperties和Jsonldentitylnfo,P1参加了event1和event2,event1里还有p2和p3,会形成循环依赖不断地加载,JsonlgnoreProperties和Jsonldentitylnfo合起来去解决问题,如果加载participants要是用lazy的方式并且打破循环加载。JsonlgnoreProperties和Jsonldentitylnfo两个标签第一个是value={“hander”,“hibernatelazyinitializer”,fieldHandler})是没有问题的,第二个是说eventld的主键类即主键的生成策略是谁,底下有eventid、title、eventDate,故意写的都不一样,Eventld对应的数据库表里的eventID,主键生成策略用的是IDENTITY,有好几种策略,所以就换了一个叫做IDENTITY,title对的是title,Event_DATA还是对的表里的Event_DATA,仍然是用这种类似的方法在做映射,用的是Column,把列和属性结偶,用 title把类和表结偶,之后ld的写法和hibernate基本上是一样的,里面多了一个Basic。equals和hashCode两个和映射本身没关系,是因为类希望以内容来比较两个对象是否相等,只要是两个对象内容而不是引用的相等,都要覆盖这两个方法。
底下有一个参加的人list<person>participants,里面仍然是Many To Many,因为标签不是hibernate专用的,也不是spring专用的,而是在企业版里面规定的。
底下是一样的即关联表是谁,关联的列是谁,反过来列是谁,这些实际上是Java企业版里面的,所以写法和hibernate相同,原因是用的是相同的标签,所以类总的来说,SpringJPA的时候和hibernate没有什么太大差异,引用的也都是企业版里面默认的标签,只是多了一点东西,就是spring自己的东西JsonlgnoreProperties以及底下Jsonldentityinto,加的目的就是防止循环依赖,底下Person也是一样,加的这两个东西,其的也没什么区别,跟hibernate也是一样,也是Many To Many,mappedBy=participants映射成什么是一模一样的。
不太一样的是底下的Emails,可以看到写法上也基本一样,但是要注意的是,getemails写的仍然是集合类集合表,personid对应的是要在表里找Personld就是当前的ld,唯一要注意的就是PERSON_ID和之前写的不太一样,是因为在Entities:person类里面取的名字叫personid,仔细看跟刚才的写法没有什么本质差异,所以在springJPA里面和mappedBy里面,需要注意没有什么本质差异,写的全部是在用java的企业管理定义的annotation来定义如何映射,唯一的是JsonlgnoreProperties和Jsonldentitylnfo是spring自己的,要打破循环依赖,而hibernate在这方面做的比较好,不需要定义,自己会停止循环依赖,循环加载的情况,spring到此为止映射的方式是一模一样,没有任何区别,把四张表映射成两个类就是这样的效果。
六、存储
Spring再往上有一个叫做JpaRepository的东西,注意大括号里什么都没有写,但是还是需要定义一下,定义好之后spring会帮助生成自动生成实现两个接口的一个类,就可以拿来去用,需要类因为在JpaRepository里面定义了很多方法,即定义好EventRepository之后将来就可以在创建的类上面spring自动帮助做类类实现两个接口,在实现的类里面可以调用findAll找到所有的Event,或者是person类的对象。
get one传递主键值会自动帮助实现,所以EventRepository和PersonRepository必须写,在里面不需要写任何代码,spring会帮助生成两个实现两个接口的类,分别针对两个接口帮助实现EventRepository和PersonRepository的类。
在接口里面要注意的要写类的名字一个是Event,一个是针对Person来操做,后面是主键类,主键必须写类而不是基本类型,写int是不行的,必须要写Integer是类,写好之后按着写,没有提供任何事件,spring帮助实现,一旦实现,就可以拿来用,帮助实现的类不知道叫什么如何用,写了一个接口public interface EventDao{Event find One(integer id)}完全是自己定义的叫做EventDao即数据访问对象,定义一个叫做findOne的方法,findOne的方法在传递进去一个Integer类型的值之后,组键之后会返回一个Event,EventDao会写一个实现类去实现,实现的时候会去说清楚两个东西,第一个里面会用到定义的EventRepository,需要注意EventRepository只是一个接口,spring会帮助实现一个类,类名不知道没有关系,要定义一个EventRepository类型的实例出来,之后写一个Autowired。spring在加载类的时候,会自动去找接口的实现类,接口只有一个实现类,就是spring帮助实现类,所以spring会帮助实现接口的类直接创建一个对象赋给EventRepository,这叫做注入。
所以逻辑是只写了一个接口,要求创建一个对象,对象是spring创建的一个类的对象,类不知道叫什么,但一定是实现了接口的类,所以直接生成EventRepository,即接口有一个实现类,创建一个实现类的对象,可以当接口用,而类是spring在读到了Autowired的annotation之后,spring会把自己创建的默认类创建一个对象赋给EventRepository,一旦赋给EventRepository,接口里有很多种方法,所以就直接可以调用刚才的方法,比如getOne,findone在调用EventRepository的getone,于是便会拿到。
与EventDao类似的PERSON也是这样实现的,所以从来没有写过getOne是如何得到的,所有的逻辑都没写,只是定义了一个接口,接口本身有方法。而spring会帮助实现getOne的具体逻辑,findone就是这样实现的。,findone调用了EventRepository的getone。
接下来再有一层,叫做EventService定义了一个叫做findEventById的东西,具体实现类是service层,所以用了service来描述,与之前的道理一样,针对接口编程需要一个EventDAO类型的对象可以看到interface是一个接口,EventDaolmpl是实现类,接口不能创建出对象,同样的道理,如果写了Autowired,spring一旦读到Autowired,会在当前的项目里去找,实现了EventServicelmpl的类,有一定的规则,规则以后有时间再去讲。
当前只能找到实现类,类创建了一个对象赋给了EventDao,所以EventDao实际上不是一个接口对象,接口创建不出对象,是实现了一个接口类的类的对象。
于是便可以调用上面的findOne方法,就是看到的Dao里面findOne方法,底下的 Personservice也是这样编程,在最前面有一个响应EventController类的,起个名字叫EventController,同样的道理,需要EventServicelmpl,就是EventService接口类,Autowired以后,自动帮助找EventService实现类,于是就找到了类,创建一个对象赋给Service,Service就可以用相应的方法,RestController是是返回的对象,会转成一个JSON字符串往前传,JSON在讲前后端通信的时候会详细讲,把对象转成一个字符串传给前段,这是RestController,就是Spring RestController的作用,GetMapping是前段的请求,如果value是这样写的,也就是这种由底下来处理,和之前说的Servlet道理是一样的,在监听某一个东西,后面就用大括号括起来后面可以是一个变量,可以是1.2.3.4都可以,在这里面看到把id路径里变量就可以给取出来,这就是用户输入的变量。然后就按照主件,去到后面,去调Service来查看,这就是完整的工程,再从项目里面去写,比如说findEvent1,加载了Event的一些东西,findPerson1就加载了Person的信息,这是代码,接下来看一下例子,例子是用spring来写的,写好之后,
是loadpost8080findEvent/1,可以看到1事件被加载进来,1事件被加载进来以后,1后面为什么有很多,因为1有一些参加的人,参加的人会有几个,除了每一个人,又有一个参加的活动,可以看到要循环,循环到一定程度就结束没有继续循环下去,如果把之前的两个删掉,JSAN的两个删掉就会有问题,locahost8080/findPerson/1就找的是人,另一边找的是事件,但是不论是找事件还是找人,都有循环加载到一定深度之后停止,就是例子,只是把代码讲了一遍,接下来还要继续进行