在观看这个之前,大家请查阅前序内容。
JavaEE的渊源
JavaEE平台技术——预备知识(Web、Sevlet、Tomcat)
JavaEE平台技术——预备知识(Maven、Docker)
JavaEE平台技术——Spring和Spring Boot
MyBatis这样的一个框架,主要是用来做面向对象和面向关系的映射的。
它的起源可以追溯到后端系统设计过程中的面向对象方法。
在软件设计领域,面向对象的设计方式是经过多个阶段的演变发展的。
面向对象设计的目的不仅仅是实现已知功能,而更重要的是捕捉这些功能背后的概念和属性。通过理解这些概念,我们不仅能够完成当前已知的功能,而且能够在不违背基本模型的前提下,扩展和发展未来的功能。
因此,这一阶段是面向对象模型设计中的一个关键步骤,我们从需求分析到领域建模一直都在贯彻这一思想。在这之后,我们会进行面向对象的详细设计过程。
在面向对象的详细设计过程中,第一步是构建对象模型。这一步并非关乎功能的实现,而是从领域模型中推导出对象模型。对象模型与领域模型有着相似之处,但在构建对象模型时,我们通常会做出一些折中考虑。这包括根据具体技术要求明确对象之间的关系,定义对象的属性,以及判断属性是否应具有一定的冗余等。无论考虑了哪些因素,最终的结果是,我们在面向对象设计中处理的是对象而不是数据。对象意味着这些实体具有属性和方法,它们之间存在关联关系和继承关系。在处理对象时,我们并不是像处理数据那样孤立地获取一个个单独的数据,而是获得一系列关联的对象。
举个例子,当我们获得一个商品时,我们也能够获取与之关联的其他商品。我们获得一个商品,同时了解其当前的有效价格以及可能的促销活动。因此,对象并非孤立存在,而是相互关联的实体。
在内存中存储有关联关系的对象确实没有问题,但是当前我们面临的挑战是,我们需要处理的对象数量非常庞大。举例来说,就我们电子商城而言,仅商品一项就可能达到成千上万甚至几十万的数量。而针对每个商品的历史价格,以及每个商品可能产生的众多订单,需要乘以更大的系数。因此,在我们的系统中,要将所有对象都加载到内存中,并建立它们的关联关系供我们使用是不可能的。
唯一可行的解决办法就是将这些对象存储到外部存储器中。当然,有许多种外部存储方式可供选择。最成熟的存储方式众所周知就是关系数据库,因为它已经存在了几十年,并且在工业应用中已经得到了广泛验证。对于关系数据库的性能,我们已经非常清楚它能够实现什么,以及它的局限性。此外,关系数据库提供了强大的搜索能力。
AAAAA
关系数据库有其自身的限制。其中最主要的限制之一是必须符合关系数据库的范式定义。
在定义关系数据库时必须遵循关系数据库的范式。因为众所周知,如果不符合关系数据库范式,你的程序在运行时会出现数据问题。与对象模型相比,关系数据库的这些限制存在着明显差异。因为在对象模型中,我们从不考虑范式问题,对象模型的设计方式是根据实用性来进行设计和组织的。因此,它的设计方式与组织方式与关系数据库完全不同。
这就需要一个工具,即对象关系映射框架。该框架的作用是将我们存储在关系数据库中的数据转换成对象供我们使用。当我们使用完这些对象后,如果我们不再需要它们,我们仍然需要将它们存回数据库中。这是因为我们可能已经对它们的值、关系以及许多其他特性进行了更改,需要将这些更改存回数据库。
这就是对象关系映射工具要做的事情,它负责维护关系数据库中的数据与内存中对象模型之间的对应关系。我们需要从数据库中提取对象模型,使用完毕后将其存回数据库。在需要时可以再次提取出来。
这样的一个对象模型的一种框架,其实市面上主要就是两种:
通常来说,Hibernate是一种最常用的全自动框架。对于开发人员来说,这种全自动框架肯定非常简便,因为我们完全不必担心它在数据库方面的实现。我们所看到的仅仅是一个对象模型,我们使用的这个对象模型由框架负责完成对象模型与关系模型之间的转换。然而,这种全自动框架的副作用是,如果一切都自动完成的话,那它一定不可能是最优的结果。
对于我们的应用来说,99%的事情自动完成是没有问题的。因为大家也相信Hibernate是一个非常成熟的框架,并且被广泛使用,99%的事情在Hibernate上都可以自动完成。然而,有1%的事情自动完成并不一定是最佳的结果,而这1%可能会成为我们的瓶颈。
我们的系统就像一个木桶,它遵循着短板原则,那个最脆弱的地方往往是最频繁使用的地方。如果卡住你脖子的地方并不是经常用的话,它的危害性并不是很大,因为即便速度慢一些,由于使用频率不高,影响也不会很大。然而,如果这个瓶颈正好出现在你最常用的地方,并且你又想将其存储到关系数据库中,你就必须进行手工优化。
手工优化的结果肯定比自动处理的结果要更优。但在Hibernate中进行手工优化相对有些麻烦,因为一开始我们完全不了解它是如何映射的,一直把它当作一个对象模型来使用。突然有一天你想要进行手工优化,就必须先理解Hibernate的默认映射方式,然后通过编写原生的SQL语句来改变它的默认映射方式以进行优化。如果发现默认映射方式无法解决问题,必须修改默认映射方式的话,问题就会变得更加复杂。因为不仅仅需要修改这个特定的点,所有使用到这个地方的点都需要进行修改。
虽然Hibernate能解决99%的问题,但如果恰好碰到这1%的问题成为瓶颈,又必须解决的话,使用Hibernate就会变得有些棘手。这就是Hibernate所存在的问题。
-MyBatis是一种半自动化的框架,与Hibernate相比,它同样能够完成对象模型和关系模型之间的映射 。两者的不同之处在于,Hibernate不让你知道它的内部运作方式,而MyBatis将所有细节都公之于众,毫无保留地展示给你。你能清楚地了解它的每一个细节,无需查看相关文档或了解默认行为,因为MyBatis将所有细节都清晰地展示出来。
这种特性使得如果你不想对其进行更改,它会保持不变,因为你也不需要去修改它。然而,如果你想进行修改,你就可以轻松地进行调整,因为你清楚地知道所有的工作原理以及对其进行更改可能产生的影响。如果需要进行修改,就好像修理一台机器一样,MyBatis将所有细节都呈现在外面,这使得修理变得更加容易。
在中国,由于市场的特殊性,流量是至关重要的。无论是大公司还是小公司,都希望尽可能地获得收益。因此,对于互联网公司来说,如何以最小的资源来承载最大的流量是一个至关重要的问题。MyBatis在国内得到广泛应用,因为所有的公司都希望通过最有效的方式来获得最大的收益。对于我们来说,在MyBatis上进行优化可能会很困难,因为改动可能会变得相当复杂和繁琐。但是,考虑到流量的重要性,解决这些困难带来的收益是非常巨大的。因此,特别是在国内的互联网公司中,MyBatis是一个非常常见的框架,几乎占据了90%以上的应用。
对象模型和关系模型之所以能映射,是因为它存在着一定的相似性。
面向对象模型 | 关系模型 |
---|---|
类 | 表 |
对象 | 记录 |
属性 | 列 |
对象ID | 主键 |
关联关系 | 外键 |
继承关系 | 不支持 |
方法 | 不支持 |
面向对象的类可以派生出很多的对象,类中间定义的属性,在不同对象上面的值是不一样的,表可以塞很多记录,表的字段,每一个记录都可以是不同的值,所以你会发现类跟表,记录跟对象,对象跟记录,属性跟表的列,天然就有这样的一个对应关系,所以前三者是非常好对应到一起去的。
在面向对象和关系数据库的映射框架中,存在着一个特殊的问题:我们无法将所有对象都放入内存中。如果将所有对象都放入内存,那么用于标识对象唯一性的是什么呢?
大家都知道,在面向对象中,比较两个对象是否相等通常不是比较它们的属性是否相等,而是比较它们在内存中的地址是否相等。由于我们无法将所有对象都放入内存,因此我们如何判断一个对象和另一个对象是相等的,特别是在判断对象与数据库中的某条记录是否对应时,我们依赖于一个特定的属性,通常称为ID。
在数据库中,这个ID通常是主键。在类中,它是一个普通的属性,通常是一个长整型属性。我们使用这个属性来标识面向对象模型中的一个对象和数据库中的一条记录之间的对应关系。由于对象并不全部在内存中,当我们需要一个对象时,我们会告诉OR Mapping框架使用ID来获取它。如果我们对从面向对象模型中获取的某个对象进行操作,修改它的属性或关系,最终我们需要将这个对象存入数据库中。由于无法让对象永远待在内存中(因为内存容量有限),我们需要使用ID来确定该对象对应数据库中的哪条记录。
因此,在面向对象模型中,为了实现映射,我们引入了一个特别的属性,称为ID长整型。我们在数据库中为该属性定义了一个名为ID的字段,也是一个长整型。在关系数据库中,主键具有特殊的含义,因为整个表中的数据都是按照主键进行存储的。通过主键,可以快速定位到存储的数据。因此,使用主键来访问数据库中的记录是最快的方式,没有比它更快的方式了。因此,我们使用主键作为条件来查找数据库中的记录。我们将对象中的属性作为唯一标识对象的方式,不再是比较它们的内存地址是否相同,而是比较它们的属性ID是否相同。如果属性ID相同,我们就认为这两个对象是相同的。如果该对象存在于数据库中,我们就去查找具有相同ID值的记录,并确保其与我们的对象模型中的属性保持一致。这就是我们使用属性ID在数据库中作为主键来维持它们之间映射关系的方式。
在对象模型中,最有趣的部分通常涉及对象之间的关联关系和继承关系。简而言之,对象关系的设计源泉在于对象模型中的关系。如果我们仅仅将对象定义为属性、方法和类,它将缺乏趣味性,而所有的精彩之处都存在于对象之间的关系,包括关联关系和继承关系。
关联关系可以使用外键来自然表示。数据库中的外键用于表述两条记录之间的关系,类似地,两个对象之间的关联关系,无论是一对一、一对多还是多对多,都可以使用外键来表示。
然而,继承关系更为复杂,因为它不像关联关系那样直接映射,而且在面向对象编程中,继承关系具有多态性,多态性是面向对象编程的一个显著特点。
多种设计原则,如Liskov替代原则等,都建立在多态性的基础上。如果无法实现多态性,面向对象模型的价值就会大打折扣。因此,在关系数据库中,我们必须使用一种妥协的方式来支持继承关系,以使其成为真正的OR Mapping工具。
需要注意的是,对象模型中的方法不需要存储在数据库中,因为方法是在类的定义中声明的,对象的方法都是一样的,只是对象的行为。不同的对象具有相同的方法,因为它们属于同一类,所以方法的定义通常在代码中。对象的定义存在于类的代码中,而对象的数据则是从数据库中获取的。对象之间的关系,包括关联关系和继承关系,都需要进行映射,而不需要映射方法。
方法就在那个Java的,代码的类的定义里头。所以我们类的定义是在代码中间,对象是由类产生的,对象的值是从数据库里给它捞出来的,无论是关联关系还是继承关系,这是我们的这样的一个映射关系。
面向对象模型 | 关系模型 |
---|---|
类 | 表 |
对象 | 记录 |
属性 | 列 |
对象ID | 主键 |
关联关系 | 外键 |
继承关系 | 不支持 |
方法 | 不支持 |
那我们最头疼的就是这个继承关系,继承关系在数据库里头是没有关系,数据库里是没有直接的映射的。
另一种方式是使用多张表,将父类和子类的属性分别存储在不同的表中。在这种情况下,每个子类会有一个独立的表,其中包含它自己的属性,同时也会包含从父类继承的属性。这种方式能够更好地保持数据库的结构整洁,同时也有利于提高数据库的查询效率。
对于任何一类对象,例如RoadVehicle,它在数据库表中的记录实际上只有四个属性具有值,其他属性都是空的。对于Car这类对象,它除了自身的属性之外,还会继承RoadVehicle的四个属性,其他属性仍然是空的。为了实现这样的继承关系存储方案,我们需要一个特殊的属性来描述对象所属的类别。在此方案中,我们首先使用ID来建立对象与数据记录之间的一对一关系,然后使用一个属性来标识它属于哪个类别。除此之外,所有属性都是这些对象的属性。
在这种继承关系的存储方案中,我们如何实现继承关系的特性呢?
比如说,当我们获取一个Car对象时,我们需要得到一个具有5个属性的Car对象。因此,当我们从数据库中进行查询时,我们会选择检索一条记录,而这条记录的类型是Car类型。(然后我们会使用Java的反射机制来实例化这个Car对象。通过反射机制,我们可以读取Car对象的属性,并将数据库中检索到的字段值对应到Car对象的属性上。)尽管Car对象本身只有5个属性,但实际上从数据库中检索出的可能是更多的属性,但我们只需要这些对应的属性。通过确保匹配,我们可以实例化Car对象,即使所有的属性都存储在同一张表中,我们也可以根据DISC类型来正确地实例化该对象。
我们在关系和对象模型映射中,它怎样来实现的呢?
在使用单表来实现继承关系时,我们可以通过执行不带条件的select语句来将所有对象的属性都检索出来。这将返回数据库中的多条记录,每条记录都包含对象的所有属性。接着,我们根据每条记录中的DISC属性,使用Java的反射机制来实例化相应的子类对象。这些实例化后的子类对象将被放入一个集合中,而这个集合的元素类型被定义为RoadVehicle对象。实际上,这些元素是具体的子类型。通过这种方式,返回的集合对象是父类对象,但实际上却是子类对象,因此可以展现出多态的特征。如果希望实现多态,可以将其强制转换为父类型,并将其放入集合中。
AAAAA
使用单表来实现继承关系的另一种方式是,每个类对应一张表。在这种情况下,实例化对象可能会稍微复杂一些,因为需要对每张表进行单独的检索。
在多表的模式下,实例化一个对象(比如Car对象)需要在多张表之间进行查询,并将多张表的记录合并为最终结果。这种方式的实现在关系数据库中较为常见,尤其在处理继承关系时会采用这种方式。举例来说,假设我们有一个RoadVehicle表,其中包含了Motorcycle的一条记录,而Motorcycle表中也有一条记录。通过使用它们的ID进行关联,我们可以在Motorcycle表中找到与之相关的记录,并将两张表的属性合并,从而得到最终的Motorcycle对象。
然而,这种方法存在一个问题,即在实例化对象时需要进行多次连表查询,特别是当对象的实例化需要多个连表操作时,查询的成本会更高。与单表模式相比,多表模式需要进行更多的查询操作,因此查询速度会受到较大的影响。需要注意的是,尽管在存储继承关系时Hibernate和MyBatis等框架默认采用多表模式,但在面对大量数据或高负载系统时,开发人员可能需要手动优化查询性能,以减少查询成本和提高系统的整体性能。
这是继承关系我们,可以看到用关系数据表,用一种特定的方式,我们能够把继承关系的,完整的特性能够把它存下来,然后把它实际化出对象来,要能把它表现出它的完整特性,关联关系呢我们分为一对一,一对多多对一。
订单和快递单之间的关系通常是一对一的。当我们需要考虑这种关系时,有一个重要的方向问题需要注意,即是从订单到快递单的访问频率通常比从快递单到订单的访问频率要高。出于提高查询速度的考虑,我们可以在订单表中将快递单的ID作为外键进行存储。这种做法的好处在于,利用主键访问的速度较快。通过查找订单记录,我们可以快速找到与之对应的快递单的主键ID,然后使用该主键ID在快递单表中找到对应的记录,最终我们可以得到订单对象和快递单对象,两者之间形成了关联。这样实例化出来的对象既包含订单对象又包含快递单对象,通过这种方式,我们可以迅速地了解订单所对应的快递轨迹。
在面向对象模型中,当我们获取一个订单对象时,它并不是一个孤立的对象,而是一个与其他对象相关联的对象。这意味着我们的逻辑应该基于有联系的对象来构建,而不是通过获取一个订单对象后再去查询数据库以获取其他对象。这种面向对象的方式要求我们建立具有关联性的对象,并在这些关联对象上进行业务逻辑的处理。
我们已经讨论了从订单到快递单的方向,那么从快递单到订单呢?实际上,我们的对象模型可以实现双向关联。当我们获取一个快递单对象时,我们可以通过查询数据库得到与之相关的订单。这种双向关联的实现是可行的。然而,需要注意的是,由于查询过程中涉及到外键的查询,相对于主键查询而言会更加耗时。在这种情况下,索引的加入是必不可少的。索引的加入可以提高查询速度,但即使使用了索引,由于查询过程的复杂性,仍然会比直接使用主键查询要慢一些。在实际应用中,我们应该根据不同的业务需求和性能要求来选择合适的查询方向。
一对多和多对一的关系实际上正好是相互对应的两种关系。在关系数据库中存储这种关系时,都具备这样的特征:只要存储了其中一种关系,就能够通过这种关系找到另外一种关系。无论是存储一对多还是多对一,都可以通过已知的一端找到与之关联的多端或者通过多端找到与之关联的一端。这种对应关系可以方便地实现数据的检索和查询。(上一到多,下多对一)
一对多和多对一的关系存储方式的选择通常取决于查询的方向和性能需求。
对于一对多关系,如果你经常需要从"一"的一端查找"多"的多个对象,那么使用一对多方式存储可能更有效,因为可以通过一次非主键查询找到多端的对象,然后做主键查询。反之,多对一的方式则可以通过一次非主键查询直接找到一端的对象,更加高效。
无论是Hibernate还是MyBatis,它们通常默认使用一对多和多对一的方式来存储关联关系,因为无论查询方向是哪一端,这两种方式都提供了较好的性能。
但对于多对多的关系,必须使用中间表来存储关联关系,因为多对多关系不具备上述一对多和多对一的性能优势。
使用Spring Boot和MyBatis来实现面向对象和关系的映射是一种常见的开发方式。当使用Spring Boot时,通常可以通过引入适当的Starter包来轻松配置和使用MyBatis。
Spring Boot的Starter包是用于快速启动和配置特定功能的依赖项。通过引入MyBatis Starter包,你可以在项目中轻松配置和使用MyBatis框架,而无需手动引入和配置相关的Jar包和XML文件。
在Spring Boot项目中,通常建议使用Spring Boot的自动配置来配置MyBatis。你只需提供MyBatis的Mapper接口和相应的SQL语句,Spring Boot会自动配置数据源和MyBatis的SqlSessionFactory,以及自动扫描并注册Mapper接口。这简化了配置过程,使开发更加高效。
因此,使用MyBatis Starter包是一个常见的实践,可以帮助你更容易地集成MyBatis到Spring Boot项目中,从而实现面向对象和关系的映射。不再需要手动引入和配置相关的Jar包和XML文件,而是通过Starter包来完成自动化配置。
Starter包的好处在于它能够方便地引入一系列必要的依赖项。以MyBatis为例,MyBatis是一个需要与数据库交互的OR Mapping工具。因此,使用MyBatis时,必然会涉及到与数据库的交互,这就需要使用JDBC。此外,如果要使用事务管理,也需要引入事务支持。
如果没有Starter包,那么这些依赖项就需要手动逐一引入。比如,你想使用MyBatis,但缺少了JDBC,MyBatis将无法正常工作。如果需要使用事务管理,你还需要额外引入事务管理的相关配置。这样的操作非常繁琐。
因此,Spring Boot的Starter包在其中的作用非常明显。它将MyBatis相关的依赖一并引入,包括JDBC和事务管理等。这样一来,你只需引入MyBatis的Starter包,就能自动获取所有相关的依赖项,不需要手动一一引入和配置。这也是Spring Boot的革命性变化之一,它能够方便地将相关的配置和依赖项一并引入,使整个开发过程更加简单高效。
在使用MyBatis时,Mapper是主要的组件,它是一系列方法的集合。每个Mapper都是一个接口,定义了一些操作方法。通过这些Mapper提供的方法,我们可以对对象模型进行操作。相比之下,如果你熟悉Hibernate,你会发现Hibernate根本就没有Mapper这一概念。
Hibernate会自动管理对象的更新,如果你从Hibernate获取对象,并修改了它的属性,它会自动将修改更新到数据库中。但是MyBatis没有这种自动分配的功能,它是半自动的。所有的操作都必须通过Mapper的方法来完成。
Mapper的方法针对关系数据库执行一些操作,但它将传入和传出的数据都转换成了对象模型。这种映射是在Mapper的方法中完成的。
Mapper的方法的具体实现依赖于配置文件,MyBatis有两种配置文件的写法:
虽然注解的写法出现得比XML晚,但是目前来看,大多数人还是更习惯使用XML。这是因为配置文件的编写通常会比较复杂,如果使用注解的方式进行配置,尤其是对于新手来说,可能会感到有些困惑。
MyBatis在国内已经使用了十几到二十年,因此很多老一辈的开发者都习惯使用XML配置。尽管大量的业界项目目前仍然采用XML配置,但这并不意味着XML就是一个更好的选择。技术的整体趋势是朝向Annotation发展的。这是因为Annotation与代码一起编写,你可以在方法的定义之前直接使用Annotation来描述它的映射关系,使得映射关系和方法的使用更紧密地结合在一起。这样看起来更加方便。相比之下,如果你选择使用XML来配置映射关系,就会面临与最初所有使用XML的方式所面临的相同问题。你需要遵循默认的定义规范,如果不符合默认规范,就会导致找不到对应的XML或方法,从而导致错误的发生。因此,默认规范越多,你在使用时就会感到越烦恼。
老一辈的开发者可能已经熟悉了这些默认规范,所以更喜欢使用XML。而对于新手来说,可能更喜欢使用Annotation。
无论使用XML还是Annotation,都不影响我们使用自动生成的方式来完成映射。
因此,MyBatis可以使用Annotation和XML来编写配置信息,然后通过Mapper的方式向其他程序提供操作关系数据库的接口。Mapper是我们在MyBatis中定义的接口,它的方法传入和传出的都是对象。
这些对象是如何映射到关系数据库中的,以及我们如何操作数据库,都在XML和Annotation中得到了明确定义。
因此,MyBatis是一个半自动的框架,它的工作原理和具体操作都是可以清楚地看到的。如果需要进行调优,你可以根据实际情况选择使用Annotation或XML来编写配置,以实现最优的结果。
Hibernate是一个完全自动化的框架,你只需要从Hibernate中获取对象,然后进行更改并将其存回去,它会自动处理整个过程。你压根不需要关心它的具体实现细节。相比之下,MyBatis需要连接数据库,因此它的数据库设置是来自于Spring框架的。在Spring框架中,我们会设置一个DataSource,这是通常用来连接数据库的设置,包括使用的数据库类型(如MySQL)、数据库驱动程序(如MySQL的JDBC驱动程序)、数据库的地址和IP、使用的数据库等等。
MyBatis框架实际上是从Spring框架中获取这个DataSource,然后创建一个叫做SqlSessionFactory的对象。为什么叫做SqlSessionFactory呢?
这是因为数据库的连接并不会在整个应用启动后立即建立。数据库连接实际上是有限的,比如说我的数据库同时只能连接100个客户端。一般情况下,我们的应用程序需要连接300个客户端,但是当你的应用程序启动时,并不是立即去连接数据库的。数据库连接是在需要的时候才去建立的,使用完毕后会将其释放掉。这样对于数据库服务器来说更加友好,你可以使用尽可能少的max connection来服务更多的客户端。
因此,当MyBatis启动后,它会创建一个SqlSessionFactory,并将从Spring框架中获取的DataSource放入其中。这样就建立了一个连接数据库的过程,使得数据库连接在需要时建立,用完后释放,提高了数据库服务器的使用效率。
**每当一个事务开始时,MyBatis会通过SqlSessionFactory实例化出一个SqlSession来连接数据库。**它会使用DataSource中配置的信息来连接特定的数据库。在使用过程中,我们实际上并不直接使用SqlSession,我们使用的是由MyBatis框架实例化出来的Mapper对象。
Mapper对象实际上包含了我们在代码中定义的一些方法。你可以将Mapper对象视为一些Bean对象,但实际上,当你仔细研究MyBatis的原理时,你会发现它不完全是Bean对象。这是因为你可能会定义非常多的Mapper对象,这可能会让你对MyBatis是如何根据你的定义来实例化出这么多不同的Mapper对象感到好奇。如果你对此感兴趣,你可以深入阅读MyBatis的内部工作原理,它其实相当复杂。
这些Mapper对象通过实例化的SqlSession来实现对数据库的操作。SqlSession是一个固定的操作集,它使用你在MyBatis中编写的标签和特定的Annotation来工作。MyBatis的Mapper中使用这些标签来描述Mapper应该如何工作。因此,你的描述将使Mapper调用SqlSession中对应的方法,并访问数据库以完成相应的操作。这就是MyBatis内部工作的原理。
Mapper对象是由Spring框架实例化出来的,根据我们的定义来实例化出我们想要的Mapper对象。然后我们通过这个Mapper对象去调用SqlSession的方法来访问数据库。在Mapper中编写的内容实际上是调用SqlSession不同方法的描述。Mapper对象的实现是通过XML和Annotation的配置来完成的。
Mapper通常是一个接口,我们需要在接口前面加上MyBatis的注解。在Spring的应用中,我们可以使用各种注解,包括来自MyBatis的注解。当我们写了这些注解之后,MyBatis框架会根据这些信息来实例化Mapper对象,并告诉它要完成什么样的操作。
Mapper接口的定义通常包含了一些方法的定义,但是并没有具体的实现。具体的实现是通过XML或者Annotation的配置来完成的。在这里,我们展示的是XML的例子。如果使用Annotation,那么这些配置会写在方法前面,通过一些复杂的Annotation标签来描述这个方法具体要完成的操作。
因此,在MyBatis中,Mapper的作用就是通过传入对象和返回对象来完成对象模型和关系模型之间的映射。具体的操作则是通过我们自己编写的SQL语句来实现的。
实际上,在大多数情况下,我们希望能够自动生成大部分的映射,除非性能要求无法满足,我们才会手动编写映射。
XML的一个问题在于它有一些默认的对应规则。比如,我们在Mapper接口中定义了一个方法,但是方法名本身是不能唯一标识一个方法的,因为可以进行方法重载。因此,在XML中,ID、parameterType和resultType这三者结合起来才能唯一标识一个方法。如果其中有任何一处出错,它就无法正确对应,从而导致报错。这是XML的一个讨厌之处。
相比之下,如果我们使用Annotation,情况就会简单得多。Annotation只需要写在方法前面,告诉它这个方法应该做什么。因此,它不需要遵循那些默认规则。这就是为什么Annotation可能会成为替代XML的一种做法,因为它避免了那些默认规则,而且更加直观。
然而,Annotation的缺点在于,对于一些复杂的情况,它可能变得更加复杂。相比之下,XML至少是格式化的,可以进行缩进,而Annotation可能没有这些格式化的选项。因此,如果使用Annotation来编写一段复杂的内容,可能会显得很难看。
所以我不排斥大家使用Annotation,只要你能够熟练使用它。但是很多老手们仍然喜欢使用XML。对于XML部分,我不打算过多深入讲解,我只想让大家理解,在Mapper中的每个方法中,传入对象和传出对象是如何工作的,可以通过使用Annotation和XML来完成这些工作。
数据库操作通常涉及四种基本操作,即增删改查(INSERT、UPDATE、DELETE、SELECT)。这些操作对应了SqlSession中的一些操作。在这些操作中,使用不同的标记来指明操作类型,如SELECT、INSERT等。
SELECT操作:SELECT用于从数据库中检索数据。它通常伴随着一个ResultMap,这是一个复杂的映射规则,将数据库表和对象模型之间的关系互相映射。SELECT操作的ID对应了在Mapper中定义的方法名,参数是需要传递给SQL的值,返回值是SQL查询的结果对象。
INSERT操作:INSERT用于向数据库中插入新数据。参数通常对应了插入数据的字段值。特别之处在于,如果有自增字段,需要将这个字段的值返回,因此在Mapper接口中的方法参数中会有一个对象,其属性对应了插入数据的字段,但ID属性的值通常为空,因为数据库会自动生成。
DELETE操作:DELETE用于从数据库中删除数据。DELETE的SQL语句需要你手动编写,以及与Mapper接口方法的映射关系。与其他操作不同,DELETE不会自动生成返回值。
这些操作中的SQL语句可以使用动态SQL来处理特殊情况,例如在WHERE子句中添加条件时,需要对参数进行判断。此时,可以使用动态SQL来动态构建SQL语句,以避免不必要的条件。在动态SQL中,你可以使用IF、TRIM等标签来处理特定条件和构建SQL查询。
此外,还有一种复杂的情况,涉及多个关联对象和继承关系。在这种情况下,使用ResultMap和特定的映射规则可以帮助你正确映射数据库表和对象模型之间的复杂关系。
总之,MyBatis提供了强大的数据库操作工具,可以用于执行各种数据库操作,从基本的增删改查到复杂的条件构建和对象关系映射。虽然你可以手动编写SQL语句和映射规则,但MyBatis还提供了很多便捷的方式来处理这些问题,使数据库操作更高效和易于维护。
AAAAA
在工程开发中,配置文件通常都存放在application.yaml
中。让我们来看一些具体的例子来解释。最后我们来讨论一个问题,即XML和注解都难以编写。
实际上,我们有工具可以生成简单的XML和注解。如果你对对象模型没有特别要求,比如你不关心它们之间的关联关系,而是将所有对象视为孤立的表和对象,则可以直接使用工具来生成。因为这不需要费脑子,手动编写会显得有些浪费,通常我们使用一些半自动化的工具。
在这些工具中,有些是非常自动化的,有些则不太自动化。我们选择了中间的方式,因为很多事情我们还需要自己手动编写。我们希望能够通过工具将表映射为类,将记录映射为对象,以及在XML中描述如何将表映射为PO对象。此外,我们还提供了一个Mapper,用于针对单个对象的增删改查。这就是我们所说的工具。
这里引出了另一个问题,即对象模型的构建是在Mapper层完成还是在DAO层完成。如果我们计划在DAO层完成对象模型的构建,那么Mapper就只负责将表变为对象。对象之间的关系由DAO层处理。那么到底应该选择哪种方案呢?是由Mapper层完成所有的映射关系,还是只将对象拿出来,然后在DAO层建立关系呢?
这两种方式的选择取决于性能是否有差异。如果性能没有差异,我们会选择使用Java代码来编写,因为Java代码是可以进行错误验证的。我们可以使用测试工具来验证通过Java代码编写的部分是否经过测试,从而确定编码是否没有问题。然而,XML和注解却无法做到这一点。
因此,如果两者性能相同,我们会选择使用Java代码来完成这一过程。如果使用Java代码来完成,MyBatis就变成了一个最简单的工具,它只负责将对象变为记录,而不会产生对象为两条记录、继承关系和关联关系等复杂性。这些都由DAO层来完成。因此,我们可以使用生成器工具来生成这些内容。如果你这样做,实际上使用MyBatis和Hibernate就没有差别了,因为你也不会使用它们复杂的功能,你只是希望每个类对应一张表,每个对象对应一条记录,而没有其他关联。这就是我们引入MyBatis生成器插件的原因。
这个插件并不是我们程序中要使用的,它只是用来生成代码的。因此,我们在plugins中将这个插件与我们的任何一个phase没有关联。它只是一个独立的插件,你可以在plugins中运行它。它的配置文件在configurationFile
中。关于这个插件的使用方法,我在视频中也不打算讲解,因为关于这个东西的用法我已经写得非常繁琐了。
基本上你可以理解注释的意思,都有中文注释。大家可以自己看。最后让我们来谈谈事务。
我们知道在处理高并发时一定需要使用事务。事务解决的问题是当多人同时访问同一条数据时,它要保证数据的一致性。数据库中的ACID是这种解决问题的最简单和直接的方式。它意味着只要你的操作都在一个事务中,要么都成功,要么都失败。如果两个人同时访问同一条数据,各自在各自的事务中,它们彼此是完全独立的,不会相互影响。因此,在MySQL中,我们将对事务进行深入讨论。这不仅仅是讲解大家所了解的ACID,而是要讲解在MySQL上如何实现这些ACID。我们之所以要讨论这个,是因为我们的系统既有高负载部分,又有高并发部分。因此,你需要知道你使用的每个东西在高并发条件下是如何工作的。这样你就会知道它的高并发特性是否能满足你的要求,或者说如果它在产生巨大的高并发时是否会产生很大的副作用。
因此,在MySQL中,我们会对事务进行更深入的讨论。在这里,我们主要使用了一种基于注解的方式来标记事务。这个注解叫做@Transaction。通常我们会将它标记在服务层,即控制器层的目的。大家应该知道控制器层的目的是用来处理界面的,因此它与事务无关。但凡要处理的业务都从服务层开始。因此,服务层是我们整个后端逻辑的起点。因此,我们从起点开始就标记一个@Transaction。这就意味着整个后面处理的逻辑都符合ACID。当我们标记一个@Transaction时,它会带来一些问题。首先一个问题是它会具有传播性。什么是传播性呢?因为我们服务层的代码是相互调用的,所以我们服务层的代码不能够独立调用,它可能会相互调用。如果两个方法都标记了@Transaction,它的事务会是怎样的呢?
默认情况下它是REQUIRED
。这是什么意思呢?比如一个方法调用另一个方法,如果另一个方法在进来的时候已经开启了事务,那么它就会在这个事务中继续执行。这会带来什么样的结果呢?事务在出错后会回滚,所以如果这个方法出错,它会将前面的东西也回滚。另外一种情况是,如果我调用它,但是我这边没有事务,它会怎样呢?它会在这里开启一个事务,只在这个范围内有事务。因此,如果抛出异常会有什么结果呢?它会滚到它最开始的状态,而这边的东西不会回滚。这是默认情况。还有一种可能会使用的是REQUIRES_NEW
。这个名字很直接,不管前面有没有,我都会开启一个新的事务。在这种情况下,如果前面已经有一个事务,那么第二个事务产生的结果会怎样呢?在回滚的时候,它首先会将第二个事务回滚,然后再看我们前面设置的,是否要带着它回滚。如果要带着它回滚,那么两个都会回滚,如果不带着它回滚,它可以控制只回滚一小部分。因此,事务具有灵活性,它既可以滚自己也可以滚别人。为什么不将它设置成默认的方式呢?大家觉得它挺灵活的,可以根据情况滚自己也可以滚别人。为什么不将它设置成默认的呢?因为大家知道事务嵌套对数据库的压力很大,因为它们可以相互调用,所以它们有可能相互嵌套很多层。事务嵌套太多对数据库的压力很大。因此,通常我们默认是不嵌套的。说到底,在事务上,大家还可以看到我们会使用另外两个值,即rollbackFor
和noRollbackFor
。在出现异常时会回滚事务,但是哪些异常会导致事务回滚,哪些异常不会导致事务回滚,你可以在这里指定。