crosstab 是 postgresql 的 tablefunc 扩展模块所提供的函数之一,tablefunc 提供了多个返回多行数据的函数,crosstab 便是其一,crosstab 提供了一种将多行数据转换成一行数据的能力,可以简单地理解为行转列。postgresql 帮助文档上都有对 crosstab 函数的详细介绍,不过使用的例子比较抽象,再加上粗陋的翻译质量可能让人不是很好理解,在此总结一下 crosstab 这个函数,并提供一个手册上没有的实用技巧(见本篇后半部的《双参数的 crosstab 示例2》)。

安装 tablefunc 模块

默认情况下 tablefunc 模块是未安装的,需要手动执行下方命令安装,重复执行会提示该扩展已创建:

CREATE EXTENSION tablefunc;

创建示例表

创建示例表:

CREATE table prod_attr
(
    prod_name      VARCHAR(20), -- 产品名称
    attr_name      VARCHAR(40), -- 属性名称
    attr_val       NUMERIC(15, 2), -- 属性值
    attr_val_desc  VARCHAR(20) -- 属性单位
);

写入示例数据:

insert into prod_attr(prod_name, attr_name, attr_val, attr_val_desc)
VALUES
('农夫山泉','库存数量', 100, '瓶'),
('农夫山泉','规格', 500, '毫升'),
('农夫山泉','单价', 2.00, '元'),
('雪碧','规格', 200, '毫升'),
('雪碧','单价', 1.80, '元'),
('雪碧','折后价', 1.60, '元'),
('手工面包','库存数量', 10, '盒'),
('手工面包','规格', 300, '克'),
('手工面包','单价', 12.00, '元'),
('手工面包','保质期', 7, '天');

执行查询:

select * from prod_attr;

下图是表里的原始数据:

在进一步介绍上述语句前先介绍下示例表和示例表中的数据:
示例表有四个字段,前三个字段分别是产品名称(prod_name),属性名称(attr_name)和属性值(attr_val),第四个字段(attr_val_desc)在这里并未实际使用,仅出于方便理解的目的作为对第三个字段的解释。

通过插入的实际值能够看出这是一个记录产品常见属性的表,如规格、价格、库存数量、保质期,每一个这样的产品属性占据一行,即同一个产品有几个属性就有几行数据在表里。

这里我们使用 crosstab 的最终目的就是要把分散在每一行的这些产品属性置于同一行来显示,以便用户使用,可以简单地把这种行为理解为行转列。

crosstab 第一个参数的约束限制

上述语句中的 crosstab 只有一个字符串参数,这个字符串参数必须是一个合法的 sql 查询,它的作用是从数据表中找出来需要转换处理的目标数据,这个查询需要满足以下几个条件:

  1. select 字段:必须具有顺序固定的三个字段列,第一列是名称(示例是产品名称 prod_name),第二列是属性名称(示例是属性名称 atrr_name),第三列是属性值(示例是属性值 attr_val);
  2. order by 字句:order by 子句的作用是给 select 子句的前两个字段(prod_name、atrr_name)排序,顺序必须是第一个字段在前、第二个字段在后,order by 不能省略且字段顺序一定要处理好,否则执行结果会有问题;

定义 crosstab 的返回字段和数据类型

同时,由于 crosstab 函数定义的返回类型为 record 即多行记录,需要在 crosstab 的 from 子句中为 crosstab 定义返回的字段及数据类型,这里的字段顺序是固定的,第一个字段名及其数据类型同 select 子句的第一个字段(prod_name),后续字段可以根据实际需要(一般是需要显示几个属性就定义几个)自行定义,但是它们的数据类型必备与 select 子句的第三个字段(attr_val)相同。

至此,上述 crosstab 的使用就清楚了,如果你不想看上述繁琐的文字叙述,看一下接下来的图片即明白这个函数的作用了。

一个参数的 crosstab 示例

注意参数中的 sql 里加了 where 条件限制,移除 where 后执行查询你会发现这里有一个问题

select
prod_name,attr_a as "规格",attr_b as "库存数量",attr_c as "单价", attr_d as "保质期"
from crosstab(
    'select prod_name, attr_name, attr_val
       from prod_attr t1
      where t1.prod_name in (''农夫山泉'', ''手工面包'') -- 条件过滤
   order by prod_name, attr_name desc'
) as cte(prod_name VARCHAR, attr_a NUMERIC, attr_b NUMERIC, attr_c NUMERIC, attr_d NUMERIC)

这是通过 crosstab 转换后的数据:

是不是很简单,这里的前提是建立在每个商品属性一致的(即有都有同样的属性,即便其值有所不同值)假设前提下,使用者需要做的是通过第一个参数 sql 中的 order by 保证每个产品名称(prod_name)的属性字段(atrr_name)的顺序一致。当商品的属性不一致时上述语句就会有问题,这也是接下来要描述的双参数的 crosstab 要解决的问题。

两个参数的 crosstab

双参数的 crosstab 与一个参数的 crosstab 作用是相同的,区别是对属性字段的处理上更加灵活。

双参数 crosstab 的第一个参数

双参数 crosstab 的第一个参数与单参数 crosstab 的第一个参数是一样的,也是一个 sql 查询,特别之处是

  1. 在第一列(商品名称(prod_name))后方可以出现可选的一个或多个额外字段列(extra columns);与单参数一样,属性名称(示例是属性名称 atrr_name)和属性值(示例是属性值 attr_val)这两个字段始终要保证在倒数第二和倒数第一的位置上;
  2. order by 仍然要做排序,区别是只需要对第一列商品名称(prod_name)进行排序了。

双参数 crosstab 的第二个参数

它的第二个参数也是一个 sql 语句它的作用是指定需要显示的属性名称的列表,即在这里指定那些你希望在最后的结果出现的属性列,且要满足如下条件:
第二个参数的查询结果只能有一个字段,且不能存在重复行,否则会报错,即你要在第二个参数的 select 子句里预防性地加一个 distinct;
注意顺序,与返回字段配合好,关于顺利这里还有另一个技巧可以让你显示地指定这些字段和它们出现的顺序,下面会介绍。

定义 crosstab 的返回字段和数据类型

要求同单参数,不再赘述。

双参数的 crosstab 示例1

还是以上述的示例表中的数据为例,加入第二个参数,通过第二个参数里的 sql 查询指定属性名称(atrr_name)及其排序顺序。

select
prod_name,
attr_a as "规格",
attr_b as "单价",attr_c as "折后价", attr_d as "保质期", attr_e as "库存数量"
from 
crosstab(
    'select prod_name, attr_name, attr_val
       from prod_attr t1
   order by prod_name', -- 要排序,仅需对第一个字段排序
	 'select DISTINCT attr_name from prod_attr' -- 保证只有一列且不能存在重复行
) as cte(prod_name VARCHAR,
				 attr_a NUMERIC, attr_b NUMERIC, attr_c NUMERIC, attr_d NUMERIC, attr_e NUMERIC)

执行结果如下图:

双参数的 crosstab 示例2(强烈推荐)

通过第二个参数显示指定属性名称字段及其顺序。

select
prod_name, attr_a as "保质期", attr_b as "规格",attr_c as "单价",attr_d as "折后价"
from 
crosstab(
    'select prod_name, attr_name, attr_val
       from prod_attr t1
   order by prod_name', -- 排序仍然要, 仅需对第一个字段排序
	 'VALUES (''保质期''), (''规格''), (''单价''), (''折后价'')' -- 显示指定attr_name值和排序
) as cte(prod_name VARCHAR, attr_a NUMERIC, attr_b NUMERIC, attr_c NUMERIC, attr_d NUMERIC)

执行结果如下图:

参考:http://postgres.cn/docs/12/tablefunc.html

WordPress 使用 wpdb 类创建的全局对象 $wpdb 实现对底层 MySQL 数据库的 DML 操作。如果你想将 WordPress 用作涉及数据库交互的开发框架,那么面临的第一个问题是 $wpdb 不支持事务,没有事务的支持,就无法保证业务的一致性。对于事务的概念这里就不做介绍了,如果你找到这篇文章,那你自然是理解的。

本插件是 wpdb 类的子类,以 wpdb 为基础实现了对事务的支持,做法是在 wpdb 类上扩展出一个子类 conn,在子类 conn 上实现对事务的支持(不受影响 wpdb 的实例 $wpdb)。为方便使用已做成插件,项目地址在这里:https://github.com/yusn/wp_conn下载地址在这里

使用方法

插件已定义了全局对象 $conn,在您的程序里只需要加入global $conn;即可通过 $conn 调用那些从 wpdb 类继承来的方法,以及 $conn 自有的一些方法。换句话说在需要时你完全可以用 $conn 来替代 $wpdb。

特性介绍

这是一个试验性的项目,仅供交流。

  • 仅支持 REST API;
  • 自动开启事务;
  • 异常自动回滚;
  • 运行结束自动提交(返回 REST API 请求时);
  • 支持多行插入;
  • 支持自动提交模式;
  • 支持手动提交;
  • 支持手动回滚;

仅支持 REST API

之所以仅仅支持 REST API,是因为开启事务/自动提交/自动回滚的实现是在 REST API 提供的 filter 上实现的(与 $wpdb 不同,$conn 默认运行在手动提交模式下)。

自动开启事务

接收到 REST API 请求时自动开启事务,无需手动开启。

运行结束自动提交

API 程序运行结束,如未有异常发生,响应返回前将自动提交事务。

异常自动回滚

当 API 程序抛出异常,或返回 WP_ERROR 类实例时,将自动执行事务回滚。
若在程序抛出异常前执行了手动提交 $conn->commit();,则提交之前的 DML 操作不会被回滚;若异常发生在手动开启事务之后,或执行$conn->commit();之后返回 WP_ERROR,则回滚仅限于手动开启事务之后的部分。

支持自动提交模式

也可以像 $wpdb 那样,让你的代码运行在自动提交模式下,只需要在程序中执行执行数据库操作前执行$conn->set_autocommit();即开启自动提交模式,此后若再发生异常或错误,则无法回滚。

支持手动提交

默认情况下(未开启自动提交)在程序执行过程中,你可以在需要时随时手动提交之前的操作,只需要执行$conn->commit();方法。执行$conn->commit();将自动开启一个新的事务,这会让你之后的代码运行在一个新事务里,这个事务在正常响应返回前会自动被提交,若后续代码发生了异常或返回 WP_ERROR 类的实例,则这部分代码会被回滚。
开启自动提交后($conn->set_autocommit();),若再执行手动提交($conn->commit();),则手动提交不会生效(也不会产生新事务),因为当前已经运行在自动提交模式下,无需手动提交。

支持手动回滚

在一个事务里,你可以随时执行$conn->rollback();方法以回滚之前的操作;若在此之前已开启了自动提交模式($conn->set_autocommit();),则回滚操作不会生效。

支持多行插入

$wpdb 的插入方法只支持单行插入,使用$conn->insert_rows();方法能够实现一次插入多行,该方法的返回结果是成功插入的总行数。

$res = $conn->insert_rows(
            'test', // 写入的表名称
            array('name'), // 写入的表字段
            array('string'), // 写入字段的字段类型
            array(
                array('aa'), // 第一行数据
                array('bb'), // 第二行数据
                array('cc'), // 第三行数据
            ),
        );

注意事项

MySQL 的 DDL 操作是无法被回滚的,即 create/drop databases,create/drop/alter table 这些命令无法被回滚,应慎重考虑在事务中包含 DDL 语句,若无法避免应考虑做相应的容错处理。

图一是去年栽的平枝栒子,特点是果子大,心型的叶子入秋后变红。
春季·栒子·开花

这盆栒子今天刚刚开花,初花时间比去年提前了一天。
春季·阳光·栒子·开花

珍珠黄杨,买花得的赠品,买的花没活赠品却活下来了。今年是来我这的第五个年头了,今年第一次开花结果,之前以为它不会开花呢。
春季·阳光·珍珠黄杨

小草不知其名,繁殖能力极强,去年首次栽种就泛滥了,大规模清除后,仅剩此两棵,差点被灭族。
春季·阳光·花盆·小草·苔藓·石头

木香,香味很淡。
春季·木香·花朵·绿叶

WordPress save_post 这个钩子有三个参数分别是:$post_ID$post$update,本篇对第三个参数 $update 总结一下。

$update 的定义

$update 是一个布尔型参数,它要么是 true,要么是 false,WordPress 开发文档对 $update 参数的定义是:

"Whether this is an existing post being updated."

字面上理解理解这句话:是否是对现有日志的更新。

上述定义带有一定的歧义和误导,事实上在日志编辑界面,并不是点更新按钮 $update 就是 true,点保存或发布按钮 $update 就是 false 。 WordPress 是根据编辑界面的标题或内容字段(仅限这两个字段)较上一次保存的版本是否发生变更来确定 $update 其值的:不管点什么按钮,只要标题或内容这两个字段有变更,$update 就是 false,否则,如果未发生变更,不管点什么按钮,点多少次,$update 的值都是 true 。

有趣的是当 $update 为 false 时,会生成修订版本,否则不生成修订版本。因此,推断 $update 这个字段可能是用来判断是否生成修订版本的。

简单一句话总结 $update 参数: 标题或内容无变动时, 点操作按钮(保存草稿/发布/更新)$update 为 true; 反之 $update 为 false 。

save_post 的触发时机

关于 save_post 这个钩子在什么情况下会触发,WordPress 开发文档给的解释如下:

save_post is an action triggered whenever a post or page is created or updated, which could be from an import, post/page edit form, xmlrpc, or post by email

大意是当我们通过编辑界面、 xmlrpc(如 wordpress APP)、 email 、导入工具,创建或更新日志或页面时,就会触发 save_post 钩子。

通过实验总结,以上解释也不是很具体。以发布一篇日志为例,从创建到保存、发布、更新,期间会多次触发 save_post 这个钩子,在此总结一下(可能不全,甚至有误)。

  1. 点 “写文章”,生成 $post_id,并立即触发 save_post 钩子,$update 值是 false
  2. 撰写新文章界面,不填任何内容, 直接点保存草稿/发布,均不触发 save_post, 发布不会成功
  3. 自动保存不触发 save_post
  4. 除以上情况,手动点保存草稿/发布/更新,都将触发 save_post

总结

如果希望借助 save_post 钩子做很细的控制操作,仅仅凭借 $update 一个参数是不够的,可能还需要通过 wp_get_post_parent_id() 借助 $post_ID 判断是否为修订版本,甚至是 save_post 第二个参数 $post 中的 post_datepost_statuspost_modified 来辅助。

wp_get_post_parent_id()会返回当前日志的父 ID,如果当前日志是修订版本,wp_get_post_parent_id() 返回它所附属日志的 post_id(大于 0);否则,返回 0 。下面结合编辑界面的按钮动作将 $update 和 wp_get_post_parent_id() 的对应值做一下总结,简短起见且将 wp_get_post_parent_id() 的返回结果命名为 parent_id

有变更 动作 $update parent_id
/ 新建 false 0
保存 true 0
发布 true 0
更新 true 0
保存 false >0
发布 false >0
更新 false >0

一直想将自己在用的主题分享出来,只因有一些私人信息不好处理,一直作罢。最近一段时间,将主题里相关的配置信息剥离出来,统一放到配置文件中,使得分享得以实现,等于现在我只需要维护一个私有的配置文件即可。现在将主题托管在了 GitHub 上,点这里直接下载。

功能介绍

  • 响应式布局;
  • 自动暗黑模式;
  • 支持 schemas 标记;
  • 支持部分 web app 特性;
  • 日志和评论支持无限滚动加载;
  • 标准(standard)格式支持视频背景;
  • 状态(status)格式支持喜欢按钮、 支持地理位置坐标;
  • 日志、评论自动记录发布者终端设备信息;
  • 上传附件附加随机字符串的安全机制;
  • 相对完善的垃圾评论抑制机制;
  • 丰富多样的参数化配置;
  • 原生 JavaScript 支持;
  • 支持代码高亮。