YuMingzhe's blog

Writing and Teaching is my way of Learning

金融知识笔记

多头市场和空头市场。多头,简单来说就是特指买入股票的投资者,而多头市场就是买入股票的人比卖出股票的人多,也就是传说中的牛市。然后与多头市场对应的是空头市场,也就是所谓的熊市。


债券和股票的区别:

  • 发行主体无论是国家,地方公共团体还是企业,都可以发行债券,股票只能是股份制企业才可以发行。
  • 收益稳定性,债券在购买前。利率已定,到期获得固定利息;股票一般在购买前不定股息率,股息收入随股份公司盈利情况而定。
  • 保本能力,债券到期可回收本金;股票本金一旦交给公司就不能收回。公司一旦破产,还要看公司剩余资产清盘状况。
  • 经济利益关系不同债券表示的是对公司的一种债权;股票表示的是对公司的所有权。

头寸,就是款项的意思,是金融界及商业界的流行用语。证券,股票,期货交易中经常用到。比如股票,期货中常常讲的多头空头其实也是多头头寸,空头头寸的简称。头寸一词在银行也经常用到:

  • 银行当日的全部收付款中收入大于支出款项的情况,称为”多头寸“;如果付出款项大于收入款项,称为”缺头寸“;
  • 到处想方设法调进款项的行为称为”掉头寸“。
  • 如果资金需求量大于闲置量时就称为”头寸紧“。

风险投资与天使投资的区别

  • 投资阶段不一样.天使投资大部分时候投资一个企业的初创阶段,或是项目刚启动阶段,投资额相对较小.风险投资往往是企业或项目已经运营一段时间后,或许是产品经过了市场检验,或许是品牌有了一定的知名度,它才进来让企业做强做大,投资额较大.
  • 天使投资往往是个人投资,风险投资一般是机构投资。
  • 对项目的要求也不太一样。风险投资的门槛会更高,因为他们要对出资人负责,不但更严谨,监管也不一样。

停牌。当有突发事件可能影响证券交易的正常进行时,相关股票可能主动或者被动的暂停交易。例如媒体突然发布了某个公司的重大负面新闻,为了保护公司的股价,一般会先停牌,等公司澄清以后再恢复交易。那停牌是否都是有坏事发生呢?那可不一定,当上市公司有重组、收购等重大事件要发生时,一般也会主动停牌,等消息发布以后再复牌,而这类事件通常也是利好的,另外,发布重要业绩数据前也会停牌。那停牌一般需要多久呢?看到股票停牌了,查查该股票最新的几条公告,一般都能了解停牌的原因,短的1~2天,长的几个月也有。

OTC(场外交易市场,又称柜台交易市场),泛指在交易所之外进行交易的市场。OTC 没有固定的场所,没有规定的成员资格,没有严格可控的规则制度,没有规定的交易产品和限制,主要是交易对手通过私下协商进行的一对一的交易。在 OTC 市场交易的是未能在证券交易所上市的证券。在我国建立柜台交易市场,能为数百万计达不到上市条件的企业提供股权交易平台,有利于中小企业发展,也有助于我国形成一个多层次的资本市场。

负利率,指物价指数快速攀升,导致银行存款利率实际为负。简单来说,就是存在银行里的钱,即使算上利息,价值也会越来越少。比如,一件 1000 元的商品,一年后可能涨到 1065 元,但是 1000 元存在银行,一年后才 1035 元,存钱不赚反赔,存银行的利率还赶不上通货膨胀率。

市政债券是一种以城市政府为发债主体,或以城市政府下属部门或机构(如污水处理厂、水务公司、城市基础设施和管理公司等)为发债主体,向公众公开发行的债券。主要用于地方城市基础设施建设或公益设施建设,如道路、桥梁、供水、污水处理、垃圾处理、教育设施或其他公益设施等。

征信就是增进信用。手段多样,其中第三方担保、抵质押担保、债券保险、债券信托,信用准备金等最为常见。银行贷款常需要用到增信。大企业不仅容易获得贷款,利息也更低,这是因为其信用等级较高。而信用等级相对较低的中小企业,为了获得贷款和降低融资成本,往往需要引入优质企业和担保公司为其担保,以增加信用等级。

银根一词往往被用来借喻中央银行的货币政策:中央银行为减少信贷供给,提高利率,消除因需求过旺而带来的通货膨胀压力所采取的货币政策,称为紧缩银根。反之,为阻止经济衰退,通过增加信贷供给,降低利率,促使投资增加,带动经济增长而采取的货币政策,称为放松银根。

银行间债券市场。全国银行间债券市场是指依托于全国银行间同业拆借中心和中央国债登记结算公司的,包括商业银行、农村信用联社、保险公司、证券公司等金融机构进行债券买卖和回购的市场。经过近几年的迅速发展,银行间债券市场目前已成为我国债券市场的主体部分。记账式国债的大部分、政策性金融债券都在该市场发行并上市交易。

银行业间同业拆借市场,亦称同业拆放市场。是指金融机构之间以货币借贷方式进行短期资金融通的市场。通俗地讲,就是金融机构间互相借钱的市场。

企业债与公司债的区别 * 发行主体公司债是由股份有限公司或有限责任公司发行的债券;企业债,是由中央政府部门所属机构,国有独资企业或国有控股企业发行的债券。 * 定价:公司债采用核准制,由证监会进行审核,由发行人与保荐人通过市场询价确定发行价;企业债则由发改委审核。 * 发行:公司在可一次核准,多次发行。根据《证券法》规定,股份有限公司、有限责任公司发债额度的最低限度分别约为1200万元和2400万元。企业债审批后要求一年内发完,发债额度不低于10亿元。 * 信用:负债的信用来源是发债公司的资产质量、经营状况、盈利水平等;企业债通过国有机制贯彻了政府信用,而且通过行政强制落实担保机制,实际信用级别与其他政府债券相差不大。

金融脱媒是指资金供给绕开商业银行等媒介体系,直接输送到需求方和融资者手里,造成资金的体外循环。例如,企业直接在市场发债,发股票,或者短期商业票据,而不是从商业银行取得贷款。从融资方式看,金融脱媒是社会融资逐渐由间接融资向直接融资转变的过程。应该说,金融脱媒现象是我国市场经济和国民经济发展的客观规律,是政府推动金融市场创新发展的必然趋势。

市盈率指在一个考察期(通常为12个月的时间)内,股票的价格和每股收益的比率。投资者通常利用该比例值估量某股票的投资价值。一般来说,市盈率的倒数就是投资回报率。一只股票,如果市盈率是25倍,回报率就是4%,这就有可能跑不赢通货膨胀,你需要25年才能回收投资。

离岸金融是指以自由兑换货币为交易媒介,非居民(境外的个人、法人、政府机构、国际组织等)参与为主,提供结算、借贷、资本流动、保险,信托,证券和衍生工具交易等金融服务,且不受市场所在国和货币发行国一般金融法律法规限制的金融活动。离岸金融的主要业务是吸收非居民的资金,并服务于非居民融资需要,因此被形象的喻为“两头在外”的金融业务,所形成的市场称之为离岸金融市场。目前,我国的离岸金融业务尚处于探索起步阶段。

政策性银行一般是指由政府设立,以贯彻国家产业政策、区域发展政策为目的的,不以盈利为目标的金融机构。1994年,我国组建了三家政策性银行——国家开发银行,中国进出口银行和中国农业发展银行。国家开发银行于2008年12月16日转为商业银行。

影子银行,一般是指那些有着部分银行功能,却不受监管或少受监管的非银行金融机构。简单理解,影子银行是那些可以提供信贷,但是不属于银行的金融机构。在中国,影子银行主要包括信托公司、担保公司、典当行、地下钱庄、货币市场基金、各类私募基金、小额贷款公司以及各类金融机构理财等表外业务、民间融资等。特征:机构众多,规模较小、杠杆化水平较低但发展较快。

路演,是指证券发行商发行证券前针对机构投资者的推介活动。目的是促进投资者与股票发行人之间的沟通和交流,以保证股票的顺利发行。活动中,公司向投资者就公司业绩,产品,发展方向等作详细介绍,充分阐述上市公司的投资价值,让准投资者们深入了解具体情况,并回答机构投资者关心的问题。

金融期货是指交易双方在金融市场上已约定的时间和价格,买卖某种金融工具的具有约束力的标准化合约。以金融工具为标的物的期货合约。金融期货一般分为三类,货币期货,利率期货和指数期货。金融期货作为期货中的一种,具有期货的一般特点,但与商品期货相比较,其合约标的物不是实物商品,而是传统的金融商品,如证券,货币,利率等。

仓位是指投资人实有投资和实际投资资金的比例。举个例子:假如你有10万元用于投资,现用了4万元买基金或股票,你有的仓位是40%。如果你全买了基金或股票,你就满仓了。如你全部赎回基金卖出股票,你就空仓了。根据市场的变化来控制自己的仓位,是炒股非常重要的一个能力,如果不会控制仓位就像打仗没有后备部队一样,会很被动。

概念股是指具有某种特别内涵的股票,与业绩股相对而言的。业绩股需要有良好的业绩支撑,而概念股是依靠某一种题材比如资产重组概念,三通概念等支撑价格。而这一内涵通常会被当作一种选股和炒作题材,成为股市的热点。概念股是股市术语,作为一种选股的方式。相较于业绩股必须有良好的运营业绩所支撑,概念股只是以依靠相同话题,将同类型的股票列入选股标的的一种组合。由于概念股的广告效应,因此不具有任何获利的保证。

资产证券化是以特定资产组合或特定现金流为支持,发行可交易证券的一种融资形式。传统的证券发行是以企业为基础,而资产证券化则是以特定的资产池为基础发行证券。在资产证券化过程中发行的以资产池为基础的证券就称为证券化产品。资产证券化是指将缺乏流动性的资产转换为在金融市场上可以自由买卖的证券行为,使其具有流动性。其主要特点包括: 1. 利用金融资产证券化可提高金融机构资本充足率; 2. 增加资产流动性,改善银行资产与负债结构失衡; 3. 利用金融资产证券化来降低银行固定利率资产的利率风险; 4. 银行可利用金融资产证券化来降低筹资成本; 5. 银行利用金融资产证券化,可使贷款人资金成本下降; 6. 金融资产证券化的产品收益良好且稳定。

《当呼吸化为空气》读书笔记

科技发展日新月异,无论是临床还是研究工作,一旦怠惰,很快就会被新科技的潮流淹没。必须以”不允许自己犯任何错误“的完美标准鞭策自己不断学习新的知识和技术。

医生的工作确实和铁路工人没有什么区别,最终都只是把人们带到他们想要到达的地方而已。

生命的意义不只是单纯的对金钱和地位的追求而已。在生命的终点线前,回看人对虚名浮华的追逐,会发现这些都只是捕风捉影而已。

但既然忠于自己的生命意义,也就没有什么好害怕的,只要义无反顾地往前走就好。何况生命本就充满变化,每个人生命的意义也时刻都在发生转变。

生命的意义包罗万象,但每个单一的生命点,最终都是为了桥接过去和未来而存在着。

当下的我是有限的,未来的我们却是无穷的。生命本身的存在和延续就赋予了生命不可剥夺的意义在里头,一种近似返璞归真的存在主义。

生命是否有意义,某种程度上要看我们建立的关系的深度。就是人类的关联性加强了生命的意义。

呼吸急促的毛病,这些都说明她逐渐走向充血性心力衰竭,老化的血液从老化的肺老化的组织中能运送的氧气大大减少。

我们无从得知降生世上将遭遇怎样的冲突与痛苦,但通常来说我们很难脱身其外。

在鲜血和沮丧之间极富英雄主义精神的责任感才是一个医生真正的形象。

聚集在那儿的一大家子,十几口人,全都欢呼雀跃,一阵纷乱的握手和互相拥抱。我就像个伟大的先知,从山顶带回新契约的欢乐消息。

有一天我们诞生,有一天我们死去,同样的一天,同样的一秒钟……他们让新的生命诞生在坟墓上,光明只闪现了一刹那,跟着又是黑夜。

选择工作的时候,当然要把生活方式放在第一位。

你的生活即将改变——已经改变了。这是一场长途旅行,你明白吗?你们必须相互陪伴支持,但需要的时候你也要好好休息。这种病要么让你们更团结亲密,要么让你们彻底决裂。所以,现在你们要给彼此前所未有的支持和陪伴。

那些集合了生命、死亡与意义的问题,那些所有人在某个时候都必须要面对的问题,通常都发生在医院里。当一个人真正遇到这些问题,这就变成了实践,有着哲学和生物学上的双重意义。人类是生命体,遵循自然法则,很遗憾的是,这些法则就包括一条:熵总是在增大,生命是无常的。

“arete”,是一种道德、情感、思维和身体上都至臻卓越的美德。

我选择医疗事业,部分原因是想追寻死神:抓住他,掀开他神秘的斗篷,与他坚定的四目相对。

专业技术出色是不够的。人人终有一死,作为一名住院医生,我的最高理想不是挽救生命,是引导病人或家属去理解死亡或疾病。

我们在此共聚一堂,一起走过接下来的路。我承诺尽自己所能,引导你走向彼岸。

我没有哪一天哪一秒质疑过自己为什么选择这份工作,或者问自己到底值不值得。那是一种召唤,保卫生命的召唤,不仅仅是保卫生命,也是保卫别人的个性,甚至是保卫灵魂也不为过。这种召唤的神圣之处,是显而易见的。

背负起别人的十字架,你总有时候会被重负压垮。

科学,实在是最充满政治性、竞争最激烈、最你死我活的行业,处处布满了走捷径的诱惑。

无聊,就是感受到时间的流逝。

失败的痛苦让我明白,专业技术上的出类拔萃,其实是道德要求。光有一颗好心是不够的,关键还是要靠技术。有时候一两毫米的差距,可能就是悲剧与胜利的分水岭。

亚历山大·蒲柏说过:“一知半解最危险;饮则深透畅饮,否则尝不到知识的甘泉。”

达尔文和尼采才有一个观点是一致的:生物体最重要的特征就是奋斗求生。没有奋斗的人生,就像一幅画里身上没有条纹的老虎。多年来与死亡并肩而行的经历,让我更深刻地懂得,最轻易的死亡有时候并非好的结局。

道德义务是有重量,有重量的东西就有引力,所以道德责任的引力又将我拉回手术室。

你永远无法到达完美的境地,但通过不懈的努力奋斗和追求,你能看见那无限接近完美的渐进曲线。

人真正的生命是在头二十年,剩下的不过是对过去日子的反射。

Java 9 模块化入门

背景

本篇文章,我们将了解下 Java 9 带给我们的新特性—— Java 平台模块化系统(JPMS, Java Platform Module System),项目代号为 Jigsaw。我们都知道 Java 自 1995 年发布以来已经在上亿的设备上运行过,无论是体积庞大的大型机服务器还是只有手掌大小的嵌入式设备都能看到 Java 的身影,而随着 Java 平台的不断演进,Java 代码库也越发的庞大和臃肿,这样就很难在资源非常有限的 IoT 设备和嵌入式设备上部署 Java 应用,因此对 Java 平台进行模块化也越发的成为 Java 未来发展的首要任务之一。下面我们先看一下它的诞生历程:

Java 平台模块化从提出到出现在我们面前,可谓命途多舛,早在 2005 年 Java 7 时代,就以 JSR 277: Java Module System 规范的形式提出了,号称要在 Java 8 中出现,给 Java 带来历史上可以与 Java 5 相媲美的一次重要里程碑式的更新,之后又被 JSR 376: Java Platform Module System 规范所取代,可是由于该项目进度缓慢,困难重重,导致了 Java 8 发布时间一拖再拖,最终等不起了只好先把 Jigsaw 功能又延期到 Java 9,先将 Java 8 发布出来供大家使用,虽然没有 Jigsaw 特性,但 Java 8 也是一款非常重量级的更新,它带来了 Lambda、Stream 和 改进的日期时间 API 等功能特性,让程序员享受到函数式和流式编程的乐趣。而到了后 Java 8 时代,眼看 Java 9 的发布截止日期马上就要到了,可是 Jigsaw 项目仍然被许多问题困扰,如设计上的问题,过多的 bug 还没解决,因此又延期了,最终 Java 平台首席架构师 Mark Reinhold 下了最后通牒,要求 Java 9 必须在 2017 年 9 月份发布,在还有半年就要发布前,Jigsaw 项目发布了一版 Public Review 版,但是遭到了IBM 和 Redhat 的反对,遭到反对的原因在于他们认为模块化破坏了现有系统的设计原则,会导致现有系统迁移到新的模块化系统上的开销会非常巨大(其实是为了各自的利益所考虑的,这两家公司都有自己的中间件,而为了遵循统一的标准可能改动会比较大),具体信息请看这篇文章 Concerns Regarding Jigsaw。在与这两家经过协商和妥协后,Java 模块化系统终于在 2017 年 09 月 21 日发布了,模块化在经历了多重困难后终于与我们相见。下面我们将了解一些最基本的概念,实验环境:Java 9.0.1。

模块是什么

我们都知道在 Java 9 之前代码都是按包进行组织的,而模块则是在包的概念上又增加了一个更高层的抽象,它将多个逻辑上、功能上相关的包以及相关的资源文件,如 xml 文件等组织成一个可重用的逻辑单元,这个逻辑单元就称为模块。通常情况下一个模块包含着一个模块描述符文件(module descriptor)用来指定模块的名字、依赖(需要注意的是每个模块必须显式得声明其依赖)、对外可使用的包(其余的包则对外不可见)、模块提供的服务、模块使用的服务以及允许哪些模块可以对其进行反射等配置信息。模块最终都会打包成 jar 包来分发和使用的,那么模块打包的 jar 包和普通的 jar 包有什么区别呢?模块和普通的 jar 包几乎是一样的,只不过比普通的 jar 包在根目录下多了一个模块描述符文件—— module-info.class而已。

模块的类型

Java 9 的模块可以分为 3 种类型

1.具名模块(Named Module)

具名模块也称为应用模块(Application Module),通常在模块根目录下有 module-info.java 文件的话,那么该模块就称为具名模块,我们编写的模块一般都属于这种类型,还有很多第三方的依赖也可以归类于此,只要第三方依赖的维护者将库迁移到 Java 9 的模块即可,那么我们就可以在自己的模块中引用这些依赖。

2.无名模块(Unnamed Module)

无名模块指的就是不包含 module-info.java 的 jar 包,通常这些 jar 包都是 Java 9 之前构建的。无名模块可以读取到其他所有的模块,并且也会将自己包下的所有类都暴露给外界。需要注意的是无名模块导出了所有的包,但并不意味着任何具名的模块可以读取无名模块,因为具名模块在 module-info.java 中无法声明对无名模块的依赖,无名模块导出所有包的目的在于让其他无名模块可以加载这些类。但是无名模块存在一个问题,假如我们需要依赖某个第三方的构件,但这个依赖还没有迁移到 Java 9 模块化,那么我们就无法引用其中的类,我们就无法编写应用了,难道我们要一直等到依赖迁移完成才能使用吗?请看下面的自动模块。

3.自动模块(Automatic Module)

任何无名模块(没有 module-info.java 的模块)放到模块路径(module path)上会自动变为自动模块,允许 Java 9 模块能引用到这些模块中的类。自动模块对外暴露所有的包并且能引用到其他所有模块的类,其他模块也能引用到自动模块的类。由于自动模块并不能声明模块名,那么 JDK 会根据 jar 包名来自动生成一个模块名以允许其他模块来引用。生成的模块名按以下规则生成:首先会移除文件扩展名以及版本号,然后使用".“替换所有非字母字符。例如 spring-core-4.3.12.jar 生成的模块名为 spring.core,那么其他模块就可以通过 requires spring.core 来引用其中的类。

多租户架构浅析

众所周知,云计算可以划分为以下几个层次的服务——IaaS、PaaS和SaaS,而今天我们今天讲的多租户架构就是一种常见的 SaaS 软件架构模式,或者说是商业模式。 通常,一个多租户软件指的是依托云计算的弹性环境,搭建并使用一个单一的应用程序实例来服务多个客户,每个客户称之为“租户”来共享同一个软件,很像现在很火的共享经济,客户们都只是来租用系统,按时收费,客户是不需要提供或关心软件的运行环境,只要开通账号即可使用,方便快捷。举个例子,假如我们推出了一款财务软件,可以为企业提供财务方面的管理功能,按照传统的部署方法,我们通常将程序、数据库等组件都部署在客户的服务器上,这需要企业有自己的机房和服务器并配备有相关专业知识的运维人员,但如果程序或其他组件出现 bug 等严重故障时,我们就需要派遣工程师到客户现场进行救援恢复工作,耗时耗力,而有了以云计算为依托的多租户架构软件,那么我们就可以将这套财务软件部署在自己的机房内,只要让“租户”注册账号,通过互联网即可访问系统,同时“租户”们的数据相互隔离,只能看到自己的数据,软件的安全性也得到了保障。最重要的是,当系统出现故障时,工程师可以快速定位问题,并将最新的补丁应用于系统上,将“租户”服务中断时间压缩到最小,而不用花费大量的时间到现场进行排错,同时也不必派遣工程师出差,为企业运维降低了成本。 由此可见,多租户架构的软件是云计算时代软件研发的一个重要方向,不仅对客户提供了便捷,也节省了企业的成本,是一个双赢的方案。

上面有句话可能不太严谨,通常只有小型的多租户程序才只部署一个单一的运行实例,这种情况只适用于租户较少、服务器压力负载低的用例,此时租户的费用支出较为低廉,适合预算有限的客户。但这种部署方案存在缺点就是由于多个租户公用一套程序,一旦程序有 bug,那么所有的租户都会受到影响,且没法为单独租户提供定制,灵活性低,不过一分价钱一分货。其部署架构如下图所示:

而对于中大型规模的应用来说,只部署一个实例就略显单薄,并且也无法承载大量的用户,这时就需要部署多个实例,并在多实例的基础上进行负载均衡以保证性能。最重要的是,多实例的部署架构允许为每个租户定制代码,提供特色服务,其部署架构如下图所示:

由于大型的多实例方案中需要加入负载均衡以及受商业、业务相关的因素的影响,会导致在多租户架构之外包裹这更复杂的业务设计,因此本文就只讨论单实例方案下的多租户架构。从上面的介绍可知,在传统的部署方案中,客户数据始终是保留在自己的手中,而在云计算时代,多租户系统为用户提供了一个集中式的数据存储方案,这需要说服用户将他们的数据保留在云端,因此多租户系统最为关键也是客户最关心的便是数据的安全性,因此本文对多租户系统的架构分析也是从数据的安全性方面作为切入点。由于用户数据是集中存储的,数据的安全性就是能否实现对数据的隔离,防止数据不经意或被他人恶意地获取,多租户系统架构主要有3种方案实现数据的隔离,即为每个租户提供独立的数据库、独立的表空间或按字段区分租户,下面我们依次讲解这3种方案。

JPA 处理 N+1 问题的办法

N+1 问题

在使用 JPA 开发过程中,项目中会存在大量的实体类,而实体间的关系通常有一对多、多对多的情况,我们通常将多的一方的加载方式设置成”延迟加载“,即当我们查询某个实体时只是查询出了实体的基本属性,与之相关联的记录并没有加载,这就会导致一个问题,当我们需要关联对象的某些属性时,就会触发 ORM 发出 SQL 语句与将关联的记录查询出来,这就是臭名昭著的 N+1 问题。举个例子,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 教师类
@Entity
@Table(name = "teacher")
public class Teacher {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;
   @Column(name = "name_")
   private String name;
   public Teacher(String name) {
      this.name = name;
   }
   @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
   @JoinColumn(name = "teacher_id_")
   private List<Student> students;
}
// 学生类
@Entity
@Table(name = "student")
public class Student {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;
   @Column(name = "name_")
   private String name;
}

上面的代码中包含了两个非常简单的实体:老师和学生,老师和学生是单向一对多关系的。

当我们查询出某个老师的记录后,再访问其“学生”属性,就会产生一条 SQL 语句去查询,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建EM
EntityManagerFactory factory = Persistence.createEntityManagerFactory("hibernate");
entityManager = factory.createEntityManager();
entityManager.getTransaction().begin();
// n+1查询
Teacher teacher = entityManager.find(Teacher.class, 1);
List<Student> students = teacher.getStudents();
for (Student student : students) {
    System.out.println(student);
}
// 关闭资源
entityManager.getTransaction().commit();
entityManager.close();
factory.close();

上面的代码会产生下面的 SQL 日志:

1
2
10135 Query select teacher0_.id as id1_1_0_, teacher0_.name_ as name_2_1_0_ from teacher teacher0_ where teacher0_.id=1
10135 Query select students0_.teacher_id_ as teacher_3_0_0_, students0_.id as id1_0_0_, students0_.id as id1_0_1_, students0_.name_ as name_2_0_1_ from student students0_ where students0_.teacher_id_=1

从日志中可以看出产生了 2 条SQL,设想一下,假如我们先通过一条语句查询出 100 个老师的记录,然后分别访问每个老师所关联的学生,那么将会再产生 100 条查询学生记录的 SQL 语句,一共 1+100 条语句,这就会给应用的性能带来极大的损耗,光是 100 条语句在应用和数据库间的通讯就会消耗大量的时间,更别说再进行复杂的业务数据处理所花费的时间了。如果没有 ORM 框架,我们可以用原生SQL通过一条 join 语句就可以将 100 个老师及其关联的学生记录查询出来,那么使用 ORM 框架将如何避免这类问题呢,下面我们就是用 JPA 2.1 提供的特性来处理 N+1 问题,测试环境为 JPA 2.1(Hibernate 5.2.10.Final) + MySQL 5.6,代码依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.2.10.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.2.10.Final</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.43</version>
</dependency>

JPA 提供了 2 种方式用于处理延迟加载问题,一个是“fetch join”,另一个是 EntityGraph,当然我们也可以调用原生 SQL 来实现,不过就丧失了可移植性,下面我们就分别讲解这两种技术:

命令式与声明式编程

大家可能都听说过命令式和声明式编程这两个概念,它们都是用于描述一种编程的方式,或者代码风格,如果对这两个概念还不了解的话,百度下就能得到它们的定义:

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

如果我们对这两种编程风格很熟悉的话,这个定义自然很好理解,但是对于初学者来说,这个定义很抽象,根本想象不到 how 和 what 到底是什么意思。下面我将以生活中的例子做类比,给大家讲解下命令式和声明式编程的概念,最后再辅以代码演示下这两种编程风格。

首先,要知道的是我们使用的大部分高级编程语言都属于命令式编程的范畴,比如我们常用的 C,C++,Java 语言等,而对声明式编程可能比较陌生,其实我们每天也都接触声明式编程,只是大家没有对它有个明确的定义罢了,最常见的声明式编程语言有 SQL,HTML 等,还有一些编程语言这两种风格都支持,属于混合型的,如 JavaScript、Python 等。

想要讲清命令式和声明式编程,我们还是从它们的定义入手。为了便于理解,我们用现实中的例子来类比下:假设你和女友周末晚上约会去一家餐厅吃饭,进入餐厅后要选一个桌子坐下,下面就分别用命令式和声明式的方式来模拟找座位的场景:

  • 命令式:你和女友走进餐厅,环顾了下四周,发现有几个空座,有的空座旁边是几个东北大哥在胡吃海塞,声音吵闹,显然不适合俩人世界,有的位置附近有人在抽烟也不适合,经过多次比较,最终你们发现角落里的那个位置比较幽静,光线柔和,很有气氛,于是你们便选定了那个位置。
  • 声明式:你和女友走进餐厅,对服务员说:“麻烦你,我们想要一个比较幽静的位置”,于是服务员很快的找到了一个符合条件的位置并把你们引导过去。

从上面的例子可以看出命令式想要达成目标(找到合适的座位)需要我们全程的参与,身体力行,比如我们要挨个考察每个空位是否符合我们的要求,即如何去做(how)。而声明式只需将符合我们期望的条件告诉第三人,让他替我们实现目标即可,很省事,这里就是告诉别人你想要的是什么(what)。这就是命令式与声明式编程在“行事”方面的区别。

Hiberntae 5 新特性之一次查询多个实体

在使用 JPA 的过程中,我们可能会经常遇到这样的场景:我们要通过多个主键查询实体,可是遗憾的是,JPA 并未定义这样的接口允许我们通过主键加载多实体。在 Hibernate 5 之前,我们可以通过下面 2 个方法进行曲线救国:

1.通过在一个循环里多次调用 EntityManager.find() 方法,一次查询一个实体,缺点是会生成较多的 SQL,查询的时间跟查询数量成正比,速度比较慢,时间全浪费在与数据库的通信上了

2.创建一个JPQL语句,将所有的主键放到 IN 语句中,如下面代码所示:

1
2
List<Long> ids = Arrays.asList(new Long[]{1L, 2L, 3L});
List<PersonEntity> persons = em.createQuery("SELECT p FROM Person p WHERE p.id IN :ids").setParameter("ids", ids).getResultList();

此方法虽然在性能上没啥问题,但仍有以下缺点:

  • 1.有些数据库如 Oracle 对 IN 子句的参数个数有限制,如果传入的主键数量太多,会产生 SQL 语法级别的错误
  • 2.如果传入的主键数据量较大,那么一次抓取可能会产生巨大的流量,造成系统性能颠簸
  • 3.在使用这种方式时,Hibernate 根据 JPQL 生成 SQL 并从数据库中加载所有实体时,并不会检查一级缓存(session)中是否已经缓存某些实体,重复加载对象

上面提到的这些问题我们都可以通过代码控制得以解决,但这些代码都是非业务逻辑的代码,将会分散在所有需要性能考虑的地方,增加了代码的复杂度、难以维护。因此,Hibernate 5.1 引入了新的 API 扩展了原有 Session 的功能,让我们可以通过简单的调用接口即可轻松的实现一次加载多个实体并且还解决了性能相关的问题。 下面我们将演示新的 API 用于一次加载多个实体,我演示的环境是:JPA 2.1 + Hibernate 5.2.2。

首先,我们假设有一个实体,叫做 Student,如果想要一次通过主键加载多个 Student 的记录的话,可以通过下面的代码实现:

1
2
3
4
5
6
7
8
9
10
11
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("pu");
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 将em解封为底层实现,即hibernate的session
Session session = entityManager.unwrap(Session.class);
session.getTransaction().begin();
// 新api,可以一次加载多个实体
List<Student> students = session.byMultipleIds(Student.class).multiLoad(2,3,4,5,6);
System.out.println(students.size()); // 输出5,说明新api确实能够实现一次多实体加载
session.getTransaction().commit();
entityManager.close();
entityManagerFactory.close();

从上面的代码可以看出,我们调用 session 的 byMultipleIds()方法,并提供实体的类作为参数,会返回一个类型,然后再调用 multiLoad() 方法,并传入主键即可实现一次加载多个实体的效果。此时,Hibernate 生成的 SQL 和上面方案 2 中 JPQL 生成的语句一样,主键都放在了 in 子句中,如下所示:

1
14:32:57,602 DEBUG SQL:92 – select personenti0_.id as id1_0_0_, personenti0_.firstName as firstNam2_0_0_, personenti0_.lastName as lastName3_0_0_ from Person personenti0_ where personenti0_.id in (?,?,?)

从上面生成的结果可以看出,Hibernate 生成的 SQL 和我们自己手写 JPQL 生成的 SQL 并无不同,但我们只是提供了少量的 id,如果 id 数量变大,就需要考虑批处理了。

批量加载实体

批量加载有如下好处:

不是所有数据库都允许 IN 子句有无限个参数;为了将内存占用量控制在一定范围内,我们希望在加载下一批次的实体前要从1级缓存中移除上一批次的对象;我们想要在业务逻辑中检测是否加载的实体是我们需要的。

Hibernate 默认的批大小是与我们配置的数据库方言(dialect)相关联的,因此我们不必担心生成的sql语句会违反数据库的限制。但是 Hibernate 仍为我们提供了接口用于更改批大小,示例如下:

1
List<PersonEntity> persons = session.byMultipleIds(PersonEntity.class).withBatchSize(2).multiLoad(1L, 2L, 3L);

从下面生成的日志可以看出,如果我们传入的 id 数量大于批大小,那么 Hibernate 会生成多个 select 语句:

1
2
15:20:52,314 DEBUG SQL:92 – select personenti0_.id as id1_0_0_, personenti0_.firstName as firstNam2_0_0_, personenti0_.lastName as lastName3_0_0_ from Person personenti0_ where personenti0_.id in (?,?)
15:20:52,331 DEBUG SQL:92 – select personenti0_.id as id1_0_0_, personenti0_.firstName as firstNam2_0_0_, personenti0_.lastName as lastName3_0_0_ from Person personenti0_ where personenti0_.id in (?)

不加载已经缓存的实体

如果我们通过 JPQL 的方式批量加载实体,Hibernate 是无法检测实体是否已经存在于 session (一级缓存)中,因此可能会导致不必要的加载。由于 Hibernate 引入了新的 MultiIdentifierLoadAccess 接口,可以让 Hibernate 在加载前检测实体是否已经处于缓存中,但是这个特性默认是关闭的,我们要通过 enableSessionCheck(boolean enabled) 开启,示例代码如下:

1
2
3
4
PersonEntity p = em.find(PersonEntity.class, 1L);
log.info("Fetched PersonEntity with id 1");
Session session = em.unwrap(Session.class);
List<PersonEntity> persons = session.byMultipleIds(PersonEntity.class).enableSessionCheck(true).multiLoad(1L, 2L, 3L);

日志如下:

1
2
3
15:34:07,449 DEBUG SQL:92 – select personenti0_.id as id1_0_0_, personenti0_.firstName as firstNam2_0_0_, personenti0_.lastName as lastName3_0_0_ from Person personenti0_ where personenti0_.id=?
15:34:07,471 INFO TestMultiLoad:118 – Fetched PersonEntity with id 1
15:34:07,476 DEBUG SQL:92 – select personenti0_.id as id1_0_0_, personenti0_.firstName as firstNam2_0_0_, personenti0_.lastName as lastName3_0_0_ from Person personenti0_ where personenti0_.id in (?,?)

从代码中可以看出我们首先加载了 id 为 1 的实体,然后我们再批量加载 id 为 1,2,3 的实体,但是 Hibernate 检测到 id 为 1 的实体已经在缓存中了,生成的 SQL 则只加载剩余的两个实体。

总结

在开发中按主键批量加载实体情况还是比较常见的,我们可以简单的通过 JPQL 进行实现,但要考虑生成的 SQL 是否能符合数据库的限制,以及在性能上是否存在问题。Hibernate 引入的 MultiIdentifierLoadAccess 接口为开发人员提供了开箱即用的功能,让我们能通过简单的调用 API 即可实现性能优良的功能,把关注点更多的放在业务上而不是技术实现上。

Gradle 入门教程(1)——Groovy 基础知识

其实很早就想写关于 Gradle 的教程了,这个很早可以追溯到大约 2 年前,可是鬼知道我这 2 年经历了什么,总之拖延症晚期患者你们伤不起啊。废话不多说了,下面进入正题。

首先说说 Gradle 是个啥,很简单,它是一个项目构建工具,“项目构建工具”听起来很高大上,其实做过项目的人天天都在用,我们最熟悉的maven就是一个项目构建工具。为了照顾没用过 Maven 的小伙伴们,我还是简单的解释下吧,引用下《Maven 实战》一书中关于项目构建工具的解释,”项目构建工具能够帮我们自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署,不需要也不应该一遍遍地输入命令“。作为程序员,我们每天开工时都要更新代码并进行编译,项目开发完毕后还要打war包,然后还要部署到服务器上,部署时可能还需要运行一些脚本进行初始化,可以看出在整个开发过程中我们需要把对项目的不同阶段的产物当作输入,进行一些操作后输出产物作为下一阶段的输入,例如,把开发阶段把源代码当作输入,通过编译操作输出编译后的class文件,打包阶段把编译产生的class文件压缩到war包中等等。而对每个阶段的操作根据项目情况一般都是以固定的方式进行的,而这就为自动化带来了契机。所以先设想一下,我们要是能把所有阶段的操作都写成脚本,想要执行某一阶段的操作,我们只需从头执行脚本,一直执行到目标阶段的脚本结束为止即可实现以往手动耗时耗力的工作,节省了时间。而这就是项目构建工具的初衷,所以诸如 Maven,Gradle 之类的工具就诞生了。

再来说说为啥要用 Gradle 而不用 Maven。大家都知道 Maven 独霸 Java 市场好多年,个人感觉秒 ant 等工具好几条街,但它仍然使用 xml 作为配置文件,虽说可以通过 schema 进行语法校验,防止人为出错,但从学习曲线、需要输入字数等方面考虑还是很让人受伤的,如果项目比较大,定义的依赖、编译配置等信息一长串,满屏都是 xml 标签,对开发人员来说是非常的不友好。而 Gradle 正是看中了 Maven 这一弱点,以 Groovy 语言为基础定义了一套自己的语法规则(DSL),学习难度不算大,只是一开始可能语法接受不了,但习惯之后配置出的项目相当简洁,比 Maven 不知省了多少字。虽说 Gradle 推出的时间不长,资质比 Maven 也低很多,但其简洁的风格,简直就是 Java 项目构建领域的一股“清流”哇,一经推出不久便获得了Spring 的大奖,现在许多知名的开源项目都开始迁移到了 Gradle 上了,比如 Spring、Hibernate等等。再加上近几年移动应用的崛起,Android 项目默认就是以 Gradle 为默认构建工具的,所以 Gradle 无论是从提高效率还是顺应潮流都是值得我们去学习的。下面就开始我们的学习之旅吧,正如前面提到的, Gradle 是以 Groovy 语言为基础的,所以我们先学习下 Groovy 的基础知识,有了这些我们学习 Gradle 的速度就大大提高,就如学会了九阳神功再学习乾坤大挪移只是分分钟的事儿。

一、Groovy 简介

Groovy 是一门可以运行在 Java 虚拟机上的动态编程语言,也就是说 Groovy 代码经过编译器编译后也生成字节码文件,然后可以由 Java 虚拟机加载并运行,虽说 Groovy 语言是动态的,但它同时具备像 Python 这种脚本语言的动态功能,同时也具备像 Java 这种面向对象的静态语言的特性。由于 Groovy 编译后也是字节码文件,它能与 Java 进行很好的集成,甚至两种语言可以混写或将 Groovy 文件打成 jar 包供程序调用。Groovy 在语法上有很多地方和 Java 相类似,所以如果你有一定的 Java 基础的话,那么学习 Groovy 将会是一件很轻松的事,因为只需要拿 Java 的语法做类比就能轻松地掌握。下面我们就用类比的方式学习 Groovy 的基本语法。

我们在学习 Java 时,第一堂课老师都会告诉我们一句非常重要的话—— Object 类是所有 Java 类的父类,这句话同样适用于 Groovy,在 Groovy 中,所有的类型的父类也都是 java.lang.Object

ABO 血型不相容反应

首先,ABO 指的就是人类的四种血型字母的缩写,即 A型、B型,AB型和 O型血,我们都知道如果我们输入了不对的血型就有可能产生一系列的不良反应,甚至是严重的后果,而这些反应就叫做“ABO 血型不相容反应”。之所以会产生不良反应,是因为我们的免疫系统对这些外来的不兼容的血细胞的产生的一种保护机制,而这种保护机制可能是过度的、致命的。下面让我们了解下不相容反应的原理是什么。

血型有A、B,AB和 O型血,A型血细胞上有一种蛋白质称为 A 抗原,B型血上也有一种蛋白质称为 B 抗原,这两种蛋白质是不同的,AB 型血上既有 A 抗原又有 B 抗原,而 O型血上既没有 A 抗原又没有 B抗原。当我们的免疫系统发现体内的血细胞上存在与自身血细胞抗原不同的抗原时,就会产生免疫抗原来杀死这些不兼容的血细胞。有点绕,我们举个例子说明下,如果一个 A 型血的人输入了 B 型血,那么他的免疫系统就会产生抗原去对抗 B 型血细胞,因为 B 型血上有 B 抗原,与此人血细胞上的 A 抗原不同,就被免疫系统识别为外来入侵。再进一步讲,如果一个 A 型血的人输入了 AB 型血,那么也会产生不相容反应,因为虽然 AB 型血上有 抗原 A 但也存在抗原 B,免疫系统不认识抗原 B,眼睛里容不得沙子,照样将产生免疫反应去消灭这些“外来物”。以上就是血型不相容反应的基本原理了,我们可以看出来免疫系统根据蛋白质识别外来细胞并执行严格的清理程序,那么根据这一原理我们输血时是否只能输入完全匹配自己血型的血液呢?不一定。

如果你是 AB 型血(即血细胞上既有 A 抗原又有 B抗原),那么恭喜你,你是一个万能受血者,也就是说你可以输入任何类型的血液,当你输入 A、B、AB 型血时,免疫系统都认识这些血细胞上的抗原,当输入 O 型血时,由于 O 型血上没有 A、B抗原,也无法被免疫系统识别,所以也不会产生免疫反应。但你的血只能给 AB 型的人用。如果你是 O 型血,那么你是一个万能的供血者,你可以将血献给任何人用而不用担心引起免疫反应,因为 O 型血上没有任何抗原不会被免疫系统识别,但是你只能输入 O 型血。有没有一种感觉 AB 型是来者不拒的,而 O 型的是无私奉献的。

尽管在医学技术发达的今天,输错血的概率几乎不存在,但只要是人就避免不了出错,尤其是在医疗方面,一个小小的失误也可能引起不可挽回的后果。所以有很多的预防措施来确保输血的安全性,比如会抽血化验你的血型,然后将你的血液和要输的血融合放到显微镜下观察是否发生溶血反应等,此外医生和护士也都具备识别发生不相容反应症状的知识,所以他们能第一时间诊断并采取措施进行救治。下面是不相容反应的一些症状,如果我们输血出现下面的症状,那么应立即停止输血并报告医生,一般情况下,输错血后在几分钟之内就会有不良症状出现:

  • 发热、身体感到寒冷
  • 呼吸困难
  • 肌肉疼痛
  • 恶心、反胃
  • 胸、腹、腰疼痛
  • 血尿
  • 黄疸

当发生不良反应时,医生会抽血测定血细胞的受损程度,还要测试尿液中是否存在血红蛋白,因为如果真的发生免疫反应的话,被杀死的血细胞会释放出血红蛋白,并且医生也会再次将你的血液和输血的血液做融合测试。此时的你肯定相当难受,严重的还会被转移到 ICU 病房,打着生理盐水,防止肾衰,凝血和血压过低,再严重的话还要注射血浆和血小板,还要戴上氧气面罩,身上绑着一堆的传感器,实时监控着血压、心率、呼吸和体温。哈哈,是不是很恐怖。但是作为输血的我们学习了本篇文章,再加上输血前和医生确认,那么出事的可能性就很低了,即使发生不良反应,及时地配合医生的治疗也都能痊愈。

本文编译自:http://www.healthline.com/health/abo-incompatibility

使用 Hibernate 实现软删除

当我们做项目的时候,我们经常会遇到这样的需求:用户希望当删除数据时并不真正的从物理文件中删除记录而是将记录打上标记,下次再查询的时候就只查那些没有标记为删除的记录,也就是我们常说的“假删除”。之所以这样做是因为用户想要尽可能地保留数据的历史记录,供将来追溯,或现有的记录与其他表的数据存在关联关系,如果删除的话会导致数据完整性问题。一般来说,实现软删除的思路有 2 种,第一种是将要删除的记录写入到审计日志或转移到其他的表中,这样做有一定的局限性,以后查看不方便,甚至还要与其他表重新建立关联关系,比较麻烦;另一种方法就是为表增加一个额外的字段,比如是布尔、枚举来表示数据的状态。下面要讲的就是使用 Hibernate 实现第二种方式的软删除。

要实现软删除,我们需要克服 2 个困难:

  • 当遇到删除操作时,我们需要让 Hibernate 不生成 DELETE 语句,而是生成 UPDATE 语句并更新相应的状态字段
  • 当执行任何查询时,我们要让 Hibernate 自动根据标志字段过滤出数据,因为我们的系统通常会有大量的查询方法,如果没有一种自动机制,那么我们需要为所有查询方法都添加过滤条件,显然很麻烦也很容易出错,万一漏写了就会暴露了数据。

下面就通过代码来演示下如何实现软删除,演示环境是:JPA/Hibernate 5.2.6.Final,MySQL 5.6.26,项目具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.2.6.Final</version>
    </dependency>
    <dependency>
       <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.40</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.22</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.1.8</version>
    </dependency>
</dependencies>

首先我们先定义一个简单的实体 Student 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
@Table(name = "student")
public class Student {
    /**
     * 主键
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    /**
     * 姓名
     */
    @Column(name = "name_", length = 20)
    private String name;
    /**
     * 是否删除(标记位)
     */
    @Column(name = "is_deleted_")
    private Boolean isDeleted;
}

上面代码中,出于方便的目的我们让主键自增,该实体还有另外 2 个简单的字段,分别是姓名和删除标记位,其中 isDeleted 字段默认是 false 的,现在我们想要实现的目标是当我们使用 JPA 删除一个学生的记录时,生成的 SQL 语句不是 DELETE 而是更新 is_deleted_ 字段并将其更新为 true。 想要实现上面的目标,我们就需要重新实现 Hibernate 的删除操作,而 Hibernate 为我们提供了一个注解 @SQLDelete,我们只需通过注解指定进行删除操作时要执行的原生 SQL,这个 SQL 就会覆盖 Hibernate 原来生成的 DELETE 语句从而实现重写删除操作的目的,具体实现方式请看下面的代码:

1
2
3
4
@Entity
@Table(name = "student")
@SQLDelete(sql = "update student set is_deleted_ = true where id=?", check = ResultCheckStyle.COUNT)
public class Student {...}

上面的代码中我们在 Student 实体上使用了 @SQLDelete 注解并设置了一个原生 SQL 用于更新状态位,下面我们就试验下是否能实现软删除:

1
2
3
4
5
6
7
8
9
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("pu");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
Student student = entityManager.find(Student.class, 3);
entityManager.remove(student);
entityManager.getTransaction().commit();
logger.info("{}", student);
entityManager.close();
entityManagerFactory.close();

上面的代码中我们先查询出 ID 为 3 的记录,然后删除并提交事务,最后再打印出刚才查询出来的记录,下面是 Hibernate 生成的日志:

1
2
3
4
5
6
7
16:47:28.717 [main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_0_, student0_.is_deleted_ as is_delet2_0_0_, student0_.name_ as name_3_0_0_ from student student0_ where student0_.id=?
16:47:28.735 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [INTEGER] - [3]
16:47:28.748 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([is_delet2_0_0_] : [BOOLEAN]) - [false]
16:47:28.748 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([name_3_0_0_] : [VARCHAR]) - [abc]
16:47:28.773 [main] DEBUG org.hibernate.SQL - update student set is_deleted_ = true where id=?
16:47:28.774 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [INTEGER] - [3]
16:47:28.780 [main] INFO  org.mingzhe.test.SoftDeleteTest - Student{id=3, name='abc', isDeleted=false}

从第 5 行日志中我们可以看出删除操作变成了我们指定的更新语句,说明我们的目的实现了,但是又出现了新问题,大家可以看到最后一行日志显示我们实体管理器中 student 的 isDeleted 字段仍然为 fasle。这是为什么呢?这是因为我们执行了原生 SQL,而Hibernate 是无法探测到原生语句到底对哪些记录进行了更改,所以也就无法对其上下文中管理的实体状态进行更新,所以我们还需改进一下,让受管的实体状态得到更新。针对这个问题,比较优雅的方式就是使用 JPA 的生命周期方法,我们在 Student 类中定义一个方法并用 @PostRemove 注解,告诉 Hibernate 当执行完删除操作后更新实体的状态,代码如下:

1
2
3
4
5
6
7
8
9
@Entity
@Table(name = "student")
@SQLDelete(sql = "update student set is_deleted_ = true where id=?", check = ResultCheckStyle.COUNT)
public class Student {
    @PostRemove
    public void delete() {
        this.isDeleted = true;
    }
}

从上面的代码中我们可以看出,当执行完删除操作有 JPA 会自动调用 delete() 方法将删除的实体的 isDeleted 字段设置为 true

下面我们要解决的就是如何在执行查询时自动排除那些已经标记为删除的记录。这次我们还是使用一个 Hibernate 私有的注解——@Where,这个注解用于在每次生成的 WHERE 语句后面都自动加上我们指定的过滤条件,下面代码演示了通过这个注解根据状态字段来自动过滤数据:

1
2
3
4
5
@Entity
@Table(name = "student")
@SQLDelete(sql = "update student set is_deleted_ = true where id=?", check = ResultCheckStyle.COUNT)
@Where(clause = is_deleted_  != true)
public class Student {...}

上面的代码中我们在实体类上使用了 @Where 注解,并指定了过滤条件为 is_deleted_ 不等于 true,每次查询时这个条件都会自动加到 where 语句后面,下面我们进行一次查询,看看是否能得到正确的结果:

1
2
3
4
5
6
7
8
9
10
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("pu");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
Student student1 = entityManager.find(Student.class, 3);
logger.info("{}", student1);
Student student2 = entityManager.find(Student.class, 4);
logger.info("{}", student2);
entityManager.getTransaction().commit();
entityManager.close();
entityManagerFactory.close();

上面的代码中,我们分别查询了 id 为 3,4 的记录,并打印查询结果,其中 id 为 3 的记录删除标记位已设置为 true,所以打印出 student1 为 null,而 student2 则正常查询出来,说明我们的设置起作用了,下面是生成的日志:

1
2
3
4
5
6
7
8
08:25:50.864 [main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_0_, student0_.is_deleted_ as is_delet2_0_0_, student0_.name_ as name_3_0_0_ from student student0_ where student0_.id=? and ( student0_.is_deleted_ != 1)
08:25:50.885 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [INTEGER] - [3]
08:25:50.896 [main] INFO  org.mingzhe.test.SoftDeleteTest - null
08:25:50.897 [main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_0_, student0_.is_deleted_ as is_delet2_0_0_, student0_.name_ as name_3_0_0_ from student student0_ where student0_.id=? and ( student0_.is_deleted_ != 1)
08:25:50.897 [main] TRACE org.hibernate.type.descriptor.sql.BasicBinder - binding parameter [1] as [INTEGER] - [4]
08:25:50.918 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([is_delet2_0_0_] : [BOOLEAN]) - [false]
08:25:50.920 [main] TRACE org.hibernate.type.descriptor.sql.BasicExtractor - extracted value ([name_3_0_0_] : [VARCHAR]) - [张三]
08:25:50.925 [main] INFO  org.mingzhe.test.SoftDeleteTest - Student{id=4, name='张三', isDeleted=false}

从日志中可以印证在执行查询过程中 Hibernate 会自动为生成的语句添加上我们指定的过滤条件。

有同学可能会问如果我想要查询所有记录该如何呢?这种情况下我们可以定义一个专门的方法,用原生 SQL 来查询出来所有的记录。本文讲解的使用 Hibernate 来实现软删除其实就是使用了 Hibernate 私有的注解,虽然可以很优雅的完成目标,但也存在一些弊端,如不可移植,如果大家有更好的方法,欢迎指教。