坑爹的部署脚本

我司在开发机器上部署最新版的代码需要用到一些陈年的脚本,经历以下3个步骤:

  1. 执行一个叫 mkview.sh 脚本,脚本运行之后会提示你输入 input 一个URL地址,就是你仓库分支的 gitlab 地址。然后这个脚本会去拉最新的代码分支;
  2. 执行一个叫 build.sh 的脚本,等待 3min;
  3. 执行一个脚 deploy.sh 的脚本,等待5min;

虽然说只有三步,但是太反人类了。首先必须 ssh 登陆执行,然后竟然是命令提示输入这种奇葩形式,而不是直接参数传入。另外这个竟然是传 URL 进去(然后脚本里面奇葩的再把 URL 解析成仓库名字,分支名字)。每次输入之间还要等待几分钟,烦的要死。

操作了几次就受不了了,所以就想办法简化这些步骤。

重写的尝试

首先尝试的是把这三个脚本合成一个,我来打开看看这里面都是什么妖魔鬼怪。结果一打开发现事情并不简单,这都是上千行的脚本(虽然我也不知道里面都写了些啥),然后中间还调用了一些 binary,里面掺杂着各种 xxx 已转岗,xxx 不再维护的注释。看了半天,我连哪里把 git 权限塞进去的都没找到,直接 clone 是不行的,这个脚本就有权限 clone。头大,我还是不去动他了。

上 Ansible

然后我就尝试用 Ansible 处理这三个操作。

处理 prompt

首先要解决的是那个蛋疼的 prompt 问题,毕竟我可不想每次都去拼 URL。搜索了一下,看起来只有这个 expect 模块能够解决终端提示输入的问题。看了一下,不出所料,实现其实是用的 pexpect 包,这个包我最近在开发终端工具 iredis 的时候用了很多。其实这个包坑也蛮多的,比如找不到预期输出的时候只能等待 Timeout。我猜大家都用这个是可能没有更好的选择吧。

因为这是 Ansible 在 remote 机器上的功能,所以需要给机器装上 pexpect。可我司机器 Python 版本是 2.6,没有自带pip……折腾一顿之后,总算是装上了 pexpect。

执行的时候,出现更加蛋疼的编码问题。好吧,看了一下,发现这个脚本把编码写死在脚本里面了。也太反人类了。

放弃这条路了。然后我想这个输入是从键盘输入进去的啊,也就意味着 stdin 输入进去就可以了。试了一下直接用下面这命令就可以:

假设 /bin/script 需要用户输入的话,user_input 就会输入进去。

其实这个问题之前遇到过的,像 apt 这种东西要你确认的Y/n?  可以这样: yes | apt install htop ,yes 会一直输出 y 。当然也可以直接用 -y 选项,正常的脚本都会有这种选项的。

处理 Daemon 进程问题

花了我时间最多的,是一个 deploy.sh 那个操作。很神奇的是,我直接 ssh 跳上去执行,部署成功。

但是放在 Ansible 里面,一模一样的命令,就不行了。

shell command 不同的模块都试过了,换 bash deploy.sh 啥的来执行也试过,都不行。但是直接 ssh 去执行就可以。

Ansible 和 ssh 到底差在哪里呢?

想不出来,去问老师。

老师说:

-_-|| 看了一下,果然是 Ansible 退出的时候会清理一个 session group 的进程

我试了下 Ansible 的 async 功能,好像还是没用,过段时间进程还是被杀掉的。最后直接暴力使用 setsid 重置进程组,父进程直接设置为1,就好了。命令是这么写的:

有关 setsid,nohup 和 disown 的区别,这篇文章写得很好,可以看下。

 

等等,这明明是 deploy.sh 脚本啊,部署的时候不应该把进程 id 啥的都给处理好吗?我都是在经历了些啥啊!

 

Java8 Stream API 介绍

Java 从版本8开始支持“Stream API”,即函数式编程,可以用简单的代码表达出比较复杂的遍历操作。本文介绍这些 Stream API 的基本概念,用法,以及一些参考资料。我之前写 Python 比较多,所以一些地方可能用 Python 的视角来解释。

简单用法

这些函数和 Python 的 filter map sort 很像了,所以很容看懂。就是先过滤出以 "b" 开头的字符串,然后用 map 转换成大写的方式,排序之后,输出。

Stream API 中有一个概念,将这些 API 分成了两种:

  1. 中间结果(intermediate):像 filter map sorted 的结果都是中间结果,可以继续使用 Stream API 连续调用;
  2. 最终结果(terminal):类似 forEach ,这种 API 将会终止 Stream 的调用,它们的结果要么是 void ,要么是一个非 Stream 结果。

连续调用 Stream 的方式叫做  operation pipeline。所有的 Stream API 可以参考这个 javadoc

大多数的 Stream 操作都接收一个 lambda 表达式作为参数,Lambda 表达式描述了 Stream 操作的一个具体行为,通常都是 stateless 和 non-interfering 的。

Non-interfering 意味着它不会修改原始的数据,比如上面的例子,没有操作去动 myList,迭代结束之后,myList 还是保持着原来的样子。

Stateless 意味着操作都是确定的,没有依赖外面的变量(导致可能在执行期间改变)。

不同类型的 Stream 操作

Stream 可以从不同的数据类型创建,尤其是集合(Collections)。

List 和 Set 支持 stream() 方法和 parallelStream() 方法,parallenStream() 可以在多线程中执行。

在 List 上调用 stream()可以创建一个 Stream 对象。

但是我们不必专门为了创建 Stream 对象而创建一个集合:

Stream.of() 可以从一些对象引用中自动创建一个 Stream。

除了从对象创建 Stream,Java8 还提供了方法从基本类型创建 Stream,比如int long double,这些方法分别是 IntStream LongStream DoubleStream.

IntStream 可以用来替代 for 循环:

基本类型的 Stream 和普通的 Stream 对象基本一样,几点区别如下:

  1. 基本类型使用特殊的 lambda 表达式,比如 IntFunction 之于 Function , IntPredicate 之于 Predicate;
  2. 基本类型支持一些特殊的“最终结果API”,比如 sum() average()

有时候,我们想把普通的 Stream 转换成原始类型的 Stream,比如我们想用 max() ,这时可以使用转换的方法 mapToInt() mapToLong() mapToDouble():

原始类型可以通过 mapToObject 方法,将原始类型的 Stream 转换成普通的 Stream 对象。

下面这个例子结合了普通的 Stream 和原始类型的 Stream:

执行顺序

前面介绍了 Stream 的基本概念,下面开始深入原理。

产生中间结果的 Stream 一个比较重要的特性是,它是惰性的。下面这个例子,我们只有中间结果,没有最终结果的 Stream,最终 println 不会被执行。

因为 Stream 操作是惰性的,只有用到的时候才会真正执行。

如果我们在后面加上一个终止类型的 Stream 操作,println 就会执行了。

这段代码的输出如下:

注意从输出的顺序也可以看到“惰性执行”的特征:并不是所有的 filter 都打印出来,再打印出来 forEeach。而是一个元素执行到底,再去执行下一个元素。

这样可以减少执行的次数。参考下面这个例子:

anyMatch 会在找到第一个符合条件的元素就返回。这样我们并不需要对有的元素执行 map ,在第一个 anyMatch 返回 true 之后,执行就结束了。所以前面的中间状态 Stream 操作,会执行尽可能少的次数。

执行的顺序很重要(Stream 的优化)

下面这个例子,我们用了两个生成中间结果的 Stream 操作 map filter,和一个最终结果的操作 forEach 。

map filter 各执行了5次,forEach 执行了1次。

如果我们在这里稍微改变一下顺序,将 filter 提前执行,可以将 map 的执行次数减少到1次。(有点像 SQL 优化)

现在 map 只执行一次了,在操作很大的集合的时候非常有用。

下面我们引入一下 sorted 这个操作:

排序是一个特殊的中间操作,是一个 stateful 的操作。因为需要原地排序。

输出如下:

排序操作会在整个集合上执行。所以和之前的“垂直”执行不同,排序操作是水平执行的。注意排序影响的只是后面的 Stream 操作,对于原来的集合,顺序依然是不变的。参考这段代码:

输出如下(注意原来的 foo 并没有变化):

Sort 也有惰性执行的特性,如果我们改变一下上面那个例子的执行顺序:

可以发现 sorted 不会执行,因为 filter 只产生了一个元素。

Stream 的重用

Java8 的 Stream 是不支持重用的。一旦调用了终止类型的 Stream 操作,Stream 会被 close。

在同一个 Stream 上,先调用 noneMatch 再调用 anyMatch 会看到以下异常:

所以,我们必须为每一个终止类型的 Stream 操作创建一个新的 Stream。可以用 Stream Supplier 来实现。

每次调用 get() 都会得到一个新的 Stream。

高级操作

Stream 支持的操作很多(不像Python的函数式编程只支持4个)。我们已经见过了最常用的 filter 和 map。其他的操作读者可以自行阅读 Stream 文档。这里,我们再试一下几个复杂的操作:collect flatMap reduce.

下面的例子都会使用一个 Person 的 List 来演示。

Collect

Collect 是很有用的一个终止类型的 Stream 操作,可以将 Stream 转换成集合结果,比如 List Set Map 。Collect 接收一个 Collector 作为参数,Collector 需要支持4种操作:

  1. supplier
  2. accumulator
  3. combiner
  4. finisher

听起来实现很复杂,但是好处是 Java8 已经内置了常用的 Collector,所以大多数情况下我们不需要自己实现。

下面看一个常用的操作:

可以看到这个 Stream 操作最后构建了一个 List,如果需要 Set 的话只需要将 toList() 换成 toSet()

接下来这个例子,将对象按照属性存放到 Map 中。

Collectors 非常实用,还可以对 Stream 进行聚合,比如计算所有 Person 的平均年龄:

如果需要更全面的统计数据,可以试一下 summarizing Collector,这个内置的 Collector 提供了 count, sum, min, max 等有用的数据。

下面这个例子,将所有的对象 join 成一个 String:

Join Collector 的参数是一个分隔符,一个可选的前缀和后缀。

将 Stream 元素转换成 map 的时候,需要特别注意:key 必须是唯一的,否则会抛出 IllegalStateException 。但是我们可以传入一个 merge function,来指定重复的元素映射的方式:

最后,来尝试一下实现自己的 Collector。前面已经提到过,实现一个 Collector,我们需要提供4个东西:supplier,accumulator,combiner,finisher.

下面这个 Collector 将所有的 Person 对象转换成一个字符串,名字全部大写,中间用 | 分割。

Java 的 String 是不可修改的,所以这里需要一个 helper class StringJoiner,来构建最终的 String。

  1. 首先 supplier 构建了一个 StringJoiner,以 | 作为分隔符;
  2. 然后 accumulator 将每个 Person 的 name 转换成大写;
  3. combiner 将2个 StringJoiners 合并成1个;
  4. 最后 finisher 从 StringJoiner 构建最终的 String。

FlatMap

前面我们演示了如何用 map 将一种类型的对象转换成另一种类型。但是 map 也有一些限制:一个对象只能转换成一个对象,如果需要将一个对象转换成多个就不行了。所以还有一个 flatMap 。

FlatMap 可以将 Stream 中的每一个对象转换成0个,1个或多个。无论产生多少对象,最终都会放到同一个 Stream 中,供后面的操作消费。

下面演示 flatMap 的功能,我们需要一个有继承关系的类型:

下面,我们使用 Stream 来初始化多个几个对象:

现在我们生成了一个 List,包含3个 foo,每个 foo 中包含3个 bar.

FlatMap 接收一个方法,返回一个 Stream,可以包含任意个 Objects. 所以我们可以用这个方法得到 foo 中的每一个 bar:

上面这个代码将 foo 的 Stream 转换成了包含 9 个 bar 的 Stream。

上面所有的代码也可以简化到一个 Stream 操作中:

flatMap 中也可以用 Optional 对象,Optional 是 Java8 引入的,可以检查 null 的一种机制。结合 Optional 和 flatMap 我们可以相对优雅地处理 null ,考虑下面这种数据结构:

为了正确地得到 Inner 中的 foo String,我们要这么写:

flatMap 的话,我们可以这么写:

每一个 flatMap 都用 Optional 封装,如果不是空,就返回里面的对象,如果是空的话就返回一个 null .

Reduce

Reduce 操作可以将所有的元素编程一个结果。Java8 支持3种不同的 reduce 方法。

第一种可以将 Stream 中的元素聚合成一个。比如下面的代码,可以找到 Stream 中年龄最大的 Person.

reduce 方法接收一个二元函数(一个只有两个参数的函数)作为参数,返回一个对象。(所以叫做 reduce)

第二种 reduce 接收一个初始对象,和一个二元函数。通常可以用于聚合操作(比如累加)。

第三种 reduce 方法接收3个参数:一个初始化对象,一个二元函数,和一个 combiner 函数。

初始化值并不一定是 Stream 中的对象,所以我们可以直接用一个整数。

结果依然是 76,那么原理是什么呢?我们可以打印出来执行过程:

可以看到 accumulator 做了所有的工作,将所有的年龄和初始化的 int 值 0 相加。但是 combiner 没有执行?

我们将 Stream 换成 parallelStream 再来看一下:

这次 combiner 执行了。并发执行的 Stream 有不同的行为。Accumulator 是并发执行的,所以需要一个 combiner 将所有的并发得到的结果再聚合起来。

下面来看一下 Parallel Stream。

Parallel Stream

因为 Stream 中每一个元素都是单独执行的,可想而知,如果并行计算每一个元素的话,可以提升性能。Parallel Stream 就是适用这种场景的。Parallel Stream 使用公共的 ForkJoinPool 来并行计算。底层的真正的线程数据取决于 CPU 的核数,默认是3.

这个值可以通过 JVM 参数修改:

Collections 可以通过 parallelStream() 来创建一个并行执行的 Stream,可以在普通的 Stream 上执行 parallel() 来转换成并行执行的 Stream。

下面这个例子,将并行执行的每一步的线程执行者打印出来:

输出如下,展示了每一步都是由哪一个线程来执行的:

从上面的结果页可以看出,所有的 ForkJoinPool 中的线程都参与了计算。

如果在上面的例子中加入一个 sort 操作,结果就有些不同了:

结果如下:

看起来 sort 好像是顺序执行的。实际上,sort 使用的是 Java8 的 Arrays.parallelSort() 方法,文档里提到,这里的排序是否真正的并行执行取决于数组的长度,如果长的话就会用并行排序,否则就用单线程排序:

If the length of the specified array is less than the minimum granularity, then it is sorted using the appropriate Arrays.sort method.

 

回到之前的 reduce 方法,我们知道 combiner 只会在并行的时候执行,现在来看一下这个方法到底是做什么的:

可以看到 accumulator 和 combiner 都使用了多线程来运行:

综上,在数据量很大的时候,并行执行的 Stream 可以带来很大的性能提升。但是注意像 reduce 和 collect 这样的操作,需要特殊的 combiner。(因为前一操作产生的类型不同,需要做聚合,所以无法和迁移操作的函数一样,需要另外提供)。

另外要注意的是,Parallel Stream 底层使用的通用的 ForkJoinPool ,所以需要注意不要在并行的 Stream 中出现很慢或阻塞的操作,这样会影响其他并行任务。

 

以上就是基本的 Stream API 介绍了,强烈建议阅读 Java8 的官方文档。

参考资料:

  1. package summary
  2. Document
  3. Toturial
  4. Java 8 Stream Tutorial
  5. Collection Pipeline by Martin Fowler
  6. Stream.js Javascript 版的 Java Stream API
 

IRedis 开发记2:CircleCI workflow 自动发布到 pypi

上周末设置了一下在 CircleCI 的流程,之前也讲过如何设置一个 Django 的 CircleCI,那只是一个简单的 CI build。本文讲一下如何设置一个更加复杂和自动化的流程。以及 IRedis 项目最近一些开发进展(项目主页)。

完整的 Workflow 🎉

CircleCI 整体的使用体验不错,相比于之前只用了 jobs,这一次引入了 workflow 的概念。workflow 就是先定义好 jobs,然后在 Workflow 中将这些 jobs 组合起来,形成一个完整的开发部署流程。

现在,只要我在 master 分支上打一个 tag,就会触发整个构建流程:

首先会进行单元测试、black 强制代码风格检查,flake8 检查,全部通过后就会构建一个 pip package,然后测试这个 package 是否可用,确认可用之后上传到 pypi。上传这一步只会在有 tag 的时候执行,如果没有 tag 就说明不是一个新版本,不会上传。

实现方法可以参考这个 PR。总体上是比较简单的,就是用 yaml 文件编排一个 workflow。

这里有一个小坑,是完成“只在有 tag 的时候上传”这个语义实现上。CircleCI 不支持“仅在 Master 分支上,有 tag 的时候执行”这个语义的。即不支持 AND 关系,只支持 OR 关系。

如果你使用下面这种声明方式:

那么实际的意思是:

  1. master 分支所有的 commit 都执行;
  2. tag 符合条件的都执行;

即满足任何一个条件都会执行,而不是两者同时满足才会执行。

这个坑踩了很久,才发现一个 work around 的方法:

这样首先我们忽略所有的 branch,这样所有的分支都不会执行,然后只有在有新 tag 的时候执行。就可以完成上传的需求了。

上传到 Pypi 很简单,首先 pypi.org 提供了针对每一个包设置一个上传 Token 的功能,非常好用,权限最小化。然后 CircleCI 设置一个环境变量,在 CI 中读取这个环境变量即可。配合上传包的命令行工具 twine 可以这么用:

其中注意 Token 的申请,和设置,需要分别在 Pypi.org 和 CircleCI 的 web 界面上操作好。

Workflow 中可以添加定时构建任务,在需要 nightly build 的项目中非常实用;也可以添加需要手动确认的任务,比如将自动发布集成到流程中,但是发布之前需要手动确认一下。More on this. 但是要知道,每一个手动确认的东西,在增加了安全性的同时,也拉长了整个流程,有人操作的地方就会卡住,会成为最耗时的东西,某一天也许会成为一种形式主义,所以我个人非常不喜欢需要人工确认的东西。

除了发布到 pypi,因为这个项目是一个 Redis 的命令行工具,所以也准备发布到其他的软件源,比如 apt,yum,brew 等。

再来说下这段时间其他一些更新。

更友好的补全

key 的补全,我自己实现了一个超屌的 Completer,在做自动补全的时候,最近使用过的 key 会出现在最前面(PR)。除了 KEYS 命令之外,所有包含 key 的 Redis 命令都会更新 Completer。效果如下(注意所有包含 key 的命令,都将输入过的 key 放到了 completers 列表中):

支持命令提示

也许在上张图你发现了,在 最底下一行含有命令的提示,包括这个命令适用于 Redis 哪种数据类型,命令的语法是什么,什么 redis-server 版本出现的,命令的复杂度是什么。redis-cli 的 readline 风格提示,在输入的过程中如果匹配不上命令,hint 会消失,我觉得体验不太好。另外输入的阴影我用来显示历史命令了,用  键可以快速输入。

放一张开发的时候写的脚本,渲染出所有的命令(laike9m说我的配色辣眼睛):

Transaction 支持

这是某天我想出来的一个 idea,觉得不错,就是在 transaction 状态中的时候,用右侧的 prompt 来提示用户状态。这个想法我记了 issue,Github 上的哥们帮我实现了(Thanks guoweikuang)。

效果如下:

其他还有一个项目内部中的重构,比如一套更加合理的命令前 hook,命令后 hook 方式,命令结果渲染框架等。打包方式也替换成了 poetry,写 pyproject.toml 比写 setup.py 爽多了。

发点牢骚,很多人对这种工具开发不屑一顾,张口闭口高并发,觉得架构设计啊才能显示出自己的技术,但是代码写的稀烂,一个很简单的系统虽然几十万并发量,却用了上千台机器,算下来一分钟请求才几百而已,非常可笑。我觉得一个靠谱的程序员首先应该精通自己的工具,工具不好工作就不能高效,自己的工具遇到问题解决不好,工作上遇到问题也稀松。自己的问题没有现成的工具解决,就没办法了,工作上除了搭积木水平也一般。

牢骚发完,顺便说下,这个项目正在开发中,如果你对一个 Redis 命令行有什么期待,有什么 Feature Request,或者想法,欢迎 issues,我来实现。也欢迎参与到开发中。目前有很多命令还没有实现,最近我在写 string 部分的命令。打算在所有的命令都实现了语法解析和结果渲染之后发布1.0版本。

 

认识Hibernate

最近工作的项目中,用的 ORM 技术是 Hibernate,学习了一下它的用法,正好 PyCon 上我有一个演讲主题是介绍 Django 的 ORM,可以拿来比较一下。这篇文章介绍了 Hibernate 的定位,基本的概念,以及用代码演示了如何使用 Hibernate。本文的内容参考了 jboss 上的一篇教程,所有的代码直接下载:hibernate-tutorials.zip

Hibernate 是什么?

Java 是一个面向对象的编程语言,数据库提供的数据结构只有 Table。所以我们在读写数据库的数据的时候不可避免的要进行结构的转换,保存数据的时候,将 Object 变成 Table 可以保存的形式,读回数据的时候要转换回来。

这样每次在读写数据的时候做转换,是非常重复的,开发成本很高。Hibernate 就是一个 对象/关系映射 的转换解决方案。在 Java 程序中,我们只写类,数据在保存的时候,Hibernate 负责将类的属性转换成 Table 的 Column。更确切的说,如果没有 Hibernate,我们就需要通过写 SQL 和用 JDBC 来跟数据库交互。

和其他 ORM 不同,Hibernate 没有完全屏蔽 SQL。并承诺你的关系型数据库的知识,在 Hibernate 下依然有价值(来源)。

一、使用 Hibernate 原生的 API 读取和保存数据

ORM 需要做的最终要的一件事情是负责 Class 和 Table 的数据结构的转换,在 Hibernate 中,定义这种转换有两种方式:通过配置文件,或者通过注解。

使用 Mapping 配置文件来映射Java class和Table的关系

Bundle/resources 下面的 hibernate.cfg.xml 是 Hibernate 的配置文件。有数据库地址,连接参数等配置。其中 dialect 定义了使用哪种SQL方言,在使用特定数据库的时候可以使用。

auto 可以指定启动的时候自动创建表结构:

这个值可选的参数如下:

  1. create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  2. create-drop :每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  3. update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据 model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等 应用第一次运行起来后才会。
  4. validate :每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

<mapping /> 可以告诉 Hibernate,去哪里找描述 Class 和 Table 对应的 mapping 文件。

对应路径的 mapping 文件,描述了 Java 的 Class 如何和数据库的 Table 对应:

Hibernate 会使用 java.lang.ClassLoader 去加载这些类。

有关 Entity 有两点需要注意:

  1. 这个类用标准的 JavaBean 命令方式设置 getter 和 setter 方法,private 属性也有。这是推荐做法但不是必须做法;
  2. 无参数构造函数是必须有的,Hibernate 需要用Java的反射通过这个构造函数构造对象。

Mapping 文件的详细解释

父节点的属性中,name 结合 package 定义个 FQN,对应 java 的 class;table 定义了数据库的表名字;

子节点中:

  • 需要定义 <id/> Element 来告诉 Hibernate 如何找到表中的唯一的一个 row;推荐使用 Primary key
  • <property/> 定义了属性和表字段的 mapping,column 定义了表 Column 的名字,如果不写的话 Hibernate 默认会使用 property 的 name;
  • type 是数据类型。这个数据类型既不是 SQL 的类型,也不是 Java 的类型,而是 Hibernate 的类型,负责在 Java 和 SQL 之间做转换。如果这个 type 没写的话,Hibernate 会尝试通过 Java 的反射,按照 Java 的类型找到对应的 mapping 类型;

测试代码

示例代码如下(不要复制粘贴,如果需要运行请直接复制本文开头提供的附件):

下面来分析代码。

在 setUp 中,首先构建了一个 org.hibernate.boot.registry.StandardServiceRegistry ,用来处理 hibernate.cfg.xml 等配置信息(sessionFactory 会使用 serviceRegistry 中的配置信息)。

在 setUp 中,首先构建了一个 org.hibernate.boot.registry.StandardServiceRegistry ,用来处理 hibernate.cfg.xml 等配置信息(sessionFactory 会使用 serviceRegistry 中的配置信息)。

我们使用 org.hibernate.boot.MetadataSource 先创建了 org.hibernate.boot.MetadataSources ,这个类表示了 domain Model。然后基于此创建了 SessionFactory。

最后一步就是创建 SessionFactory 了,这是一个全局单例的类,线程安全。

SessionFactory 是 Session 的工厂类,每次使用一个 Session 来完成一块任务:

以上代码中,testBasicUseage() 先创建了一个 Event 对象,然后交给 Hibernate 处理,Hibernate 的 save() 方法将其插入数据库中。

获取对象的代码如下:

我们通过写 Hibernate Query Language 向 Hibernate 表达查询,Hibernate 将其转换成 SQL 执行查询,然后将结果转换成我们需要的 class。

使用注解描述 Java class 和 Table 的映射关系

使用注解API,在配置文件中我们的 mapping 字段需要设置为:

其他的配置项和前面一样。然后,我们在定义 class 和 table 的映射的时候,不再使用 xml 文件了,而是直接在 class 上面注释:

@Entity 的注解和 xml 中的 <class /> 作用一样。这里显示地指定了表的名字,如果不指定的话,就会默认使用类名 EVENT

字段的定义如下:

@javax.persistence.Id 定义了实体的 ID;

@javax.persistence.GeneratedValue@org.hibernate.annotations.GenericGenerator 指示 Hibernate 应该用 increment 生成器自动生成这个字段。

同 xml 配置方式一样,date 也需要特殊处理一下。

实体的属性默认将会持久化,所以这个类中我们没有写 title 的 annotation,但是 title 也会被持久化。

示例代码:这部分的例子和上面一样。

二、使用 Java Persistence API(JPA)

上面我们使用的配置文件是 hibernate.cfg.xml,而 JPA 定义了自己的配置方式,叫做 persistence.xml 。启动方式是 JPA 定义的规范,Hibernate 作为持久化的提供者,需要读取并应用 META-INF/persistence.xml 里面的设置。

persistence.xml 文件范例如下:

对于每一个 persistence-unit 需要提供一个 unique name。应用在获得 javax.persistence.EntityManagerFactory 引用的时候,使用这个 unique name 来获得配置的引用。

之前的配置文件中的配置项,先在要使用 javax.persistence 前缀来区分开。对于 Herbenate 特殊肚饿配置项要用 hibernate. 前缀。

除此之外,<class /> 的内容和我们在前面看到的 Hibernate 配置文件一样。

示例代码

前面的例子中使用的是 Hibernate 的 native API,这里我们使用 JPA 的 API 。

获取 SessionFactory:

注意这里使用的 persistence unit name 是 org.hibernate.tutorial.jpa ,和上面提到的配置问题件对应。

使用 javax.persistence.EntityManager 保存实体。Hibernate 中 save 的步骤,在 JPA 中叫做 persist.

三、Envers 功能

Hibernate 有一个功能,叫做 Envers,就是可以将数据的历史版本都保存下来。数据及时更改了,也可以找到曾经的状态。

这个功能也不是无成本的,需要一些保存历史版本,保存和读取的时候,也需要额外的计算,所以我们只在需要的时候这么做。

我们通过@org.hibernate.envers.Audited 这个注解告诉Hibernate需要保留这个类的历史版本,然后,我们可以 org.hibernate.envers.AuditReader 来读取数据的历史版本。

示例代码

 

参考资料:

 

欧格玛教会与言论自由

我喜欢欧格玛教会,欧格玛教会认为知识是至高无上的,尤其是最原始的知识,最纯粹的理念。理念没有重量,但是可以移山。

欧格玛教会信奉没有任何知识是应该被销毁的,知识本身是没有问题的,可怕的是使用它的人。即使是最疯狂、最错误的想法,藏匿起来也无济于事,应该交给大众来判断。所以欧格玛教会神殿内经常会藏有被其他教会认为是禁忌的知识,比如死灵术。

欧格玛信仰如下:

Knowledge, particularly the raw knowledge of ideas, is supreme. An idea has no weight, but it can move mountains. The greatest gift of humankind, an idea outweighs anything made by mortal hands. Knowledge is power and must be used with care, but hiding it away from others is never a good thing. Stifle no new ideas, no matter how false and crazed they seem; rather, let them be heard and considered. Never slay a singer, nor stand by as others do so. Spread knowledge wherever it is prudent to do so. Curb and deny falsehoods, rumor, and deceitful tales whenever you encounter them. Write or copy lore of great value at least once a year and give it away. Sponsor and teach bards, scribes, and record keepers. Spread truth and knowledge so that all folk know more. Never deliver a message falsely or incompletely. Teach reading and writing to those who ask (if your time permits), and charge no fee for the teaching.

我在我的博客上写着:我的梦想是让网络变得更加开放、自由和快速。

对于网络,我的想法也是所有的内容不都应该被删除、藏匿。应该交由大众去判断。这是我理解的言论自由。但是最近发生了两件事,让我开始怀疑自己的想法。

第一个是 Cloudflare 停止了对 8chan 的服务

CloudFlare 是我非常崇拜的一家公司,我一度认为互联网服务就应该是 CloudFlare 提供的那样。但是有证据证明,一起枪击事件的犯罪嫌疑人从 8chan 上受到了鼓动,从而导致了这起枪击事件。8chan 创建的本意是 4chan 依然会删除部分内容,所以 8chan 想创造一个不会删除内容的 4chan,最终却成为了仇恨的温床。

第二个是Twitter关闭了一批来自大陆的账号

这么做的理由是 Twitter 认为,这部分账号进行了受组织协调的行动。

那么究竟,我们需要怎么样的言论自由呢?