持久化:ORM:引入和功能简介:自动建表 / 自定义&继承映射 / 关联对象增删查

更多
2021年09月19日 09点37分 作者:叶飞 修改
理论上,学会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的首字母简写,对象关系数据库映射(工具),至少要能够实现上述两个基本功能。

随着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
飞哥

在ORM中,我们通常也根据主表/从表的关系,把他们对应的类也称之为父类/子类要把它们和继承相混淆。所以按上述表结构,Teacher和Student是一对多(也可能是一对一)的关系,Teacher是父类,Student是子类。

新增,删除和加载

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

  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


作业

新建一个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. 分别使用entityManager和session/statelessSession,用常规方式和性能优化方式完成:
    1. 插入若干User对象
    2. 通过主键Id找到其中一个User对象
    3. 修改该User对象的Name属性,将其同步到数据库
    4. 删除该Id用户
    5. 用一个数组存储若干Id,遍历该数组,不加载对象,直接删除所有Id对应的User
  4. 根据一起帮的功能构建相应的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(用户资料)

DbHelper SQL生成器 ORM
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

数据库


SQL Server

建库建表、增删改查、索引并发、函数和存储过程等……

NoSql

mysql

数据库和SQL

公用的数据库介绍、SQL语法、其他语言对数据库的连接操作等

持久化

后端语言操作数据库,以及ORM的基础概念等

ADO&EF
如何通过C#进行数据库的读取,包含ADO.NET和Entity Framework相关知识……
JDBC&Hibernate
Java连接数据库操作,包括JDBC、Hibernate和mybatis等
全部
关键字



帮助

反馈