学编程,来源栈;先学习,再交钱
当前系列: 持久化 修改讲义
理论上,学会JDBC/ADO.NET(和学完函数一样)就可以收工了……但是,


两大问题

面向对象

@想一想@:这样(面向数据库)开发舒服不舒服?

比如一次查询,你给我一个结果集,不看数据库我就不知道里面装的啥……能不能直接给我一个对象啊?对比:

//直接拿到Student对象
Student student = Student.getBy(98);
System.out.println(student.getName());

//拿到一个结果集
ResultSet student = Student.getBy(98);
System.out.println(student.getString("Name"));

从数据库取到的数据不会自动变成对象。所以,早期很多项目我们都要做这样的封装:

ResultSet rs = DriverManager.getConnection("").createStatement().executeQuery("SELECT *");

Student student = new Student();
student.name = rs.getString(1);
student.isMale = rs.getBoolean("isMale");

return student;

这样逐个逐个的手写代码的工作即乏味无趣又很容易出错。

写SQL语句好烦的

略,都懂,^_^


曙光:映射关系

我们发现,因为对象和数据库两者之间可以简单映射:

  • 一个类就对应一张表,类名可以对应表名
  • 一个属性对应一列,属性名可以对应列名
  • 一个对象就对应表里面的一行
  • ……
程序员就开始思考:能不能开发一种工具来完成这些工作呢?
  • 根据给定的对象生成相应的增删改查SQL语句。
  • 将一行一行的数据转变成一个一个的对象呢?

@想一想@:你能开发这样的工具么?用什么技术?

关键的关键,在于动态的(在运行时):

  • 根据对象获取属性名/值,如果是新建/更新/删除的话
  • 根据类名生成对象,如果是查询的话

靠什么?当然就是反射


ORM横空出世

Object Relationship-Database Map的首字母简写,对象关系数据库映射(工具),至少要能够实现上述两个基本功能(生成SQL语句&对象

随着ORM的发展和普及,开发人员提出了更多的要求和期望,实现的有:


自动建库建表

能够根据Java和C#的类(entity)生成数据库表结构,反之亦然

谁先谁后?

  • Code first:先有entity,然后再映射生成数据库和表
  • Db first:先有数据库(和表),然后再映射生成Java和C#的类
  • mixed:上述两者混合使用
越新的项目越倾向于Code first,这是OO的要求,是DDD的要求:忘记数据库……


自定义映射

大多数情况下,类名和表名,属性名和列名,都是一一对应,都可以使用默认配置。但难免有一些非主流的要求,比如:

  • 需要指定更具体的类型,比如Java/C#都只有一个String类型,但数据库中表示字符串的类型有varchar、nvarchar、text……
  • 某类/列名不能做表名,要改
  • 某表没有Id,要使用联合主键
  • 列上要添加约束:非空/唯一/自定义CHECK
  • ……

需要自定义的映射配置。早期用XML文件,现在一般用特性/注释或Lamda表达式

继承和多态

强烈建议先按暂停,自己想想,^_^。关键是要能够单独取父类对象哟。

常见的策略有:

  • TPH(Table-Per-Hierarchy):子类和父类共用一张表。使用一个额外的discriminator列(值可以为类名)来标识具体类型

  • TPT(Table-Per-Type):父类和子类都各有各的表。Id由父类表管理,子类表使用父类表Id

  • TPC(Table-Per-ConcreteType):父类没有表,只有子类有表。其关键是如何协调不同子类之间Id的生成

#常见面试题:该如何选择继承映射的策略呢?#

  1. TPC:一般不考虑,两个明显问题:
    • Id生成策略不好弄,一般不能自增,只能GUID
    • 不能直接存父类对象
    • 取一个父类对象的时候也很麻烦(要多表UNION)
  2. TPT:
    • 取子类对象的时候需要父表子表的JOIN
    • 插入子类对象的时候需要(先)父表(后)子表两表操作
  3. TPH:多一个discriminator列,还有可能列数较多


会话/状态管理

再次复习ORM的终极目的:忘掉数据库,只是持久化对象!

状态追踪

可以把ORM看成是Repository(复习)。我们的使用,就三个步骤:

  1. 取:获取单个或多个对象,又被称之为加载(load)
  2. 改:修改对象中的数据(字段/属性)
  3. 存:把修改过后的对象保存/持久化起来,又被称之为同步(sync)

@想一想@:第3步如何实现?同步不仅仅是UPDATE哟,还可能是INSERT(1是new出来的对象)和DELETE(2改的时候进行了删除)……

关键要知道加载到内存里的entity对象,究竟发生了什么变化。

怎么才能知道?按官方的说法,是记录entity的状态并进行追踪(track)。

状态一般包括:

  • 没改:unchanged
  • 改了:changed
  • 删了:removed
  • 新加进来的:added
  • ……

有了状态,最后同步的时候就依据状态生成相应的Update/Delete/Insert语句。

PS:你可能好奇怎么追踪管理,^_^,方式多种多样,但最简单的就是比对:entity加载时就做一个快照(snapshot),持久化前再依次进行比较

但谁来做这事呢?不同的ORM有不同的称呼:

  • Hibernate:session(本章节采用)或entityManager
  • EntityFramework:context

session

session一般翻译成会话,它是一个对象,首先能够封装和数据库的连接,能完成数据的存取操作。

还能够追踪/管理它所管辖的entity的状态(state)。

一个完整的会话:

  • 始于:和数据库建立连接,生成entity(可以断开连接,也可以不断)
  • 终于:完成entity的同步,断开数据库连接
所以,并是说一个会话只有一次数据库连接

状态追踪(track),贯穿session的整个生命周期。

detached

如果一个本来是从数据库加载出来的entity脱离了session的控制/追踪,我们就称其为detached。为什么需要这么个状态呢?

假设某个session里(和快照相比)多了一个entity:

  • 如果是新的(new出来的)entity,不会有id,id一般是数据库生成的:直接进行INSERT操作
  • 如果这个entity有id,说明它是从数据库里面来到,但不是当前session加载出来的,咋办?需要特殊标记特殊处理。

妙用:绕过load直接update。

前提:detached的entity还可以再attach到session中,受session管理追踪。

有时候我们可能希望能够直接删除/更改某个entity,常规的做法是先根据id将其load出来,然后再更改其属性或删除等,这样的话load就显得浪费。

如果是按上述逻辑定状态的ORM,我们就可以直接new一个entity,只给它的id赋值,再将其attach到某个session上,然后再进行预期的sync操作,这样就只有一次的SQL操作哟!^_^

Unit of Work

上述操作,其实就是UoW(Unit Of Work,工作单元)的实现,只差最后一点点:

利用事务保证一个session里面所有的entity改动,作为一个整体同步到数据库,保证数据完整性和一致性。

ORM基本上都有对事务的支持,但和JDBC一样,实现依赖的是数据库,而且需要我们显式的开启,即:同步本身没有事务的效果。


关联

实际开发中,entity对象之间存在着大量的、链式的、复杂的引用关系(复习:ER模型

entity的引用关系映射到数据库,就是外键关系(复习)

比如Student关联(包含/引用/依赖)了Teacher,

public class Student {
	public Teacher Teacher;

表结构就这样的:

Id
Name
TeacherId
28
atai
1

Id
Name
1
飞哥

名称解释

我们需要事先统一/了解一些术语,以便接下来的学习。

描述entity之间的关系:

  • Parent/Principle(父类/主类):包含了子类/依赖类的的entity
  • Child/Dependent(子类/依赖类):包含在(父类/主类)的entity

注意要把这里的父类/子类和继承关系相混淆。

所以上述代码,Student是父类/主类,Teacher是子类/依赖类。

描述Entity类的内部成员:

  • Property(属性):简单类型,比如:
    int Age {get; set; }
  • Reference/navigation(引用/关联):引用其他单个Entity,比如:
    Classroom StudyIn {get; set; }
  • Collection(集合):引用其他多个Entity的集合,比如:
    IList<Teacher> Teachers {get; set; }


简称
定义
示例(当前entity:Studnet)
关联属性

对另一个@Entity的引用
Student taughtBy
关联集合
集合
对其他entites集合的引用
Set<Student> teachers
基本属性
属性
Java基本类型+String+时间类型等
int age、String name
ValueType

对另一个@embedable的引用
Contact contact

新增

新增一个对象时,将其关联属性/集合也持久化:一般来说都没啥问题。只是在一对多关系的时候,有两种策略:

  1. 先插入子表数据(Student),此时只能将外键(TeacherId)为NULL;再插入主表数据(Teacher)获取其id,用该id更新其从表外键……
  2. 先插入父表数据获得其Id,再使用该Id存一个完整的子表数据:如无特殊情况,明显应使用这种!
Student atai = new Student();    	//INSERT Student
atai.Teacher = new Teacher("fg");	//INSERT Teacher
session.save(atai);

//@想一想@:哪一个INSERT先执行?

删除

删除一个entity的时候,

如果entity是父类,会产生级联删除(复习),需要指示:

  • 依数据库的设定,由数据库完成
  • 还是依ORM的设定,由ORM完成
Teacher fg = session.load(1);
session.remove(fg);    //fg的学生怎么办?

//1. ORM只生成并执行DELETE Teacher,Student表的处理由数据库负责
//2. ORM生成DELETE Student 或者 UPDATE Student的语句并首先执行,最后才DELETE Teacher

加载

最复杂的是关联对象的加载:(当然也可以完全不加载,就给个TeacherId,你要这个Teacher的其他属性,自己再去查……:但这样就不面向对象了,累赘,不够智能)

Teacher fg = session.load(1);
//students从哪里来?怎么来?
List<Student> students = fg.getStudents();    //null?

预先加载

Eager Load:一般使用join查询,减少数据库访问次数。又被称之为热加载
//这时候就一次性的查询Teacher和Student两张表
//SELECT * FROM Teacher JOIN Student ON……
Teacher fg = session.load(1);

但很多时候,entity之间的关联特别复杂:

  1. 层次特别深
  2. 而且有集合关联

如果说这些关联对象都要一次性的取出,很容易在session中加载大量entity,耗尽系统内存资源。关键是,这些关联对象有可能还用不到。

所以通常都需要具体的指定要加载哪些关联对象出来:

Teacher fg = session.load(1)
	//这样指明只加载Students,其他关联entity不加载
	.include(t->t.getStudents());	

但是,这种方式的问题是:适用性不够。比如我们将其封装成一个方法:

static Teacher getById() {
	return session.load(1)
			.include(t->t.getStudents());		
}

这个方法就定死了只能加载出Students,其他关联entity没有加载。但方法是要被到处调用的:

Teacher fg = getById(1);      
  • 只需要当前entity的基本属性,比如fg.Name,不用getStudents():之前的include()浪费
  • 还需要另外的关联entity,比如fg.getMajor():没有事先被include,null值,ʅ(‾◡◝)ʃ

显式加载

Explicit Load:取得entity之后

Teacher fg = session.load(1)
//SELECT * FROM Teacher WHERE id = 1

再就entity需要关联的对象进行声明加载

session.include(fg, f->f.getStudents());
//SELECT * FROM Student WHERE StudentId = 1

这样解决了eager load的适用性问题,但会产生多次查询,而且显得麻烦……

延迟加载

deferred load,又被称之为惰性/懒惰加载(lazy load)。

应用开发人员(ORM使用者)可以假定 所有的关联对象都是已经load出来的(但实际上并没有),然而一旦我们要使用某个关联entity的时候,ORM会自动的查询数据库获取数据,填充该关联entity。神奇不?^_^

Lazyload通常是通过proxy模式实现的:

  • 用一个单独的SQL语句获取要加载entity对应的那一行数据,
  • 其关联entity是NULL,而是由ORM自动新建的一个子类(后缀Proxy)对象,比如StudentProxy
  • proxy对象只有主键(Id)被赋值,其他属性值为默认值(null,0,false等)
  • 但是,当开发人员试图获取关联entity的其他属性时,proxy对象会自动连接数据库,获取相关数据,予以加载

但有可能带来1+n的性能问题:

List<Student> students = session.load(1).getStudents();    //这是1
for (Student student : students) {    //这是n
	//每一次都会进行数据库查询
	System.out.println(student.getName());
}

选择

没有一个加载模式是完美的,ORM一般提供了几种加载关联数据的模式,供开发人员选择。

我个人的偏好:

  1. 以Lazyload为主:使用起来最方便
  2. Eagerload为辅:解决1+n的性能问题
  3. 极其特殊的场景使用Explicit


ValueType/Component

Entity中一些类,并不需要被映射成单独的表。

因为他们没有独立存在价值,总是“依附”于其他entity而存在。典型的比如Address,业务逻辑绝不会单独的查“某一个”地址而是查找一个用户,用户就有一个地址……

所以Address不需要主键,只需要在User表里给它几个列就可以了。

在Hibernate中被称之为Component,JPA中称之为ValueType, EF中被称之为Owned Entity……

其实质相当于,把多个普通“相关属性

public class Student : Person
{
    public string BedLocation { get; set; }
    public int BedSize { get; set; }
合并成一个类:
public class Student : Person
{
    public virtual Bed Bed { get; internal set; }

public class Bed 
{
    public string Location { get;set; }
    public int Size  { get; set; }

很多时候,可以代替1:1映射。


作业

新建一个ORM项目

  1. 新建User类,将User类映射到数据库:
    1. 将Name的长度限制为256
    2. Password可以为空
    3. CreateTime不能小于2000年1月1日
    4. 给CreateTime属性添加一个非聚集唯一索引
  2. 修改User类,重新映射,要求:
    1. 类名改为Register,但仍然能对应表User
    2. Name改成UserName,但仍然能对应列Name
    3. 添加一个属性:int FailedTry,但FailedTry不用映射到数据库中
  3. 获得以上改动的SQL脚本
  4. 用常规方式和性能优化方式完成:(Hiberante使用entityManager和session/statelessSession,EF使用DbContext
    1. 插入若干User对象
    2. 通过主键Id找到其中一个User对象
    3. 修改该User对象的Name属性,将其同步到数据库
    4. 删除该Id用户
    5. 用一个数组存储若干Id,遍历该数组,不加载对象,直接删除所有Id对应的User
  5. 根据一起帮的功能构建相应的entities/value types并自动映射生成相应的表,并插入数据。包括但不限于:

    • 继承
      1. 有一个所有entity的共有的基类BaseEntity,包含Id和CreatedTime能比所有子类继承使用,但BaseEntity本身不用相应的表结构
      2. 有一个Article、Suggest和Problem共有的Content基类,使用TPH策略映射
      3. 有一个Ad(广告)基类,其下有ArticleAd(文章下广告)和WidgetAd(侧边栏广告),使用TPC策略映射
    • 关联
      1. User和Email(Email中除了地址,还有:是否验证有效等字段)是一对一关系
      2. Arctle或Problem和Keywords是多对多关系
      3. Aticle和Comments是一对多关系
      想一想他们是应该单向还是双向,并按思考结果进行映射
    • ValueType:Profile(用户资料)
  6. 设置:
    1. 加载用户时(不)要实质性加载其email
    2. 加载文章时(不)同时一次性取出全部评论
    3. 加载关键字时(不)同时一次性取出全部文章
  7. 使用代码更改以下(单/双向)关联关系:
    1. 将某email由属于dk改为属于yfei
    2. 某文章不再属于dk而是属于yfei
    3. 将某篇文章加入/移出dk的文章列表
    4. 将某关键字加入/移出某篇文章
    5. 重新设置某文章的关键字
    注意观察这个过程中生成的SQL,想一想各有哪些是性能浪费
  8. 通过映射配置,以及硬code,完成以下功能:
    1. 保存用户时同时保存新生成(new)的email
    2. 保存文章时同时保存新生成(new)的关键字
    3. 从作者的文章列表中(必须是列表端)删除一篇文章,
      1. 同时删除其使用的关键字
      2. 但保留其评论
    4. 删除一个文章系列,保留该系列所有文章,并将其移动到另一个文章系列下。

  9. 完成以下简单单表查询,找出所有
    1. 悬赏值大于10的求助(Problem)
    2. 标题以【加急】开头的求助
    3. 同时满足1和2的求助
    4. 用户名中包含admin(不区分大小写)的用户
    5. 金额绝对值大于100的帮帮币交易记录
    6. 2020年发布的文章
    7. 按发布时间升序/降序排列显示文章
    8. 在上题(第7题)基础上分页显示,假设每页显示10篇文章,显示第3页文章
    9. 统计:
      1. 每个用户各发布了多少篇文章
      2. 用户发布的求助的最大/最小/平均悬赏值,以及最大最小之间的差额
    10. 按系列显示某用户(根据用户名得到)的文章,每个系列中又按发布时间降序排列
  10. 分别在单向/双向关联基础上,利用JOIN和Load完成以下复杂多表查询:
    1. 文章数量大于5的系列(不要同时查询出文章的作者)
    2. 用户dk的文章数量大于5的系列,以及该系列下的所有文章、文章的评论、评论的作者
    3. 关键字大于3个的文章
    4. 被使用10次以上的关键字
  11. 帮帮币交易记录中混入了一些重复数据,利用使用子查询将其删除。
  12. 封装一个方法GetMostRewards(count),利用native SQL,查出每个作者悬赏最多的三个求助
  13. 封装一个方法BpRegister(username, password, invitedBy),调用存储过程bp_user_register,完成用户注册。
学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

  1. 新建User类

    新建User类,将User类映射到数据库:

    1. 将Name的长度限制为256
    2. Password可以为空
    3. CreateTime不能小于2000年1月1日
    4. 给CreateTime属性添加一个非聚集唯一索引
觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 持久化 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码