将long类型变量num作为参数,实现计算该数字乘积的持续性的方法 persistence()?

13 如何正确使用 @Entity 里面的回调方法?

本课时我要介绍的是 @Entity 的回调方法。

为什么要讲回调函数呢?因为在工作中,我发现有些同事会把这个回调方法用得非常复杂,不得要领,所以我专门拿出一个课时来为你详细说明,并分享我的经验供你参考。我将通过“语法 + 实践”的方式讲解如何使用 @Entity 的回调方法,从而达到提高开发效率的目的。下面开始本课时的学习。

关于上表所述的几个方法有一些需要注意的地方,如下:

  1. 回调函数都是和 mit 在同一个线程里面执行的,只不过调用方法有先后之分,都是同步调用,所以当任何一个回调方法里面发生异常,都会触发事务进行回滚,而不会触发事务提交。

  2. Callbacks 注解可以放在实体里面,可以放在 super-class 里面,也可以定义在 entity 的 listener 里面,但需要注意的是:放在实体(或者 super-class)里面的方法,签名格式为“void ()”,即没有参数,方法里面操作的是 this 对象自己;放在实体的 EntityListener 里面的方法签名格式为“void (Object)”,也就是方法可以有参数,参数是代表用来接收回调方法的实体。

JPA 里面规定的回调方法还有一些,但不常用,我就不过多介绍了。接下来,我们看一下回调注解在实体里面是如何使用的。

这里我介绍两种方法,是你可能会在实际工作中用到的。

第一步:修改 BaseEntity,在里面新增回调函数和注解,代码如下:

在这一步骤中需要注意的是:

  1. 我们上面注释的代码,也可以改变 entity 里面的值,但是在这个 Listener 的里面我们不做修改,所以把 setVersion 和 setCreateUserId 注释掉了,要注意测试用例里面这两处也需要修改。

  2. 如果在 @PostLoad 里面记录日志,不一定每个实体、每次查询都需要记录日志,只需要对一些敏感的实体或者字段做日志记录即可。

  3. 回调函数时我们可以加上参数,这个参数可以是父类 Object,可以是 BaseEntity,也可以是具体的某一个实体;我推荐用 BaseEntity,因为这样的方法是类型安全的,它可以约定一些框架逻辑,比如 getCreateUserId、getLastModifiedUserId 等。

第二步:还是一样的道理,写一个测试用例跑一下。

通过日志我们可以很清晰地看到 callback 注解标注的方法的执行过程,及其实体参数的值。你就会发现,原来自定义 EntityListener 回调函数的方法也是如此简单。

细心的你这个时候可能也会发现,我们上面其实应用了两个 EntityListener,所以这个时候 @EntityListeners 有个加载顺序的问题,你需要重点注意一下。

  1. EntityListeners 和实体里面的回调函数注解可以同时使用,但需要注意顺序问题;

  2. 如果我们不想加载super-class里面的EntityListeners,那么我们可以通过注解 @ExcludeSuperclassListeners,排除所有父类里面的实体监听者,需要用到的时候,我们再在子类实体里面重新引入即可,代码如下:

看完了上面介绍的两种方式,关于 Callbacks 注解的用法你是不是已经掌握了呢?我强调需要注意的地方你要重点看一下,并切记在应用时不要搞错了。

上面说了这么多回调函数的注解使用方法,那么它的最佳实践是什么呢?

我以个人经验总结了几个最佳实践。

1.回调函数里面应尽量避免直接操作业务代码,最好用一些具有框架性的公用代码,如上一课时我们讲的 Auditing,以及本课时前面提到的实体操作日志等;

2.注意回调函数方法要在同一个事务中进行,异常要可预期,非可预期的异常要进行捕获,以免出现意想不到的线上 Bug;

3.回调函数方法是同步的,如果一些计算量大的和一些耗时的操作,可以通过发消息等机制异步处理,以免阻塞主流程,影响接口的性能。比如上面说的日志,如果我们要将其记录到数据库里面,可以在回调方法里面发个消息,改进之后将变成如下格式:

4.在回调函数里面,尽量不要直接在操作 EntityManager 后再做 session 的整个生命周期的其他持久化操作,以免破坏事务的处理流程;也不要进行其他额外的关联关系更新动作,业务性的代码一定要放在 service 层面,否则太过复杂,时间长了代码很难维护;(ps:我曾经看到有人把回调函数用得十分复杂,做各种状态流转逻辑,时间长了连他自己也不知道是干什么的,耦合度太高了,你一定要谨慎。)

5.回调函数里面比较适合用一些计算型的transient方法,如下面这个操作:

6.JPA 官方比较建议放一些默认值,但是我不是特别赞同,因为觉得那样不够直观,我们直接用字段初始化就可以了,没必要在回调函数里面放置默认值。

那么除了日志,还有没有其他实战应用场景呢?

确实目前除了日志,Auditing 稍微公用一点,其他公用的场景不多。当遇到其他场景,你可以根据不同的实体实际情况制定自己独有的 EntityListener 方法,如下:

例如,User 中我们有个计算年龄的逻辑要独立调用,就可以在持久化之前调用此方法,新建一个自己的 UserListener 即可,代码如下:

以上,关于 JPA Callbacks 在一些实际场景中的最佳实践就介绍这些,希望你在应用的时候多注意找方法,避免不必要的操作,也希望我的经验可以帮助到你。

那么 callbacks 的实现原理是什么呢?其实很简单,Java Persistence API规定:JPA 的实现方需要实现功能,需要支持回调事件注解;而 Hibernate 内部负责实现,Hibernate 内部维护了一套实体的 EventType,其内部包含了各种回调事件,下面列举一下:

通过一步一步断点,再结合 Hibernate 的官方文档,可以了解内部 EventType 事件的创建机制,由于我们不常用这部分原理,知道有这么回事即可,你有兴趣也可以深入 debug 研究一下。

到这里,本课时内容就介绍这么多。这一节,我们分析了语法,列举了实战使用场景及最佳实践,相信通过上面提到的异常、异步、避免死循环等处理方法,你已经知道回调函数的正确使用方法了。其中最佳实践场景也欢迎你补充,我们可以一起探讨。

下一课时,我们将迎来很多人都感兴趣的“乐观锁机制和重试机制”相关内容,到时候我会告诉你它们在实战中都是怎么使用的。

点击下方链接查看源码:(不定时更新)


14 乐观锁机制和重试机制在实战中应该怎么用

你好,欢迎来到第 14 课时,本课时我要为你揭晓乐观锁机制的“神秘面纱”,在前面的留言中,我看到很多人对这部分内容很感兴趣,因此希望通过我的讲解,你可以打开思路,真正掌握乐观锁机制和重试机制在实战中的用法。那么乐观锁到底是什么呢?它的神奇之处到底在哪?

乐观锁在实际开发过程中很常用,它没有加锁、没有阻塞,在多线程环境以及高并发的情况下 CPU 的利用率是最高的,吞吐量也是最大的。

而 Java Persistence API 协议也对乐观锁的操作做了规定:通过指定 @Version 字段对数据增加版本号控制,进而在更新的时候判断版本号是否有变化。如果没有变化就直接更新;如果有变化,就会更新失败并抛出“OptimisticLockException”异常。我们用 SQL 表示一下乐观锁的做法,代码如下:

假设本次查询的 version=1,在更新操作时,加上这次查出来的 Version,这样和我们上一个版本相同,就会更新成功,并且不会出现互相覆盖的问题,保证了数据的原子性。

这就是乐观锁在数据库里面的应用。那么在 Spring Data JPA 里面怎么做呢?我们通过用法来了解一下。

JPA 协议规定,想要实现乐观锁可以通过 @Version 注解标注在某个字段上面,并且可以持久化到 DB 即可。其支持的类型有如下四种:

这样就可以完成乐观锁的操作。我比较推荐使用 Integer 类型的字段,因为这样语义比较清晰、简单。

我们通过如下几个步骤详细讲一下 @Version 的用法。

第一步:实体里面添加带 @Version 注解的持久化字段。

我在上一课时讲到了 BaseEntity,现在直接在这个基类里面添加 @Version 即可,当然也可以把这个字段放在 sub-class-entity 里面。我比较推荐你放在基类里面,因为这段逻辑是公共的字段。改动完之后我们看看会发生什么变化,如下所示:

其中,我们通过 @Transactional 开启事务,并且在查询方法后面模拟复杂业务逻辑,用来呈现多线程的并发问题。

第五步:按照惯例写个测试用例测试一下。

从上面的测试得到的结果中,我们执行 testVersion(),会发现在 save 的时候, Version 会自动 +1,第一次初始化为 0;update 的时候也会附带 Version 条件。我们通过下图的 SQL,也可以看到 Version 的变化。

而当面我们调用 testVersionException() 测试方法的时候,利用多线程模拟两个并发情况,会发现两个线程同时取到了历史数据,并在稍后都对历史数据进行了更新。

由此你会发现,第二次测试的结果是乐观锁异常,更新不成功。请看一下测试的日志。

通过日志又会发现,两个 SQL 同时更新的时候,Version 是一样的,是它导致了乐观锁异常。

注意:乐观锁异常不仅仅是同一个方法多线程才会出现的问题,我们只是为了方便测试而采用同一个方法;不同的方法、不同的项目,都有可能导致乐观锁异常。乐观锁的本质是 SQL 层面发生的,和使用的框架、技术没有关系。

那么我们分析一下,@Version 对 save 的影响是什么,怎么判断对象是新增还是 update?

通过上面的实例,你不难发现,@Version 底层实现逻辑和 @EntityListeners 一点关系没有,底层是通过 Hibernate 判断实体里面是否有 @Version 的持久化字段,利用乐观锁机制来创建和使用 Version 的值。

其中,我们先看第一段逻辑,判断其中是否有 @Version 标注的属性,并且该属性是否为基础类型。如果不满足条件,调用 super.isNew(entity) 方法,而 super.isNew 里面只判断了 ID 字段是否有值。

第二段逻辑表达的是,如果有 @Version 字段,那么看看这个字段是否有值,如果没有就返回 true,如果有值则返回 false。

由此可以得出结论:如果我们有 @Version 注解的字段,就以 @Version 字段来判断新增 / update;如果没有,那么就以 @ID 字段是否有值来判断新增 / update。

需要注意的是:虽然我们看到的是 merge 方法,但是不一定会执行 update 操作,里面还有很多逻辑,有兴趣的话你可以再 debug 进去看看。

我直接说一下结论,merge 方法会判断对象是否为游离状态,以及有无 ID 值。它会先触发一条 select 语句,并根据 ID 查一下这条记录是否存在,如果不存在,虽然 ID 和 Version 字段都有值,但也只是执行 insert 语句;如果本条 ID 记录存在,才会执行 update 的 sql。至于这个具体的 insert 和 update 的 sql、传递的参数是什么,你可以通过控制台研究一下。

总之,如果我们使用纯粹的 saveOrUpdate方法,那么完全不需要自己写这一段逻辑,只要保证 ID 和 Version 存在该有的值就可以了,JPA 会帮我们实现剩下的逻辑。

实际工作中,特别是分布式更新的时候,很容易碰到乐观锁,这时候还要结合重试机制才能完美解决我们的问题,接下来看看具体该怎么做。

乐观锁机制和重试机制在实战中应该怎么用?

我们先了解一下 Spring 支持的重试机制是什么样的。

第四步:新建一个测试用例测试一下。

这里要说的是,我们在测试用例里面执行 @Import(RetryConfiguration.class),这样就开启了重试机制,然后继续在里面模拟了两次线程调用,发现第二次发生了乐观锁异常之后依然成功了。为什么呢?我们通过日志可以看到,它是失败了一次之后又进行了重试,所以第二次成功了。

通过案例你会发现 Retry 的逻辑其实很简单,只需要利用 @Retryable 注解即可,那么我们看一下这个注解的详细用法。

其源码里面提供了很多方法,看下面这个图片。

下面对常用的 @Retryable 注解中的参数做一下说明:

  • maxAttempts:最大重试次数,默认为 3,如果要设置的重试次数为 3,可以不写;

  • value:抛出指定异常才会重试;

  • exclude:指定不处理的异常;

  • value=delay:隔多少毫秒后重试,默认为 1000L,单位是毫秒;

  • multiplier(指定延迟倍数)默认为 0,表示固定暂停 1 秒后进行重试,如果把 multiplier 设置为 1.5,则第一次重试为 2 秒,第二次为 3 秒,第三次为 4.5 秒。

下面是一个关于 @Retryable 扩展的使用例子,具体看一下代码:

可以看到,这里明确指定 SQLException.class 异常的时候需要重试两次,每次中间间隔 100 毫秒。

此外,你也可以利用 SpEL 表达式读取配置文件里面的值。

关于 Retryable 的语法就介绍到这里,常用的基本就这些,如果你遇到更复杂的场景,可以到 GitHub 中看一下官方的 Retryable 文档:。下面再给你分享一个我在使用乐观锁+重试机制中的最佳实践。

乐观锁+重试机制的最佳实践

我比较建议你使用如下配置:

这里明确指定 ObjectOptimisticLockingFailureException.class 等乐观锁异常要进行重试,如果引起其他异常的话,重试会失败,没有意义;而 backoff 采用随机 +1.5 倍的系数,这样基本很少会出现连续 3 次乐观锁异常的情况,并且也很难发生重试风暴而引起系统重试崩溃的问题。

到这里讲的一直都是乐观锁相关内容,那么 JPA 也支持悲观锁吗?

除了乐观锁,悲观锁的类型怎么实现?

你可以看到,UserInfoRepository 里面覆盖了父类的 findById 方法,并指定锁的类型为悲观锁。如果我们将 service 改调用为悲观锁的方法,会发生什么变化呢?如下图所示:

然后再执行上面测试中 testRetryable 的方法,跑完测试用例的结果依然是通过的,我们看下日志。

你会看到,刚才的串行操作完全变成了并行操作。所以少了一次 Retry 的过程,结果还是一样的。但是,你在生产环境中要慎用悲观锁,因为它是阻塞的,一旦发生服务异常,可能会造成死锁的现象。

本课时的内容到这里就介绍完了。在这一课时中,我为你详细讲解了乐观锁的概念及使用方法、@Version 对 Save 方法的影响,分享了乐观锁与重试机制的最佳实践,此外也提到了悲观锁的使用方法(不推荐使用)。

那么现在,你又掌握了 JPA 的一项技能,希望你可以多动手实践,不断总结经验,以提高自己的技术水平。

下一课时,我们看看 JPA 对 Web MVC 开发者都做了哪些支持呢?

点击下方链接查看源码(不定时更新)


}

Android中的数据存储主要分为三种基本方法:

  2.传统文件系统。

  3.利用SQLite的数据库管理系统。

  对SharedPreferences对象和SQLite数据库,它们的数据仅对创建它们的应用是可访问的。

  换句话说,它们不是共享的,如果需要在不同的应用之间共享数据,需要建立content provider,本文对这部分内容不作介绍。

  本文基本是官方文档翻译,原文请见文后链接。

  选择内部或外部存储

  获取外部存储设备的权限

  向外部存储存储文件

三.在SQL数据库中存储文件

  向数据库中存入数据

  从数据库中读取数据

  删除数据库中的信息

   提供了一种存储键值对的方法,可以用于存储原生数据类型(boolean, float, int,

  这些数据在用户工作阶段一直被保持,即便应用被关闭也还是保持。

  所以可以用来保存一些数据,比如用户设定的字体,背景,用户名等,在下一次打开应用时,不用重新设定这些数据。

  获得对象可以使用下面两种方法:

  这是  中的方法,可以通过第一个参数指定你需要获得的文件名。

  向Shared Preferences写数据,首先需要调用方法创建一个 对象,然后调用这个对象的putXXX()方法存储键值对,键都是String类型,值是XXX所对应的数据类型(boolean,

  读取数据时需要调用SharedPreferences对象的getXXX()方法,来获得给定键(第一个参数)对应的值,如果给定键不存在,则会返回给定的默认值(第二个参数)。

  Android文件系统和其他平台上的类似,使用 APIs可以读写文件。

  这部分内容需要你已经了解了Linux文件系统的基础,并且也了解了包中的标准文件输入输出APIs。

  所有的Android设备都有两块文件存储区域:内部和外部存储。

  内部存贮一般指设备自带的非易失性存储器,外部存储指可拆卸的存储介质,比如微型的SD卡。

  一些设备把永久的存储区域分为"internal"和"external"的分区,所以即便没有可拆卸的存储介质,这些设备永远都有两种存储区域,并且不管外部存储区到底是可拆卸的还是内置的,APIs的行为是一致的。

  存储在内部存储区域的数据默认情况下只对你的app可用。无论是用户或者是其他app都不能访问你的数据。

  当用户卸载你的app时,系统会自动移除app在内部存储上的所有文件。

  不一定一直可以访问,因为用户可以拆卸外部存储设备。

  存储在外部存储的文件是全局可读的,没有访问限制,不受你的控制。可以和其他apps分享数据,用户使用电脑也可以访问在外部存储中的文件。

  当用户卸载你的app时,只有当你把文件存储在以 .获得的路径下时,系统才会帮你自动移除。

  注意:默认情况下app是安装在内存上的,可以通过在manifest中指定 属性来安排app的安装位置。具体见.

获取外部存储设备的权限

  为了向外部存储中写入数据,需要在manifest中指定权限  :

  注意:现在所有的app都可以读取外部存储中的数据,而不需要特殊的权限说明。

  但是,这点在新版本的更新中有可能会改变,如果你的应用需要读取外部存储中的数据而不需要写数据,应该声明 权限。

  为了保证应用能够持续工作,应该现在开始就加入读取权限:

  然而,如果你的应用包含了 权限,它隐式地包含了读取权限。

  向内部存储存文件时,可以通过下面两个方法获取合适的路径,返回值是一个File对象。

  返回一个File,表示的是app的应用文件在内部存储的绝对路径。

  返回一个File,表示的是app的缓存文件在内部存储的绝对路径。

  在如上路径中创建一个新文件,可以利用 的构造方法。将上面两个方法获得的File对象作为参数传入,如:

 

  另外,也可以调用 来获取 ,然后向你的内部目录写入数据,如下:

  或者,如果你需要缓存一些文件,你应该使用.

  比如,下面的例子从一个URL中提取了文件名,然后利用该文件名创建了一个文件存储在应用的内部缓存路径下:

 

  你应用的内部存储路径是由应用的包名指定的,在Android文件系统上的一个特定位置。

  技术上来说,如果你把文件的模式设置为可读的,其他应用是可以读取你的内部文件的,但是,另外的应用需要知道你的应用的包名和文件的名字。

  如果不明确指明,其他应用是不可浏览你的内部文件路径的,也不拥有读取和写入的权限。

  所以,只要你对内部存储上的文件使用,它们对其他应用来说就是不可用的。

  因为外部存储很有可能不可用,所以每次使用前都需要检查可用性。

  通过方法可以查询外部存储状态,如果返回状态为, 你可以继续对文件进行读写操作。

 

  尽管外部存储可以被用户和其他app修改,仍然有两种类型的文件你可以选择:

  这些文件对用户和其他应用都是可用的。当用户卸载应用时,这些文件对用户仍然是可用的。比如,应用拍摄的照片文件或者是下载的文件。

  属于你应用的文件,应当在应用被卸载的时候同时被删除。

  尽管从技术上来说,这些文件可以被用户和其他文件访问,因为它们是存储在外部存储介质上的,但是它们在你的应用外部并不提供什么实际价值。当用户卸载应用时,系统会删除外部存储上应用私有路径下的所有文件。

  比如,应用下载的一些额外的资源或者临时的媒体文件。

  存储公有文件,首先用获得路径,这个方法需要一个参数指明文件类型,比如  或 . 如:

  如果想要创建私有文件,利用 方法获得路径,并且传递给它一个名字指明路径类型。

  每一个用这种方法创建的路径都会被加在一个父目录中,这个目录包装了你的应用的所有外部文件,当你卸载应用时,系统会删除它们。

  比如,下面的方法为相册创建了一个路径:

  如果所有预定义的子目录都不适合于你的文件,你可以调用并且传入null。这样会返回你的应用在外部存储上私有路径的根目录。

  要记住在一个卸载应用时系统要删除的目录中创建目录如果你的文件在应用删除之后仍需要保存,你应该使用

  无论使用哪种方法,比较重要的一点是,要使用API提供的路径名常量,如等。这些路径名保证了文件会被系统正确处理。

  如果你提前知道你要存储多少数据,你可以实现查询是否有足够的存储空间,而不必引起一个 

  通过调用  和方法,你可以获取当前的空闲空间和当前卷的总空间。

  但是,系统并不保证你可以存储的容量和 方法获取的字节数一样多,如果该方法返回的容量要比你实际存储的数据大小多几个MB,或者存储后系统的填满程度小于90%,那么很可能是可以安全处理的。否则,可能就存储不下了。

  并没有要求你必须先检查剩余空间再存储文件,你可以把存储的语句写入一个try块中,然后catch IOException。在你并不知道文件多大时你需要这么做。

  在你不需要文件时你需要将其删除。

  最直接的方法:获得文件引用然后调用 方法:

  如果文件存储在内部存储上,可以请求  去定位和删除文件,通过调用:

  注意,当用户卸载你的app时,Android系统删除如下:

  所有在内部存储上的文件。

  所有用存储在外部存储上的文件。

  然而,你需要定期手工地删除利用 创建的所有缓存文件。并且,需要定期清除所有不再需要的文件。

  对于重复或结构性强的数据来说,把它们存储在数据库中是一种理想的做法。

  Android系统上你将会用到的数据库相关的APIs都在这个包中: 。

  SQL数据库中的主要原则之一就是构架(schema):一个关于这个数据库是如何组织的一个正式的声明。

  构架反映在创建SQL数据库的语句中。

  你可能会发现创建一个同伴类(companion class)很有用,同伴类同时被称作合约类(contract class),其中明确规定了你的构架的布局,以一种系统且自说明的方式。

  一个合约类(contract class)是一个常量的容器,这些常量定义了URI,表的名字,列的名字。

  合约类允许你在同一个包的其他类中使用这些名字常量。

  这就允许了你在一个地方改变列名,而同时把它传播到代码的其他地方去。

  组织合约类的一个好方法是:把对于你的整个数据库来说是全局的那些定义放在类的根级上;然后对于每一个表(table)创建一个内部类,列举其列。

  注意:通过实现 接口,你的内部类要继承一个基本的关键字域叫做_ID,一些Android的类比如cursor

  它不是必须的,但是它能帮助你的数据库更和谐地和Android framework工作

  比如,下面这个代码段定义了表名和列名:

  为了阻止不小心实例化合约类,给它一个私有的空构造函数

  一旦你定义好了你的数据库看起来什么样,你就应该实现一些方法,用来创建和维护数据库和数据表。

  下面是创建和删除表的一些典型的语句:

  就好像你存储文件到设备的内部存储上一样,Android在你应用相关的私有磁盘空间上存储你的数据库数据。

  你的数据是安全的,因为默认情况下其他应用无法访问这块区域。

   类提供了一些有用的APIs,当你使用这个类来获取数据库的引用时,系统仅在需要时执行可能长时间的操作:创建和更新数据库,而不是在应用启动的时候执行。

  你需要做的仅仅是调用 或方法。

  注意:因为它们是长时间运行的,所以请确保你在背景线程中调用:

  为了使用,创建一个子类,然后覆盖这三个回调函数:

  你也可以实现 ,但这并不是必须的。

  如下是一个对的实现例子:

  要访问你的数据库,需要先实例化你的的子类。

  想数据库中插入数据:通过向  方法中传入一个 对象实现。

  方法接收的第一个参数是表的名字,第二个参数是列的名字:可以通过这个参数设定一个列名,如果是空值,这样这个列就会插入NULL;如果第二个参数是null,那么如果是空值将不会被插入表中。

  从数据库中读取,是通过方法,向它传递你的选择标准和你想要的列。

  这个方法结合了和方法的元素,只不过它的列的表指定了你想取出的数据,而不是插入的数据。

  查询结果是由一个对象的形式返回给你的。

  想要看一个Cursor中的一行,可以使用  类中的各种move方法中的一个,当你开始读取值时你必须先调用它。

  一般情况下,你应该先调用方法,它将把读取位置指向结果中的第一项。

  对每一行,你可以通过Cursor类的get方法读取各列内容,比如 或 。

  对每一个get方法,你必须指定你想要的列的索引,你可以通过  或 方法来获得索引。

  要删除表中的行,你需要提供一定的选择标准来确定要删除的行。

  数据库API提供了一种机制,用于创建选择标准,防止SQL注入。

  该机制将选择标准分为一个选择从句和一些选择参数。这个从句定义了需要查看的列,也允许你结合列的测试。这些参数是和从句绑定的需要测试的值。

  因为结果不像常规的SQL语句那样处理,所以它是防SQL注入的。

  当你需要修改你的数据库中值的一个子集时,运用方法。

  更新数据表结合了方法中的content

}
  • mybatis官方推荐使用mapper代理方法开发mapper接口,程序员不用编写mapper接口实现类,使用mapper代理方法时,输入参数可以使用pojo包装对象或map对象,保证dao的通用性。

注意: MyBatis 将按照下面的顺序来加载属性:

  • properties元素体内定义的属性首先被读取。
  • 然后会读取properties元素中resourceurl加载的属性,它会覆盖已读取的同名属性。
  • 最后读取parameterType传递的属性,它会覆盖已读取的同名属性。
  • 因此,通过parameterType传递的属性具有最高优先级,resourceurl加载的属性次之,最低优先级的是properties元素体内定义的属性。

mybatis全局配置参数,全局参数将会影响mybatis的运行行为。

<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->

类型处理器用于java类型和jdbc类型映射,如下:

mybatis 自带的类型处理器基本上满足日常需求,不需要单独定义。

任何兼容的数字或字节类型
任何兼容的数字或短整型
任何兼容的数字或长整型
任何兼容的数字或单精度浮点型
任何兼容的数字或双精度浮点型
任何兼容的数字或十进制小数类型
VARCHAR-任何兼容的字符串类型,作为代码存储(而不是索引)。

Mapper 配置的几种方法:

#{}实现的是向prepareStatement 中的预处理语句中设置参数值,sql 语句中#{}表示一个占位

使用占位符#{}可以有效防止sql 注入,在使用时不需要关心参数值的类型,mybatis 会自动
传输单个简单类型值,#{}括号中可以是value 或其它名称。
换, ${}可以接收简单类型值或pojo 属性值,如果parameterType 传输单个简单类型值,${}
括号中只能是value。使用${}不能防止sql 注入,但是有时用${}会非常方便,如下的例子:

如果本例子使用#{}则传入的字符串中必须有%号,而%是人为拼接在参数中,显然有点麻烦,如果采用${}在sql 中拼接为%的方式则在调用mapper 接口传递参数就方便很多。

// 如果使用占位符号则必须人为在传参数中加%
// 如果使用${}原始符号则不用人为在参数中加%

再比如order by 排序,如果将列名通过参数传入sql,根据传的列名进行排序,应该写为:
如果使用#{}将无法实现此功能。

Mybatis 使用ognl 表达式解析对象字段的值,如下例子:

上边红色标注的是user 对象中的字段名称。

// 构造查询条件user对象 // 传递user对象查询用户列表

开发中通过pojo 传递查询条件,查询条件是综合的查询条件,不仅包括用户查询条件
还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。

定义包装对象将查询条件(pojo)以类组合的方式包装起来。

<!-- 查询用户列表 根据用户名称和用户性别查询用户列表 -->

Sql 映射文件定义如下:

异常测试: 传递的 map 中的 key 和 sql 中解析的 key 不一致。 测试结果没有报错,只是通过 key 获取值为空。

4.2.1输出简单类型

参考 getnow 输出日期类型,看下边的例子输出整型:

总结: 输出简单类型必须查询出来的结果集有一条记录,最终将第一个字段的值转换为输出类型。 使用 session 的 selectOne 可查询单条记录。

// 如果使用占位符号则必须人为在传参数中加% // 如果使用${}原始符号则不用人为在参数中加%

resultType 可以指定 pojo 将查询结果映射为 pojo,但需要 pojo 的属性名和 sql 查询的列 名一致方可映射成功。 如果 sql 查询字段名和 pojo 的属性名不一致,可以通过 resultMap 将字段名和属性名作 一个对应关系 ,resultMap 实质上还需要将查询结果映射到 pojo 对象中。 resultMap 可以实现将查询结果映射为复杂类型的 pojo,比如在查询结果映射对象中包 括 pojo 和 list 实现一对一查询和一对多查询。

<!-- 查询用户列表 根据用户名称和用户性别查询用户列表 -->
<!-- id标签:将查询结果集的唯一标识列(主键或唯一标识)

通过 mybatis 提供的各种标签方法实现动态拼接 sql。

}

我要回帖

更多关于 java中的long型变量 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信