首页 文章 精选 留言 我的

精选列表

搜索[学习],共10000篇文章
优秀的个人博客,低调大师

这种激励机制,运营商应该学习

最近华为高层宣布继续分红传统的新闻引爆了通信行业,无论站在哪个立场,大家都为华为如此大手笔操作感到惊叹,并据此反思运营商的激励计划能否实现激励的初衷。 一、华为用分红迎接饱和打击 近日,华为轮值董事长胡厚��宣布:华为2020年股票分红,预计每股1.86元人民币。虽然有美国的全面拉黑打击,但是华为仍将延续历年来形成的股票分红传统。通过给奋斗者分发奖励,华为一直在做迎接各种挑战的准备。 华为2019年财报显示,截至2019年12月31日员工总量约为19万人,其中持股计划参与人数为10.5万人,持股员工覆盖率约为55.3%。此次华为股票分红规模预计在400亿元作用,据此推算人均分红约为20万元。这个分红均值与运营商人均年收入相差无几。 如下图所示,华为用于股票分红的资金规模已经相当于中国联通2019年薪酬福利的80%,相当于中国电信的60%。华为这样大规模的激励无疑是在正常薪酬福利之外为员工提供了另外的收益。 中国联通的薪酬福利占主营业务收入比重已经高达19%,在某种程度上看,中国联通未来提高薪酬福利规模的难度会越来越大,这其中不但要看中国联通自身的营收增长情况,也要看同行的薪酬占营收比提升情况。 享受着行业最低的薪酬福利,中国联通员工的劳动生产率也是三大运营商中最低的。虽然中国移动的薪酬待遇低于中国电信,但是中国移动的员工劳动生产率却是行业最高的。这里面到底是什么因素在发挥作用呢? 有关华为依靠各种激励机制猛攻核心技术的各种操作,我们今天不过多讨论。这里只是提示大家,在遭遇饱和式打击或者打压时,华为现在的做法起到了凝心聚力的作用。即便也有少部分网友对这种操作不感冒。 二、员工持股不应仅关注极少数 相当于华为持股计划参与员工覆盖率超过55%的重点普惠式激励,运营商更关注金字塔尖上的极少数员工。无论是混改之后首先实施股票期权激励的中国联通,还是于2020年开始实施股票期权激励的中国移动,都是极少数员工参与。 根据网络公开信息,混改后的中国联通享有股票期权激励的仅有3%左右,而中国移动公布的信息显示其享有股票期权激励的员工首期不超过10000名,相当于其45万左右的员工总量,其参与率也仅有2%左右。 虽然运营商的极少数人参与股权期权激励有其自身的合理性,但是这种仅给极少数人激励并期待其他人一起努力或者奋斗的操作显然得不到绝大多数员工的赞同。即便大家都守口如瓶,但是大家的眼睛都是雪亮的。 另外,运营商实施的股票期权计划也没有最大限度捆绑有潜力的员工,毕竟其参与覆盖率仅有2%左右,根本无法与华为55%左右的超大覆盖率相比。华为用55%的参与率稳定了公司大盘子,而运营商仅用2%的普及率吸引各级管理层。 三、扩大参与比例或将成为趋势 虽然混改之后有BATJ等互联网大佬的资金和线上渠道加持,但是中国联通的股票并未呈现大家预期的直线上升走势,中国移动的情况也大体如此。2020年的股票走势显示,中国移动股票已经下跌了20%左右。 运营商目前面临4G与5G换挡的窗口期,虽然未来的预期非常好,但是眼前却是缺少大规模增长的强力业务支撑。运营商高层试图用股票期权捆绑极少数管理层的做法根本不可能改变当前的通信行业的基本盘。 一方面是运营商各级管理者本身就享受了超高薪酬待遇,眼前的这几万股期权根本无法大幅提升其幸福感和获得感。另外一方面,极少数享有期权而且名单不公布,实际上除了有加小灶的嫌疑之外,普通员工会作何感想呢? 在风雨飘摇的小船上,船长的作用自然不可替代,但是划桨船员也必不可缺。有公开报道称,国资委计划推行超额利润分享机制,运营商能否充分利用这个契机,重新审视一下自己的激励政策呢? 中国联通的混改已经实施了3年,中国移动的股票期权也即将年满周岁,虽然我们不能直接定性运营商的股票期权计划没有起到预期的作用,但是这种推论确实一直徘徊在我们的大脑中,而且这种推论也是符合逻辑的。不管真实情况如何,在面临与华为类似重大考验的时间节点上,运营商高层应该想方设法提振员工士气,这其中期权就是一个非常不错的选择。

优秀的个人博客,低调大师

【React学习笔记】React的双向绑定(React State)

1、什么是双向绑定、为什么需要双向绑定? 本人是先入坑的vue,所以在开发时已经习惯了vue的自动双向绑定。 什么是双向绑定呢? 在没有前端框架之前的开发时光中,我们都是直接操作页面的DOM元素(element)的。将某个变量赋值到某个页面元素时都是一次性的操作,在此之后变量值的修改并不会影响页面元素的内容变化;而页面元素(比如input框)的内容变更以后,也不会使对应变量的值发生变化,我们需要通过document.getElementById()的方法获取到页面元素,然后再根据元素对象的value获取变化以后的值再手动处理。 在vue中,框架已经帮我们自动实现了双向绑定,任何一方(哪怕是页面上纯显示的)也会自动变化。(以下代码中,变量name的变化会直接反应到input框中,而input框中值得修改也会反向引起name变量的值变化。) <!-- Vue中一个最简单的双向绑定示例 --> <template> <div> <el-input v-model=“name” /> </div> </template> <script> export default { data() { return { name: "", } }, } </script> 而到了react,发现并没有双向绑定的功能,有点懊糟。 NameShow.js import React from 'react'; class NameShow extends React.Component { myName = "" // 定义myName变量 render() { return ( <div> {/*尝试将myName变量绑定到input框上*/} <input value={this.myName}/> {/*获取myName,并直接显示在页面上*/} <div>My Name Is {this.myName}</div> </div> ) } } export default NameShow 将以上组件引入APP中并添加到页面上启动,发现不仅无法双向绑定,甚至连修改input框的内容都没有任何反应,而且控制台有警告显示。大概的意思是value是只读的,值的修改必须通过onChange事件来修改。 所以添加一个onChange事件的handle方法handleChange以后再次启动。 NameShow.js import React from 'react'; class NameShow extends React.Component { myName = "" // 定义myName变量 handleChange = (event) => { // 此处需使用箭头函数,否则方法中的this将会识别为undefined console.log(event.target.value) this.myName = event.target.value } render() { return ( <div> {/*尝试将myName变量绑定到input框上*/} <input value={this.myName} onChange={this.handleChange}/> {/*获取myName,并直接显示在页面上*/} <div>My Name Is {this.myName}</div> </div> ) } } export default NameShow 在输入框中输入内容以后,发现控制台有反应,正常拿到了修改以后的值,但是输入框和下面显示的内容却依旧没有变化。 2、React不支持双向绑定?非也 react并不是不支持双向绑定,而是双向绑定需要我们自己来实现。这时候就是react中的一个重要概念登场了——React State。 在菜鸟教程中的解释是这样的:React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。 现在回到上面那个例子,进行一下改造,由使用类成员变量myName改为使用state中的myName变量。 NameShow2.js import React from 'react'; class NameShow2 extends React.Component { constructor(props) { super(props); this.state = { // 注意1:此处需先对state进行初始化,不然编译会报错 myName: "" // 注意2:此处需要将变量在state中进行定义,否则会报警告 } } handleChange = (event) => { console.log(event.target.value) // this.state.myName = event.target.value // 注意3:此处直接赋值是无效的,一定要使用this的setState方法 this.setState( {myName: event.target.value} ) } render() { return ( <div> {/*尝试将myName变量绑定到input框上*/} <input value={this.state.myName} onChange={this.handleChange}/> {/*获取myName,并直接显示在页面上*/} <div>My Name Is {this.state.myName}</div> </div> ) } } export default NameShow2 将以上组件引入APP中并添加到页面上启动,并在input框填入内容,发现下面的显示也同步改变了,并且控制台也有输出。 到此,一个简单的react双向数据绑定的流程就完成了。 3、小结 在react中要实现数据的双向绑定,需要通过state来实现, 根据菜鸟解释,react将每个组件都定义为状态机,通过state来改变和控制组件的状态。 使用state之前需要先初始化,并且在state中定义所需要用到的变量, 这个类似于vue中使用前先要在data部分定义变量。 (一般使用时可以不用预先定义变量,只有在变量绑定对象为<input> <textarea> <select>时才需要,具体内容可以参考react官网相关解释。) 直接对state进行赋值是无效的,需要使用setState方法来赋值,这样才能触发状态的改变,从而达到值传递的效果。 另外补充一点,在上述两个例子中都将handleChange方法都以箭头函数的方式进行定义,这是由于js的闭包导致的。 如果想将handleChange定义为普通方法,可以在构造函数中将this对象绑定到handleChange方法 constructor(props) { super(props); this.state = { myName: "" } this.handleChange = this.handleChange.bind(this) // 将`this`对象绑定到`handleChange`方法 }

优秀的个人博客,低调大师

JavaScript数据结构与算法学习集合

什么是集合 集合 集合是由一组无序且唯一 (即不能重复) 的项组成。 在数学中,集合是一组不同对象的集合。比如是说,一个又大于或等于 0 的整数组成的自然数集合: N = {0, 1, 2, 3,4,5, 6, ...}。集合中的对象列表用花括号 {} 包围。 空集 空集是不包含任何元素的集合。空集用 {} 表示。 实现集合 定义集合类 我们使用 ES6 的 class 语法来创建一个基于对象的 Set 类: class Set { constructor() { this.items = {} } } 复制代码 接下来,我们为集合声明一些可用的方法: add(element) 向集合添加一个新元素 delete(element) 从集合中移除一个元素 has(element) 判断一个元素是否在集合中 clear() 移除集合中的所有元素 size() 返回集合中所包含元素的数量 values() 返回一个包含集合中所有元素的数组 isEmpty() 判断集合是否为空 size() 返回集合中元素个数 clear() 清空集合 toString() 将集合中的元素以字符串形式输出 下面,我们逐一实现这些方法: add 向集合添加一个新元素 add(element) { if (!this.has(element)) { this.items[element] = element; return true; } return false; } 复制代码 向集合中添加一个 element 的时候,首先检查它是否存在于集合中,如果不存在,就将 element 添加到集合中,并返回 true,表示添加了该元素;如果集合中已经有了这个element,则返回 false ,表示没有添加这个元素。 delete 从集合中移除一个元素 delete(element) { if (this.has(element)) { delete this.items[element]; return true; } return false; } 复制代码 同样的,从集合中移除一个元素时,首先判断该元素是否存在于集合中,如果存在,就从集合中移除,并返回 true,表示元素被移除;如果不存在,则返回 false。 has 判断一个元素是否在集合中 has(element) { return Object.prototype.hasOwnProperty.call(this.items, element); } 复制代码 我们使用 Object 的原型方法 hasOwnProperty 来判断元素是否在集合中。hasOwnProperty 方法返回一个表明对象是否具有特定属性布尔值。 clear 移除集合中的所有元素 clear() { this.items = {} } 复制代码 size 返回集合中所包含元素的数量 方法一:使用 Object.keys() size() { return Object.keys(this.items).length; } 复制代码 在 size 方法中,我们使用 Object 类的 keys 方法来获取集合中所包含元素的数量。keys 方法返回一个包含给定对象所有属性的数组,然后我们可以使用数组的 length 属性来返回集合中的元素数量。 方法二:使用 for...in 循环 size() { let count = 0; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { count++ } } return count; } 复制代码 我们定义一个 count 变量来存储集合中元素的个数, 然后使用 for...in 循环迭代 items 对象的所有属性,检查它们是否是对象自身的属性,如果是,就递增 count 变量的值,最后在方法结束时返回 count 变量。 values 返回一个包含集合中所有元素的数组 方法一:使用 Object.values() values() { return Object.values(this.items); } 复制代码 我们使用 Object 类内置的 values 方法可以很轻松的获取集合中的所有元素,但Object.values 方法目前只在现代浏览器中可用。 方法二:使用 for...in 循环 values() { let values = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { // 注意,我们保存的集合元素 key 与 value 相同,因此可以直接将 key push 进 values 中 values.push(key) } } } 复制代码 isEmpty 判断集合是否为空 isEmpty() { return this.size() === 0; } 复制代码 toString 将集合中的元素以字符串形式输出 toString() { if (this.isEmpty()) { return ''; } const values = this.values(); let objString = `${values[0]}`; for (let i = 1; i < values.length; i++) { objString = `${objString}, ${values[i].toString()}`; } return objString; } 复制代码 集合运算 在数学中,集合有 并集、交集、差集、子集等运算,我们也可以使用我们定义的集合类进行这些运算。 并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合 交集:对于给定的两个集合,返回一个包含两个集合中共有元素的集合 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合 子集:验证一个给定集合是否是另一个集合的子集 并集 在数学概念中,集合 A 和集合 B 的并集表示如下: A ∪ B 该集合的定义如下: A ∪ B = {x | x ∈ A ∨ x ∈ B} 意思是 x (元素) 存在于 A 中,或 x 存在于 B 中。下图展示了并集运算: 下面,我们实现 Set 类的 union 方法: union(otherSet) { // 存放并集的结果 const unionSet = new Set(); // 迭代当前集合的所有值,将值添加到并集 集合中 this.values().forEach(value => unionSet.add(value)); // 迭代 otherSet 的所有值,将值添加到并集 集合中 otherSet.values().forEach(value => unionSet.add(value)); return unionSet; } 复制代码 首先使用 ES6 提供的数据结构 Set 来创建一个新的集合 unionSet,代表两个集合的并集。接着获取第一个集合(当前的 Set 类实例)的所有值,迭代并全部添加到代表并集的集合中。然后对第二个集合做同样的是,最后将代表并集的集合返回。 交集 在数学概念中,集合 A 和集合 B 的交集表示如下: A ∩ B 该集合的定义如下: A ∩ B = {x | x ∈ A ∧ x ∈ B} 意思是 x (元素) 存在于 A 中,且 x 存在于 B 中,下图展示了交集运算: 下面,我们实现 Set 类的 intersection 方法: intersection(otherSet) { // 存放交集的结果 const intersectionSet = new Set(); // 当前集合(当前 Set 类的实例)的所有值 const values = this.values(); // 第二个集合的所有值 const otherValues = otherSet.values(); // 假设当前的集合元素较多 let biggerSet = values; // 假设第二个集合元素较少 let smallerSet = otherValues; // 比较两个集合的元素个数,如果另外一个集合的元素多于当前集合,则交换 biggerSet 和 smallerSet if (otherValues.length - values.length > 0) { biggerSet = otherValues; smallerSet = values; } // 迭代较小的集合,计算出两个集合共有元素添加到 交集 集合中 smallerSet.forEach(value => { if (biggerSet.includes(value)) { intersectionSet.add(value); } }) // 将交集结果返回 return intersectionSet; } 复制代码 intersection 方法会得到所有同时存在于两个集合中的元素。我们首先创建一个新的集合 intersectionSet 来存放交集的结果。然后分别获取当前 Set 实例中的所有元素和另一个集合中的所有元素。我们假定当前集合的元素较多,另一个集合的元素较少,比较两个集合的元素个数,如果另一个集合的元素个数多于当前集合中的元素个数,我们就交换 biggerSet 和 smallerSet 的值。最后迭代较小集合 smallerSet 来计算出两个集合的共有元素。 差集 在数学概念中,集合 A 和集合 B 的差集表示为: A﹣B 该集合的定义如下: A﹣B = {x | x ∈ A ∧ x ∉ B} 意思是 x (元素) 存在于 A 中,且 x 不存在于 B 中。下图展示了集合 A 和集合 B 的差集运算: 下面,我们来实现 Set 类的 subtract 方法: subtract(otherSet) { // 存放差集结果 const subtractionSet = new Set(); // 迭代当前集合的所有值 this.values().forEach(value => { // 当前值不存在于 otherSet 中,将其添加到 差集 集合中 if (!otherSet.has(value)) { // 将元素添加到集合中 subtractionSet.add(value); } }) return subtractionSet; } 复制代码 subtract 方法会得到所有存在于集合 A 但不存在于集合 B 的元素。我们首先创建一个新的集合变量 subtractionSet 来存放差集结果。通过 this.values() 拿到集合A 中的所有值,然后迭代集合 A 中的所有值,将存在于集合A 但不存在于集合B 中的元素放入 subtractionSet 集合中,迭代完集合 A 中的所有元素后,subtractionSet 集合中的元素就是集合A 与集合B 差集。 子集 在数学概念中,集合 A 是集合 B 的一个子集(或者 B 包含 A),表示如下: A ⊆ B 该集合的定义如下: {x|∀x∈A ⇒ x ∈ B} 意思是集合A 中的每一个 x (元素),也需要存在于集合B 中。下图展示了集合A 是集合B 的子集: 下面,我们来实现 Set 类的 isSubsetOf 方法: isSubsetOf(otherSet) { // 验证当前 Set 实例的大小 // 当前 Set 实例的元素多于 otherSet,那么当前 Set 实例不是 otherSet 的子集 if (this.size() > otherSet.size()) { return false; } // 假定当前 Set 实例是给定集合的子集 let isSubset = true; // 迭代当前 Set 实例的所有元素 this.values().every(value => { // 有任何元素不存在于 otherSet 中,那么当前 Set 实例就不是 otherSet 的子集 if (!otherSet.has(value)) { isSubset = false; return false; } return true; }) return isSubset; } 复制代码 isSubsetOf 方法验证 集合 A 是否是 集合 B 的子集。我们首先需要验证当前 Set 实例的大小(集合A),如果当前实例中的元素比 otherSet 实例(集合B)更多,说明当前 Set 实例不是 otherSet 的子集。(子集的元素个数需要小于等于要比较的集合)。 接下来,我们假定当前集合是给定集合的子集,然后迭代当前集合的所有元素,并验证这些元素是否存在于 otherSet 中。如果有任何一个元素不存在于 otherSet 中,那么当前集合就不是给定集合的子集,我们就返回 false。如果所有元素都存在于 otherSet 中,那么当前集合就是给定集合的子集。 完整代码 class Set { constructor() { this.items = {}; } // 向集合添加一个新元素 add(element) { if (!this.has(element)) { this.items[element] = element; return true; } return false; } // 从集合中移除一个元素 delete(element) { if (this.has(element)) { delete this.items[element]; return true; } return false; } // 判断一个元素是否存在于集合中 has(element) { return Object.prototype.hasOwnProperty.call(this.items, element); } // 移除集合中的所有元素 clear() { this.items = {}; } // 返回集合中所包含元素的数量 size() { let count = 0; for(let key in this.items) { if (this.items.hasOwnProperty(key)) { count++; } } return count; } // 返回一个包含集合中所有元素的数组 values() { let values = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { values.push(key); } } return values; } // 并集 union(otherSet) { const unionSet = new Set(); this.values().forEach(value => unionSet.add(value)); otherSet.values().forEach(value => unionSet.add(value)); return unionSet; } // 交集 intersection(otherSet) { const intersectionSet = new Set(); const values = this.values(); const otherValues = otherSet.values(); let biggerSet = values; let smallerSet = otherValues; if (otherValues.length - values.length > 0) { biggerSet = otherValues; smallerSet = values; } smallerSet.forEach(value => { if (biggerSet.includes(value)) { intersectionSet.add(value); } }) return intersectionSet; } // 差集 subtract(otherSet) { const subtractionSet = new Set(); this.values().forEach(value => { if (!otherSet.has(value)) { subtractionSet.add(value) } }) return subtractionSet; } // 子集 isSubsetOf(otherSet) { if (this.size() > otherSet.size()) { return false; } let isSubset = true; this.values().every(value => { if (!otherSet.has(value)) { isSubset = false; return false; } return true; }) return isSubset; } } 复制代码 ES6 Set 类的集合运算 并集 // A 和 B 的并集 function union(setA, setB) { const unionSet = new Set([...setA, ...setB]); return unionSet; } 复制代码 交集 // A 和 B 的交集 function intersection(setA, setB) { const intersect = new Set([...a].filter(x => b.has(x))); return intersect } 复制代码 差集 // A 相对于 B 的差集 function subTract(setA, setB) { const subTractSet = new Set([...a].filter(x => !b.has(x))); return subTractSet; } 复制代码 参考资料: 书籍:《JavaScript数据结构与算法》 作者:moozi

优秀的个人博客,低调大师

Spring学习笔记(五):JDBCTemplate+事务管理

1 概述 Spring为开发者提供了JDBCTemplate,可以简化很多数据库操作相关的代码,本文主要介绍JDBCTemplate的使用以及事务管理功能。 2 JDBC Template 2.1 配置 配置的话主要配置以下几项: 数据源:org.springframework.jdbc.datasource.DriverManager.DataSource 数据库驱动:com.cj.mysql.jdbc.Driver,这里采用的是MySQL 8,注意MySQL 5.7以下的驱动名字不同,另外若是其他数据库请对应修改 数据库URL:jdbc:mysql://localhost:3306/test,MySQL默认的3306端口,数据库test 数据库用户名 数据库密码 JDBC模板:org.springframework.jdbc.core.jdbcTemplate 参考配置如下: <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="test"/> <property name="password" value="test"/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <context:component-scan base-package="pers.dao"/> 2.2 常用方法 int update(String sql,Object args[]):增/删/改操作,使用args设置其中的参数,返回更新的行数 List<T> query(String sql,RowMapper<T> rowMapper,Object []args):查询操作,rowMapper将结果集映射到用户自定义的类中 2.3 示例 2.3.1 依赖 首先导入依赖: <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.9.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> MySQL的版本请根据个人需要更改,或使用其他数据库的驱动。 2.3.2 配置文件 完整配置文件如下: <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="test"/> <property name="password" value="test"/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <context:component-scan base-package="pers.dao"/> </beans> 2.3.3 实体类 public class MyUser { private Integer id; private String uname; private String usex; } 2.3.4 数据访问层 添加@Repository以及@RequiredArgsConstructor: @Repository @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class TestDao { private final JdbcTemplate template; public int update(String sql,Object[] args) { return template.update(sql,args); } public List<MyUser> query(String sql, Object[] args) { RowMapper<MyUser> mapper = new BeanPropertyRowMapper<>(MyUser.class); return template.query(sql,mapper,args); } } 因为直接使用@Autowired的话会提示不推荐: 所以利用了Lombok的注解@RequiredArgsConstructor,效果相当如下构造方法,只不过是简化了一点: @Autowired public TestDao(JdbcTemplate template) { this.template = template; } 2.3.5 测试 测试之前先建表: create table MyUser( id INT AUTO_INCREMENT PRIMARY KEY , uname varchar(20), usex varchar(20) ) 测试类: public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); TestDao dao = (TestDao)context.getBean("testDao"); String insertSql = "insert into MyUser(uname,usex) values(?,?)"; String[] param1 = {"chenhengfa1","男"}; String[] param2 = {"chenhengfa2","男"}; String[] param3 = {"chenhengfa3","男"}; String[] param4 = {"chenhengfa4","男"}; dao.update(insertSql,param1); dao.update(insertSql,param2); dao.update(insertSql,param3); dao.update(insertSql,param4); String selectSql = "select * from MyUser"; List<MyUser> list = dao.query(selectSql,null); for(MyUser mu:list) { System.out.println(mu); } } } 输出: 如果出现异常或插入不成功等其他情况,请检查SQL语句是否编写正确,包括表名以及字段名。 3 事务管理 Spring中的事务管理有两种方法: 编程式事务管理:代码中显式调用beginTransaction、commit、rollback等就是编程式事务管理 声明式事务管理:通过AOP实现,不需要通过编程方式管理事务,因此不需要再业务逻辑代码中掺杂事务处理的代码,开发更加简单,便于后期维护 下面先来看一下编程式事务管理的实现。 3.1 编程式事务管理 编程式事务管理的配置又有两种方法: 基于底层API 基于TransactionTemplate 需要的依赖如下: <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.2.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.2.9.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.9.RELEASE</version> </dependency> 3.1.1 底层API实现 根据PlatformTransactionManager、TransactionDefinition、TransactionStatus几个核心接口,通过编程方式进行事务管理,首先配置事务管理器: <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> 接着修改数据库访问类: @Repository @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class TestDao { private final JdbcTemplate template; private final DataSourceTransactionManager manager; public int update(String sql,Object[] args) { return template.update(sql,args); } public List<MyUser> query(String sql,Object[] args) { RowMapper<MyUser> mapper = new BeanPropertyRowMapper<>(MyUser.class); return template.query(sql,mapper,args); } public void testTransaction() { TransactionDefinition definition = new DefaultTransactionDefinition(); TransactionStatus status = manager.getTransaction(definition); String message = "执行成功,没有事务回滚"; try { String sql1 = "delete from MyUser"; String sql2 = "insert into MyUser(id,uname,usex) values(?,?,?)"; Object [] param2 = {1,"张三","男"}; template.update(sql1); template.update(sql2,param2); template.update(sql2,param2); manager.commit(status); } catch (Exception e) { e.printStackTrace(); manager.rollback(status); message = "主键重复,事务回滚"; } System.out.println(message); } } 3.1.1.1 事务定义 TransactionDefinition是事务定义,是一个接口: 主要定义了: 事务隔离级别 事务传播行为 事务超时时间 是否为只读事务 而DefaultTransactionDefinition就是上面属性的一些默认配置,比如: 也就是定义了: 传播行为为0:也就是常量PROPAGATION_REQUIREDE,表示如果当前存在一个事务,则加入当前事务,如果不存在任何事务,就创建一个新事务 隔离级别为-1:这个也是TransactionDefinition的默认参数,表示使用数据库的默认隔离级别,通常情况下为Read Committed 超时为-1:默认设置不超时,如需要设置超时请调用setTimeout方法,比如如果设置为了60,那么相当于如果操作时间超过了60s,而且后面还涉及到CRUD操作,那么会抛出超时异常并回滚,如果超时操作的后面没有涉及到CRUD操作,那么不会回滚 只读事务为false:默认为false,但是该变量不是表明“不能”进行修改等操作,而是一种暗示,如果不包含修改操作,那么JDBC驱动和数据库就有可能针对该事务进行一些特定的优化 3.1.1.2 具体执行流程 具体执行流程如下: 定义事务:实例类为DefaultTransactionDefinition 开启事务:通过getTransaction(TransactionDefinition)开启 执行业务方法 根据业务方法是否出现异常手动调用DataSourceTransaction的commit(TransactionStatus)进行提交 出现异常调用rollback(TransactionStatus)进行回滚 测试如下: 3.1.2 基于TransactionTemplate 步骤: 通过调用TransactionTemplate的execute实现 execute接受一个TransactionCallback接口参数 TransactionCallback定义了一个doInTransaction方法 通常以匿名内部类的方式实现TransactionCallback接口,在其中的doInTransaction编写业务逻辑代码 doInTransaction有一个TransactionStatus的参数,可以调用setRollbackOnly进行回滚 默认的回滚规则如下: 如果抛出未检查异常或者手动调用setRollbackOnly,则回滚 如果执行完成或抛出检查异常,则提交事务 示例如下,首先编写配置文件对Bean进行注入: <!--事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--事务模板--> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="txManager"/> </bean> 其次修改数据访问类,添加一个测试方法: public void testTransactionTemplate() { System.out.println(transactionTemplate.execute((TransactionCallback<Object>) transactionStatus -> { String deleteSql = "delete from MyUser"; String insertSql = "insert into MyUser(id,uname,usex) values(?,?,?)"; Object[] parm = {1, "张三", "男"}; try { template.update(deleteSql); template.update(insertSql, parm); template.update(insertSql, parm); } catch (Exception e) { message = "主键重复,事务回滚"; e.printStackTrace(); } return message; })); } 大部分代码与第一个例子类似就不解释了,结果也是因为主键重复出现异常,造成事务回滚: 3.2 声明式事务管理 Spring声明式事务管理通过AOP实现,本质是在方法前后进行拦截,在目标方法开始之前创建或加入一个事务,执行目标方法完成之后根据执行情况提交或回滚事务。相比起编程式事务管理,声明式最大的优点就是不需要通过编程的方式管理事务,业务逻辑代码无需混杂事务代码,但是唯一不足的地方就是最细粒度只能作用到方法上,而不能做到代码块级别。 实现方式有如下两种: 基于XML实现 基于@Transactional实现 3.2.1 基于XML Spring提供了tx命令空间来配置事务: <tx:advice>:配置事务通知,一般需要指定id以及transaction-manager <tx:attributes>:配置多个<tx:method>指定执行事务的细节 3.2.1.1 配置文件 完整配置文件如下: <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="test"/> <property name="password" value="test"/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <context:component-scan base-package="pers.dao"/> <!--事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="txManager"/> </bean> <!--声明式事务--> <tx:advice id="myAdvice" transaction-manager="txManager"> <tx:attributes> <!--任意方法--> <tx:method name="*" /> </tx:attributes> </tx:advice> <!--aop配置,具体可以看笔者之前的文章--> <aop:config> <!--定义切点,执行testXMLTranscation()时进行增强--> <aop:pointcut id="txPointCut" expression="execution(* pers.dao.TestDao.testXMLTransaction())"/> <!--切面--> <aop:advisor advice-ref="myAdvice" pointcut-ref="txPointCut"/> </aop:config> </beans> 3.2.1.2 测试 测试方法如下: public void testXMLTransaction() { String deleteSql = "delete from MyUser"; String saveSql = "insert into MyUser(id,uname,usex) values(?,?,?)"; Object [] parm = {1,"张三","男"}; template.update(deleteSql); template.update(saveSql,parm); template.update(saveSql,parm); } 运行结果: 可以看到提示主键重复了。 3.2.2 基于@Transactional @Transactional一般作用于类上,使得该类所有public方法都具有该类型的事务属性。下面创建一个示例。 3.2.2.1 配置文件 将上一个例子中的<aop:config>以及<tx:advice>注释掉,同时添加: <!--事务管理的注解驱动器--> <tx:annotation-driven transaction-manager="txManager"/> 3.2.2.2 测试 测试方法与上一个例子一致,结果也是如此: 4 参考源码 Java版: Github 码云 CODE.CHINA Kotlin版: Github 码云 CODE.CHINA

优秀的个人博客,低调大师

ES7学习笔记(十三)GEO位置搜索

ES的基本内容介绍的已经差不多了,最后我们再来看看GEO位置搜索,现在大部分APP都有基于位置搜索的功能,比如:我们点外卖,可以按照离我们的距离进行排序,这样可以节省我们的配送费和送餐的时间;还有找工作时,也可以按照离自己家的距离进行排序,谁都想找个离家近的工作,对吧。这些功能都是基于GEO搜索实现的,目前支持GEO搜索功能的中间件有很多,像MySQL、Redis、ES等。我们看看在ES当中怎么实现GEO位置搜索。 GEO字段的创建 GEO类型的字段是不能使用动态映射自动生成的,我们需要在创建索引时指定字段的类型为geo_point,geo_point类型的字段存储的经纬度,我们看看经纬度是怎么定义的, 英文 简写 正数 负数 维度 latitude lat 北纬 南纬 经度 longitude lon或lng 东经 西经 经度的简写有2个,一般常用的是lon,lng则在第三方地图的开放平台中使用比较多。下面我们先创建一个带有geo_point类型字段的索引,如下: PUT /my_geo { "settings":{ "analysis":{ "analyzer":{ "default":{ "type":"ik_max_word" } } } }, "mappings":{ "dynamic_date_formats":[ "MM/dd/yyyy", "yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss" ], "properties":{ "location":{ "type":"geo_point" } } } } 创建了一个my_geo索引,在索引中有一些基础的配置,默认IK分词器,动态映射的时间格式。重点是最后我们添加了一个字段location,它的类型是geo_point。 索引创建完了,我们添加两条数据吧,假设,路人甲在北京站,路人乙在朝阳公园。那么我们怎么“北京站”和“朝阳公园”的经纬度呢?我们在做项目时,前端都会接地图控件,经纬度的信息可以调用地图控件的API获取。在咱们的示例中,也不接地图控件了,太麻烦了,直接在网上找到“北京站”和“朝阳公园”的坐标吧。 我们查到“北京站”的坐标如下: 然后添加一条数据: POST /my_geo/_doc { "name":"路人甲", "location":{ "lat": 39.90279998006104, "lon": 116.42703999493406 } } 再查“朝阳公园”的坐标 再添加“路人乙”的信息 POST /my_geo/_doc { "name":"路人乙", "location":{ "lat": 39.93367367974064, "lon": 116.47845257733152 } } 我们再用elasticsearch-head插件看一下索引中的数据: GEO查询 “路人甲”和“路人乙”的信息都有了,但是没有location字段的信息,因为location是特性类型的字段,在这里是展示不出来的。我们搜索一下吧,看看怎么用geo搜索,假设“我”的位置在“工体”,我们先要查到“工体”的坐标, 然后再查询5km范围内都有谁,发送请求如下: POST /my_geo/_search { "query":{ "bool":{ "filter":{ "geo_distance":{ "distance":"5km", "location":{ "lat":39.93031708627304, "lon":116.4470385453491 } } } } } } 在查询的时候用的是filter查询,再filter查询里再使用geo_distance查询,我们定义距离distance为5km,再指定geo类型的字段location,当前的坐标为:39.93031708627304N,116.4470385453491E。查询一下,看看结果: { …… "hits":[ { "_index":"my_geo", "_type":"_doc", "_id":"AtgtXnIBOZNtuLQtIVdD", "_score":0, "_source":{ "name":"路人甲", "location":{ "lat": 39.90279998006104, "lon": 116.42703999493406 } } }, { "_index":"my_geo", "_type":"_doc", "_id":"ZdguXnIBOZNtuLQtMVfA", "_score":0, "_source":{ "name":"路人乙", "location":{ "lat": 39.93367367974064, "lon": 116.47845257733152 } } } ] } 看来,我们站在“工体”,“北京站”的路人甲和“朝阳公园”的路人乙都在5km的范围内。把范围缩短一点如何,改为3km看看,搜索的请求不变,只是把distance改为3km,看看结果吧, { …… "hits":[ { "_index":"my_geo", "_type":"_doc", "_id":"ZdguXnIBOZNtuLQtMVfA", "_score":0, "_source":{ "name":"路人乙", "location":{ "lat": 39.93367367974064, "lon": 116.47845257733152 } } } ] } 只有在“朝阳公园”的路人乙被搜索了出来。完全符合预期,我们再看看程序中怎么使用GEO搜索。 JAVA 代码 在定义实体类时,对应的GEO字段要使用特殊的类型,如下: @Setter@Getter public class MyGeo { private String name; private GeoPoint location; } location的类型是GeoPoint,添加数据的方法没有变化,转化成Json就可以了。再看看查询怎么用, public void searchGeo() throws IOException { SearchRequest searchRequest = new SearchRequest("my_geo"); SearchSourceBuilder ssb = new SearchSourceBuilder(); //工体的坐标 GeoPoint geoPoint = new GeoPoint(39.93367367974064d,116.47845257733152d); //geo距离查询 name=geo字段 QueryBuilder qb = QueryBuilders.geoDistanceQuery("location") //距离 3KM .distance(3d, DistanceUnit.KILOMETERS) //坐标工体 .point(geoPoint); ssb.query(qb); searchRequest.source(ssb); SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); for (SearchHit hit : response.getHits().getHits()) { System.out.println(hit.getSourceAsString()); } } SearchRequest指定索引my_geo 创建工体的坐标点GeoPoint 创建geo距离查询,指定geo字段location,距离3km,坐标点工体 其他的地方没有变化 运行一下,看看结果, {"name":"路人乙","location":{"lat":39.93360786576342,"lon":116.47853840802}} 只有在“朝阳公园”的路人乙被查询了出来,符合预期。 距离排序 有的小伙伴可能会有这样的疑问,我不想按照距离去查询,只想把查询结果按照离“我”的距离排序,该怎么做呢?再看一下, public void searchGeoSort() throws IOException { SearchRequest searchRequest = new SearchRequest("my_geo"); SearchSourceBuilder ssb = new SearchSourceBuilder(); //工体的坐标 GeoPoint geoPoint = new GeoPoint(39.93367367974064d,116.47845257733152d); GeoDistanceSortBuilder sortBuilder = SortBuilders .geoDistanceSort("location", geoPoint) .order(SortOrder.ASC); ssb.sort(sortBuilder); searchRequest.source(ssb); SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); for (SearchHit hit : response.getHits().getHits()) { System.out.println(hit.getSourceAsString()); } } 这次查询并没有设置查询条件,而是创建了一个geo距离排序,同样,先指定geo字段location,和当前的坐标工体,再设置排序是升序。运行一下,看看结果, {"name":"路人乙","location":{"lat":39.93360786576342,"lon":116.47853840802}} {"name":"路人甲","location":{"lat":39.902799980059335,"lon":116.42721165631102}} 离“工体”比较近的“路人乙”排在了第一个,也是符合预期的。有问题大家评论区留言吧~

优秀的个人博客,低调大师

ES7学习笔记(十一)与SpringBoot结合

在前面的章节中,我们把ES的基本功能都给大家介绍完了,从ES的搭建、创建索引、分词器、到数据的查询,大家发现,我们都是通过ES的API去进行调用,那么,我们在项目当中怎么去使用ES呢?这一节,我们就看看ES如何与我们的SpringBoot项目结合。 版本依赖 SpringBoot默认是有ElasticSearch的Starter,但是它依赖的ES客户端的版本比较低,跟不上ES的更新速度,所以我们在SpringBoot项目中要指定ES的最新版本,如下: <properties> <elasticsearch.version>7.6.1</elasticsearch.version> </properties> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency> 我们在项目中指定ES客户端的版本为7.6.1。 配置文件 然后我们在SpringBoot的配置文件application.properties当中,配置ES集群的地址,如下: spring.elasticsearch.rest.uris=http://192.168.73.130:9200,http://192.168.73.131:9200,http://192.168.73.132:9200 多个地址之间我们使用,隔开即可。 与ES交互 所有配置的东西都准备好了,下面我们看看在程序当中如何交互,还记得前面咱们提到的动态映射吗?这个东西是非常的好用的,简化了我们不少的工作量。在这里我们还用前面的索引ik_index举例,我们先看看目前ik_index索引中有哪些字段, 在索引中只有3个字段,id、title和desc。接下来我们在创建索引ik_index对应的实体类,内容也很简单,具体如下: @Setter@Getter public class IkIndex { private Long id; private String title; private String desc; private String category; } 在实体类中,我们新添加了一个字段category表示分类,我们可以联想一下,category字段动态映射到ES当中会是什么类型?对了,就是text类型,我们再深入想一步,text类型会用到全文索引,会用到分词器,而在索引ik_index当中,我们配置了默认的分词器是IK中文分词器。能够想到这里,我觉得你对ES了解的比较深入了。 接下来,我们就要编写service了,并向ik_index索引中添加一条新的数据,如下: @Service public class EService { @Autowired private RestHighLevelClient client; /** * 添加索引数据 * @throws IOException */ public void insertIkIndex() throws IOException { IkIndex ikIndex = new IkIndex(); ikIndex.setId(10l); ikIndex.setTitle("足球"); ikIndex.setDesc("足球是世界第一运动"); ikIndex.setCategory("体育"); IndexRequest request = new IndexRequest("ik_index"); // request.id("1"); request.source(JSON.toJSONString(ikIndex), XContentType.JSON); IndexResponse indexResponse = client.index(request, RequestOptions.DEFAULT); System.out.println(indexResponse.status()); System.out.println(indexResponse.toString()); } } 首先,我们要引入ES的高等级的客户端RestHighLevelClient,由于我们在配置文件中配置了ES集群的地址,所以SpringBoot自动为我们创建了RestHighLevelClient的实例,我们直接自动注入就可以了。然后在添加索引数据的方法中,我们先把索引对应的实体创建好,并设置对应的值。 接下来我们就要构建索引的请求了,在IndexRequest的构造函数中,我们指定了索引的名称ik_index,索引的id被我们注释掉了,ES会给我们默认生成id,当然自己指定也可以。大家需要注意的是,这个id和IkIndex类里的id不是一个id,这个id是数据在ES索引里的唯一标识,而IkIndex实体类中的id只是一个数据而已,大家一定要区分开。然后我们使用request.source方法将实体类转化为JSON对象并封装到request当中,最后我们调用client的index方法完成数据的插入。我们看看执行结果吧。 CREATED IndexResponse[index=ik_index,type=_doc,id=f20EVHIBK8kOanEwfXbW,version=1,result=created,seqNo=9,primaryTerm=6,shards={"total":2,"successful":2,"failed":0}] status返回的值是CREATED,说明数据添加成功,而后面的响应信息中,包含了很多具体的信息,像每个分片是否成功都已经返回了。我们再用elasticsearch-head插件查询一下,结果如下: 数据插入成功,并且新添加的字段category也有了对应的值,这是我们期望的结果。下面我们再看看查询怎么使用。代码如下: public void searchIndex() throws IOException { SearchRequest searchRequest = new SearchRequest("ik_index"); SearchSourceBuilder ssb = new SearchSourceBuilder(); QueryBuilder qb = new MatchQueryBuilder("desc","香蕉好吃"); ssb.query(qb); searchRequest.source(ssb); SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); SearchHit[] hits = response.getHits().getHits(); for (SearchHit hit : hits) { String record = hit.getSourceAsString(); System.out.println(record); } } 我们先创建一个查询请求,并指定索引为ik_index; 然后我们创建一个请求体SearchSourceBuilder,再构建我们的查询请求QueryBuilder,QueryBuilder是一个接口,它的实现类有很多,对应着ES中的不同种类的查询,比如咱们前面介绍的bool和boosting查询,都有对应的实现类。在这里,咱们使用MatchQueryBuilder并查询desc包含香蕉好吃的数据,这个查询咱们在前面通过API的方式也查询过。 最后我们封装好请求,并通过client.search方法进行查询,返回的结构是SearchResponse。 在返回的结果中,我们获取对应的数据,咦?这个为什么调用了两次Hits方法?咱们可以从API的返回值看出端倪,如下: 我们可以看到返回的结果中确实有两个hits,第一个hits中包含了数据的条数,第二个hits中才是我们想要的查询结果,所以在程序中,我们调用了两次hits。 在每一个hit当中,我们调用getSourceAsString方法,获取JSON格式的结果,我们可以用这个字符串通过JSON工具映射为实体。 我们看看程序运行的结果吧, {"id":1,"title":"香蕉","desc":"香蕉真好吃"} {"id":1,"title":"香蕉","desc":"香蕉真好吃"} {"id":1,"title":"橘子","desc":"橘子真好吃"} {"id":1,"title":"桃子","desc":"桃子真好吃"} {"id":1,"title":"苹果","desc":"苹果真好吃"} 查询出了5条数据,和我们的预期是一样的,由于使用IK中文分词器,所以desc中包含好吃的都被查询了出来,而我们新添加的足球数据并没有查询出来,这也是符合预期的。我们再来看看聚合查询怎么用, public void searchAggregation() throws IOException { SearchRequest searchRequest = new SearchRequest("ik_index"); SearchSourceBuilder ssb = new SearchSourceBuilder(); TermsAggregationBuilder category = AggregationBuilders.terms("category").field("category.keyword"); ssb.aggregation(category); searchRequest.source(ssb); SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); Terms terms = response.getAggregations().get("category"); for (Terms.Bucket bucket : terms.getBuckets()) { System.out.println(bucket.getKey()); System.out.println(bucket.getDocCount()); } } 同样,我们创建一个SearchRequest,然后再创建一个TermsAggregationBuilder,TermsAggregationBuilder我们指定了name叫做category,这个name对应着上一节中的那个自定义的名称,大家还有印象吗? 后面的field是我们要聚合的字段,注意这里因为category字段是text类型,默认是不能够做聚合查询的,我们指定的是category.keyword,还记得这个keyword类型吗?它是不使用分词器的,我们使用这个keyword类型是可以的。 最后把AggregationBuilder封装到查询请求中,进行查询。 查询后,我们怎么去取这个aggregation呢?取查询结果我们是通过hits,取聚合查询,我们要使用aggregation了,然后再get我们的自定义名称response.getAggregations().get("category")。至于前面的类型,它是和AggregationBuilder对应的,在咱们的例子中使用的是TermsAggregationBuilder,那么我们在取结果时就要用Terms;如果查询时使用的是AvgAggregationBuilder,取结果时就要用Avg。 在取得Terms后,我们可以获取里边的值了。运行一下,看看结果。 体育 1 key是体育,doc_count是1,说明分类体育的数据只有1条。完全符合我们的预期,这个聚合查询的功能非常重要,在电商平台中,商品搜索页通常列出所有的商品类目,并且每个类目后面都有这个商品的数量,这个功能就是基于聚合查询实现的。 好了,到这里,ES已经结合到我们的SpringBoot项目中了,并且最基础的功能也已经实现了,大家放心的使用吧~

优秀的个人博客,低调大师

ES[7.6.x]学习笔记(十)聚合查询

聚合查询,它是在搜索的结果上,提供的一些聚合数据信息的方法。比如:求和、最大值、平均数等。聚合查询的类型有很多种,每一种类型都有它自己的目的和输出。在ES中,也有很多种聚合查询,下面我们看看聚合查询的语法结构, "aggregations" : { "<aggregation_name>" : { "<aggregation_type>" : { <aggregation_body> } [,"meta" : { [<meta_data_body>] } ]? [,"aggregations" : { [<sub_aggregation>]+ } ]? } [,"<aggregation_name_2>" : { ... } ]* } aggregations实体包含了所有的聚合查询,如果是多个聚合查询可以用数组,如果只有一个聚合查询使用对象,aggregations也可以简写为aggs。aggregations里边的每一个聚合查询都有一个逻辑名称,这个名称是用户自定义的,在我们的语法结构中,对应的是<aggregation_name>。比如我们的聚合查询要计算平均价格,这时我们自定义的聚合查询的名字就可以叫做avg_price,这个名字要在聚合查询中保持唯一。 在自定义的聚合查询对象中,需要指定聚合查询的类型,这个类型字段往往是对象中的第一个字段,在上面的语法结构中,对应的是<aggregation_type>。在聚合查询的内部,还可以有子聚合查询,对应的是aggregations,但是只有Bucketing 类型的聚合查询才可以有子聚合查询。 metrics 聚合查询 metrics 我觉得在这里翻译成“指标”比较好,也不是太准确,我们还是用英文比较好。metrics 聚合查询的值都是从查询结果中的某一个字段(field)提炼出来的,下面我们就看看一些常用的metrics 聚合查询。我们有如下的一些索引数据,大家先看一下, 索引的名字叫做bank,一些关键的字段有account_number银行账号,balance账户余额,firstname和lastname等,大家可以直接看出它们代表的含义。假如我们想看看银行里所有人的平均余额是多少,那么查询的语句该怎么写呢? POST /bank/_search { "query": { "bool": { "must": { "match_all": {} } } }, "aggs": { "avg_balance": { "avg": { "field": "balance" } } } } 在查询语句中,查询的条件匹配的是全部,在聚合查询中,我们自定义了一个avg_balance的聚合查询,它的类型是avg,求平均数,然后我们指定字段是balance,也就是我们要计算平均数的字段。我们执行一下,然后看看返回的结果, { "took": 11, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": …… "aggregations": { "avg_balance": { "value": 25714.837 } } } 在返回的结果中,我们看到在aggregations中,返回了我们自定义的聚合查询avg_balance,并且计算的平均值是25714.837。 如果我们要查询balance的最大、最小、平均、求和、数量等,可以使用stats查询,我们来看一下如何发送这个请求, POST /bank/_search { "query": { "bool": { "must": { "match_all": {} } } }, "aggs": { "stats_balance": { "stats": { "field": "balance" } } } } 我们只需要把前面聚合查询的类型改为stats就可以了,我们看一下返回的结果, { "took": 20, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": …… "aggregations": { "stats_balance": { "count": 1000, "min": 1011, "max": 49989, "avg": 25714.837, "sum": 25714837 } } } 我们可以看到在返回的结果中,返回了5个字段,我们最常用的最大、最小、平均、求和、数量都包含在内,很方便是不是。 Bucket 聚合查询 Bucket 聚合不像metrics 那样基于某一个值去计算,每一个Bucket (桶)是按照我们定义的准则去判断数据是否会落入桶(bucket)中。一个单独的响应中,bucket(桶)的最大个数默认是10000,我们可以通过serarch.max_buckets去进行调整。 如果从定义来看,理解Bucket聚合查询还是比较难的,而且Bucket聚合查询的种类也有很多,给大家一一介绍不太可能,我们举两个实际中用的比较多的例子吧。在上面的metrics 聚合中,我们可以查询到数量(count),但是我们能不能分组呢?是不是和数据库中的group by联系起来了?对,Bucket 聚合查询就像是数据库中的group by,我们还用上面银行的索引,比如说我们要看各个年龄段的存款人数,那么查询语句我们该怎么写呢?这里就要使用Bucket 聚合中的Terms聚合查询,查询语句如下: POST /bank/_search { "query": { "bool": { "must": { "match_all": {} } } }, "aggs": { "ages": { "terms": { "field": "age" } } } } 其中,ages是我们定义的聚合查询的名称,terms指定要分组的列,我们运行一下,看看结果, …… { "aggregations": { "ages": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 463, "buckets": [ { "key": 31, "doc_count": 61 } , { "key": 39, "doc_count": 60 } , { "key": 26, "doc_count": 59 } , { "key": 32, "doc_count": 52 } , { "key": 35, "doc_count": 52 } , { "key": 36, "doc_count": 52 } , { "key": 22, "doc_count": 51 } , { "key": 28, "doc_count": 51 } , { "key": 33, "doc_count": 50 } , { "key": 34, "doc_count": 49 } ] } } 我们可以看到在返回的结果中,每个年龄的数据都汇总出来了。假如我们要看每个年龄段的存款余额,该怎么办呢?这里就要用到子聚合查询了,在Bucket 聚合中,再加入子聚合查询了,我们看看怎么写, POST /bank/_search { "query": { "bool": { "must": { "match_all": {} } } }, "aggs": { "ages": { "terms": { "field": "age" }, "aggs": { "sum_balance": { "sum": { "field": "balance" } } } } } } 我们在聚合类型terms的后面又加了子聚合查询,在子聚合查询中,又自定义了一个sum_balance的查询,它是一个metrics 聚合查询,要对字段balance进行求和。我们运行一下,看看结果。 "aggregations": { "ages": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 463, "buckets": [ { "key": 31, "doc_count": 61, "sum_balance": { "value": 1727088 } } , { "key": 39, "doc_count": 60, "sum_balance": { "value": 1516175 } } , { "key": 26, "doc_count": 59, "sum_balance": { "value": 1368494 } } , { "key": 32, "doc_count": 52, "sum_balance": { "value": 1245470 } } , { "key": 35, "doc_count": 52, "sum_balance": { "value": 1151108 } } , { "key": 36, "doc_count": 52, "sum_balance": { "value": 1153085 } } , { "key": 22, "doc_count": 51, "sum_balance": { "value": 1261285 } } , { "key": 28, "doc_count": 51, "sum_balance": { "value": 1441968 } } , { "key": 33, "doc_count": 50, "sum_balance": { "value": 1254697 } } , { "key": 34, "doc_count": 49, "sum_balance": { "value": 1313688 } } ] } } 我们看到返回结果中,增加了我们定义的sum_balance字段,它是balance余额的汇总。这个例子我们应该对bucket(桶)这个概念有了一个非常形象的认识了。还有一些其他的bucket聚合查询,这里就不给大家一一介绍了,比如:我们只想查某几个年龄段的余额汇总,就可以使用filters-aggregation。 好了,ES的一些基本的聚合查询就给大家介绍到这里了,如果要用到一些其他的聚合查询,可以参照ES的官方文档。

优秀的个人博客,低调大师

ES7学习笔记(六)分析器

在前面的章节中,我们给大家介绍了索引中的映射类型,也就是每一个字段都有一个类型,比如:long,text,date等。这和我们的数据库非常的相似,那么它的不同之处是什么呢?对了,就是全文索引,在ES当中,只有text类型的字段才会用的全文索引,那么这里就会引出ES中一个非常重要的概念,文本分析器(Text analysis)。 分析器使ES支持全文索引,搜索的结果是和你搜索的内容相关的,而不是你搜索内容的确切匹配。我们用ES官网中的例子给大家举例,假如你在搜索框中输入的内容是Quick fox jumps,你想得到的结果是A quick brown fox jumps over the lazy dog,或者结果中包含这样的词fast fox或foxes leap。 分析器之所以能够使搜索支持全文索引,都是因为有分词器(tokenization),它可以将一句话、一篇文章切分成不同的词语,每个词语都是独立的。假如你在ES索引中添加了一条记录the quick brown fox jumps,而用户搜索时输入的内容是quick fox,并没有完全匹配的内容,但是因为有了分词器,你索引的内容被切分成了不同的、独立的词,用户搜索的内容也会进行相应的切分,所以用户搜索的内容虽然没有完全匹配,但也能够搜索到想要的内容。 分析器除了要做分词,还要做归一化(Normalization)。分词器能够使搜索内容在每一个词上匹配,但这种匹配也只是在字面上进行的匹配。 比如你搜索Quick,但是不能匹配到quick,它们的大小写不同。 比如你搜索fox,但是不能匹配到foxes,它是复数形式。 比如你搜索jumps,不能匹配到leaps,虽然它们是同义词。 为了解决这些问题,分析器要把这些分词归一化到标准的格式。这样我们在搜索的时候就不用严格的匹配了,相似的词语我们也能够检索出来,上面的3种情况,我们也能够搜索出相应的结果。 分析器的组成 分析器,无论是内置的,还是自定义的,都是由3部分组成:字符过滤器(character filters)、分词器(tokenizers)、分词过滤器(token filters)。 字符过滤器 字符过滤器接收最原始的文档,并且可以改变其内容,比如:可以把中文的一二三四五六七八九,变成阿拉伯数字123456789。它还可以过滤html标签,并对其进行转义。还可以通过正则表达式,把匹配到的内容转化成其他的内容。一个分析器可以有多个字符过滤器,也可以没有字符过滤器。 分词器 一个分析器只能有一个确定的分词器,它可以把一句话分成若干个词,比如:空格分词器。当你输入一句话Quick brown fox!,它将被切分成[Quick, brown, fox!]。 分词过滤器 分词过滤器接收分词并且可以改变分词,比如:小写分词过滤器,它将接收到的分词全部转换成小写。助词过滤器,它将删除掉一些公共的助词,比如英语里的 the,is,are等,中文里的的,得等。同义词过滤器,它将在你的分词中,添加相应的同义词。一个分析器可以有多个分词过滤器,它们将按顺序执行。 我们在建立索引和搜索时,都会用的分析器。 配置文本分析器 前面我们讲了分析器的基本概念,也了解了全文搜索的基本步骤。下面我们看一下如何配置文本分析器,ES默认给我们配置的分析器是标准分析器。如果标准的分析器不适合你,你可以指定其他的分析器,或者自定义一个分析器。 ES有分析器的api,我们指定分析器和文本内容,就可以得到分词的结果。比如: POST _analyze { "analyzer": "whitespace", "text": "The quick brown fox." } 返回的结果如下: { "tokens": [ { "token": "The", "start_offset": 0, "end_offset": 3, "type": "word", "position": 0 }, { "token": "quick", "start_offset": 4, "end_offset": 9, "type": "word", "position": 1 }, { "token": "brown", "start_offset": 10, "end_offset": 15, "type": "word", "position": 2 }, { "token": "fox.", "start_offset": 16, "end_offset": 20, "type": "word", "position": 3 } ] } 我们指定的分析器是空格分析器,输入的文本内容是The quick brown fox.,返回结果是用空格切分的四个词。我们也可以测试分析器的组合,比如: POST _analyze { "tokenizer": "standard", "filter": [ "lowercase", "asciifolding" ], "text": "Is this déja vu?" } 我们指定了标准的分词器,小写过滤器和asciifolding过滤器。输入的内容是Is this déja vu?,我们执行一下,得到如下的结果: { "tokens": [ { "token": "is", "start_offset": 0, "end_offset": 2, "type": "<ALPHANUM>", "position": 0 }, { "token": "this", "start_offset": 3, "end_offset": 7, "type": "<ALPHANUM>", "position": 1 }, { "token": "deja", "start_offset": 8, "end_offset": 12, "type": "<ALPHANUM>", "position": 2 }, { "token": "vu", "start_offset": 13, "end_offset": 15, "type": "<ALPHANUM>", "position": 3 } ] } 我们可以看到结果中,is变成了小写,déja变成了deja,最后的?也过滤掉了。 为指定的字段配置分析器 我们在创建映射时,可以为每一个text类型的字段指定分析器,例如: PUT my_index { "mappings": { "properties": { "title": { "type": "text", "analyzer": "whitespace" } } } } 我们在my_index索引中,创建了title字段,它的类型是text,它的分析器是whitespace空格分析器。 为索引指定默认的分析器 如果我们觉得为每一个字段指定分析器过于麻烦,我们还可以为索引指定一个默认的分词器,如下: PUT my_index { "settings": { "analysis": { "analyzer": { "default": { "type": "whitespace" } } } } } 我们为my_index索引指定了默认的分析器whitespace。这样我们在创建text类型的字段时,就不用为其指定分析器了。 这一节给大家介绍了分析器,我们可以看到例子中都是使用的英文分析器,下一节我们一起看一下强大的中文分析器。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册