postgresql's common table expressions

WITH语句指定临时命名的结果集,这些结果集称为公用表表达式 (CTE)。 该表达式源自简单查询,并且在单条 SELECT、INSERT、UPDATE 或 DELETE 语句的执行范围内定义。WITH语句通常被用来作为一次查询的临时表而存在,通过使用WITH关键字能够创建辅助的查询语句,对于复杂的查询语句,能够极大的简化逻辑,使得SQL语句更加便于阅读。

CTE’s在查询语句中总是被物化的

CTE只要被引用一次就会被物化,以后再引用的话就不会重复进行额外的计算。如果一个CTE节点没有被任何地方引用,那么他将不会执行,下面的语句将会正常的输出而不会报错:

WITH not_executed AS (SELECT 1/0),
    executed AS (SELECT 1)
SELECT * FROM executed;

但是这条查询会给出一个ERROR: division by zero的错误。

WITH a AS (SELECT 1/0),
b AS (SELECT * FROM a),
executed AS (SELECT 1)
SELECT * FROM executed;

目前看来,使用了order by的CTE总是输出排序后的记录,但是不能完全依赖这种特性,可能再未来的某个时候,这项特性会被改掉。而且,如果在外部查询也使用了order by,那么postgresql可能不能正确的排序。

WITH a AS (SELECT x, y FROM big_table ORDER BY x)
SELECT *
FROM a ;

CTE没有自动的谓词下推(prediction pushdown)

优化关系SQL查询的一项基本技术是,将外层查询块的WHERE子句中的谓词移入所包含的较低层查询块(例如视图),从而能够提早进行数据过滤以及有可能更好地利用索引。只要新的查询和旧的查询在逻辑上是等价的,那么最终会返回所有相关的结果,相比较旧的查询,优化后的查询更快。但是在postgresql中,并没有采用这种优化方法,也没有区分只读还是可写的CTE,它再指定执行计划时采取了一种比较保守的做法。由于采用了相对保守的方式,优化器不会将WHERE语句提升到CTE子句中,即使这种做法看起来是安全的。

我们可以等待postgresql团队优化CTE,目前我们能够做的只能改变我们进行sql查询的方式。

1. 在查询语句中,硬编码谓词下推后得到的sql

原始的查询语句:

CREATE OR REPLACE FUNCTION suggest_movies(IN query text, IN result_limit integer DEFAULT 5)
  RETURNS TABLE(movie_id integer, title text) AS
$BODY$
WITH suggestions AS (

  SELECT
    actors.name AS entity_term,
    movies.movie_id AS suggestion_id,
    movies.title AS suggestion_title,
    1 AS rank
  FROM actors
  INNER JOIN movies_actors ON (actors.actor_id = movies_actors.actor_id)
  INNER JOIN movies ON (movies.movie_id = movies_actors.movie_id)

  UNION ALL

  SELECT
    searches.title AS entity_term,
    suggestions.movie_id AS suggestion_id,
    suggestions.title AS suggestion_title,
    RANK() OVER (PARTITION BY searches.movie_id ORDER BY cube_distance(searches.genre, suggestions.genre)) AS rank
  FROM movies AS searches
  INNER JOIN movies AS suggestions ON
    (searches.movie_id <> suggestions.movie_id) AND
    (cube_enlarge(searches.genre, 2, 18) @> suggestions.genre)
)
SELECT suggestion_id, suggestion_title
FROM suggestions
WHERE entity_term = query
ORDER BY rank, suggestion_id
LIMIT result_limit;
$BODY$
LANGUAGE sql;

将查询过滤的条件移到CTE语句内部:

CREATE OR REPLACE FUNCTION suggest_movies(IN query text, IN result_limit integer DEFAULT 5)
  RETURNS TABLE(movie_id integer, title text) AS
$BODY$
WITH suggestions AS (

  SELECT
    actors.name AS entity_term,
    movies.movie_id AS suggestion_id,
    movies.title AS suggestion_title,
    1 AS rank
  FROM actors
  INNER JOIN movies_actors ON (actors.actor_id = movies_actors.actor_id)
  INNER JOIN movies ON (movies.movie_id = movies_actors.movie_id)
  WHERE actors.name = query

  UNION ALL

  SELECT
    searches.title AS entity_term,
    suggestions.movie_id AS suggestion_id,
    suggestions.title AS suggestion_title,
    RANK() OVER (PARTITION BY searches.movie_id ORDER BY cube_distance(searches.genre, suggestions.genre)) AS rank
  FROM movies AS searches
  INNER JOIN movies AS suggestions ON
    (searches.movie_id <> suggestions.movie_id) AND
    (cube_enlarge(searches.genre, 2, 18) @> suggestions.genre)
  WHERE searches.title = query
)
SELECT suggestion_id, suggestion_title
FROM suggestions
ORDER BY rank, suggestion_id
LIMIT result_limit;
$BODY$
LANGUAGE sql;

第一个查询语句会通过两次查询将数据库中相关的表的数据全部查询出来然后组合成一张表,然后再进行过滤。可以看到通过这种方式,数据库返回了很多不必要的数据,更糟糕的是组合的新表由于无法利用索引导致查询的效率更低下。优化后的语句由于将过滤的条件移动到了CTE中,所以会提早的进行过滤处理,从而提高查询的效率。

2. 使用子查询

postgresql能够很好的针对子查询语句进行谓词下推的优化,通过这一特性,我们能够编写出效率一样而且更加的易于维护的查询语句。
首先准备需要的表结构和数据:

CREATE TABLE a (c INT);

CREATE TABLE b (c INT);

CREATE INDEX a_c ON a(c);

CREATE INDEX b_c ON b(c);

INSERT INTO a SELECT 1 FROM generate_series(1, 1000000);

INSERT INTO b SELECT 2 FROM a;

INSERT INTO a SELECT 3;

使用CTE进行查询:

EXPLAIN ANALYZE
WITH cte AS (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
)
SELECT c FROM cte WHERE c = 3;

查询计划如下:

  QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 CTE Scan on cte (cost=28850.00..73850.00 rows=10000 width=4) (actual time=607.968..1138.062 rows=1 loops=1)
  Filter: (c = 3)
  Rows Removed by Filter: 2000000
  CTE cte
  -> Append (cost=0.00..28850.00 rows=2000000 width=4) (actual time=0.013..579.749 rows=2000001 loops=1)
  -> Seq Scan on a (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.012..142.085 rows=1000001 loops=1)
  -> Seq Scan on b (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.007..122.516 rows=1000000 loops=1)
 Planning time: 0.284 ms
 Execution time: 1144.234 ms
(9 rows)

使用子查询:

EXPLAIN ANALYZE
SELECT c
FROM (
  SELECT c FROM a
  UNION ALL
  SELECT c FROM b
) AS subquery
WHERE c = 3;

查询计划如下:

  QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Append (cost=0.42..8.88 rows=2 width=4) (actual time=0.045..0.055 rows=1 loops=1)
  -> Index Only Scan using a_c on a (cost=0.42..4.44 rows=1 width=4) (actual time=0.044..0.045 rows=1 loops=1)
  Index Cond: (c = 3)
  Heap Fetches: 1
  -> Index Only Scan using b_c on b (cost=0.42..4.44 rows=1 width=4) (actual time=0.008..0.008 rows=0 loops=1)
  Index Cond: (c = 3)
  Heap Fetches: 0
 Planning time: 0.163 ms
 Execution time: 0.097 ms
(9 rows)

参考文献

Ruby 2.4.0 概览

Binding#irb: Start a REPL session like binding.pry

通常大家在调试的时候会选择使用p打印出变量的值或者使用pry,现在,我们又多了一种方法,通过引入binding.irb,可以启动一个类似如pry的REPL
session:

'''
require "irb"

class Test
  def initialize name
      @name = name
  end

  def say
     binding.irb
     puts "hello,#name"
  end
end

Test.new("jack").say

ruby test.rb

#𔓘 Work ruby test.rb

#2.4.0-preview3 :001 > @name
# => "jack"
#2.4.0-preview3 :002 >
'''

Add non-ASCII case conversion to String

字符串增加对非ASCII的操作.

Unify Fixnum and Bignum into Integer

CRuby有两种类型的整数类,分别是Integer和Bignum,现在两者合并,所以在Sequel中,应该使用symbol版本的Bignum.

Float#round with keyword

Float#round现在支持可选的参数,默认的行为是向最近的偶数取整

1
2
3
4
5
6
7
# ruby 2.3
(2.5).round
3

# ruby 2.4
(2.5).round
2

分布式事物:两阶段提交

两阶段提交是指,为了使基于分布式系统架构下的所有节点进行事务提交时保持一致性而设计的一种算法,通常也叫做两阶段提交协议。在分布式系统中,通常一个事务会跨越很多个节点,但是每个节点只能知悉自己的操作是否成功,对于其他节点操作的成功与否无法知晓。所以,为了保持分布式事务提交的ACID特性,需要引入一个组件来统一管理各个节点的操作结果,并最终决定是否要把这些节点的操作结果进行提交。–维基百科

两阶段提交主要包括一个协调者和数个参与者,在第一阶段中,任何一个参与者可以单方面的终止事务,在第一阶段的末期,所有的参与者都不能单方面的终止事务。
具体的步骤如下:

  • 提交请求阶段:也称投票阶段,协调者向所有参与者发起询问,参与者如果已经就绪,就回复”ready”,否则就回复”not ready”。后一种情况多发生在并发事务有冲突或者请求超时上。这个阶段,各个参与者并不会真正的提交,而是会将提交的操作写入日志。
  • 提交阶段:
    • 当所有的参与者都回复”ready”,并且协调者收到消息后,将会对所有的参与者发送”commit”指令,所有的参与者执行事务,并回送”commit ACK”给协调者,协调者收到后,结束事务。
    • 当有任何一个参与者回复”not ready”,并且协调者收到消息后,将会对所有的参与者发送”abort commit”指令,参与者终止事务并回送”abort ACK”,协调者收到后,结束事务。

缺陷

  • 对节点的容错处理非常脆弱,尤其是协调者出现了错误。
  • 会产生阻塞的情形。虽然2PC追求一致性,但是并不完美。如果锁没有被释放,将会导致数据不可以被访问。
  • 有一个不确定期,无法准确的确定参与者在那一个时间段能够单方面的做决定。
  • 耗时,耗费很多时间在投票表决阶段。

使用2PC保证正对所有资源的更新要么都操作要么都不操作,同时也带来了额外的开销,协调操作会对可扩展性,性能以及延迟造成不利影响,当增加依赖的资源和用户基数变大时,这种不利影响会呈几何级扩大。可用性也依赖所有依赖的资源的可用性。

参考文献

使用sequel需要注意的地方

使用sequel有一段时间了,愈发的觉得这是一个优秀的框架。但是作为一个从ActiveRecord资深使用者切换过来的开发人员,还是多少带有一些习惯性思维在里面,因而容易不恰当的使用一些Sequel的特性。这篇文章的主要目的是记录一些需要注意的地方。

调用first时,采用不同的方法返回的结果的类型并不完全一致

1
2
3
Post.where(:title => 'Hello world').first.class #=> Post
Post.eager(:author).first.class #=> Post
Post.eager_graph(:author).first.class #=> Hash

没有pluck方法,可用select_map

1
Post.select_map([:id, :title])

没有attribute_names方法

1
album.keys.map{|x| x.to_s}.sort

打印sql语句

1
items.where{price * 2 < 50}.sql

关闭数据库连接

数据库连接,本质上,就是一个长期的 TCP 连接。通过系统的命令行,可以观察连接。
比如在 Linux 使用 ss -anp | grep [端口号],可以查看指定端口的 TCP 连接数量。Mac 下面的命令是:lsof -i -n -P | grep TCP | grep [端口号]。

对于数据库来说,维护一个应用的 TCP 连接,是需要消耗固定内存,Postgresql 中维护一个连接,耗费的内存是 2 MB。
在数据库中,可以通过 SQL 语句,查询连接的状态,以及上一条执行的 SQL 语句,在指定数据库中执行:SELECT * FROM pg_stat_activity;

对于数据库,维护大量的数据库连接,耗费的内存高。

Sequel可以根据设置的时间,关闭数据库连接。在设置数据库连接后设置:

1
2
DB.extension(:connection_expiration)
DB.pool.connection_expiration_timeout = 10

Read-Only Slaves/Writable Master

需要注意的是事物和存储过程均不能运行在slave机器上,若需要使用,则需要指定master机器。如果想指定某一条查询使用特定的数据库择可以采用这种方式:

1
DB[:users].server(:default).all

数据库使用单个主库单个丛库的配置如下:

1
DB=Sequel.connect('postgres://master_server/database',    :servers=>{:read_only=>{:host=>'slave_server'}})

这条配置会让select查询语句运行在slave机器上,其它的查询运行在master机器上。

Transaction

由于sequel的设计理念,即使是在transaction块中,select语句和非select语句会在不同的shard中执行。要保证transaction块中的所有语句都在同一个数据库中执行则需要显示的指定server。
或者更方便的复写DB.transaction, 主要是利用server_block这个extendion。

1
2
3
4
5
6
DB.extensions :server_block
DB.transaction do
DB.with_server(:default) do
-----
end
end

参考文献

TCP复位攻击

虽然TCP/IP是一个可靠的面向连接的协议,但是仍然有许多的漏洞。本文将要讲解其中的一种: rst复位攻击, 在这之前让我们先回顾一下TCP的基础知识。

TCP概览

TCP数据报被封装在一个IP数据报中,如图[1]所示。
Alt "TCP包首部"

图[2]展示的是TCP首部的数据格式,如果不计任选字段,它将会是20个字节。
Alt "TCP数据在IP数据报中的封装"

由图[2]可以清楚的看到TCP数据报的头部包含一个16位的源端口地址和一个16位的目的端口地址,用于寻找发送端和接收端的应用程序。这两个端口号和IP首部中的发送端IP地址和接收端IP地址结合起来就能够唯一确定一个TCP连接。

TCP连接的建立

接下来讲解TCP是如何建立连接的,也就是通常所说的三次握手的过程。为了简便起见,我直接使用rails new blog创建一个app,然后运行rails s启动服务,然后本机运行telnet localhost 3000。再开一个窗口运行sudo tcpdump -i lo0 port 3000,得到如下输出。

16:44:23.751241 IP6 localhost.59405 > localhost.hbci: Flags [S], seq 396289691, win 65535, options [mss 16324,nop,wscale 5,nop,nop,TS val 896927289 ecr 0,sackOK,eol], length 0
16:44:23.751315 IP6 localhost.hbci > localhost.59405: Flags [S.], seq 588037038, ack 396289692, win 65535, options [mss 16324,nop,wscale 5,nop,nop,TS val 896927289 ecr 896927289,sackOK,eol], length 0
16:44:23.751331 IP6 localhost.59405 > localhost.hbci: Flags [.], ack 1, win 12743, options [nop,nop,TS val 896927289 ecr 896927289], length 0
16:44:23.751345 IP6 localhost.hbci > localhost.59405: Flags [.], ack 1, win 12743, options [nop,nop,TS val 896927289 ecr 896927289], length 0
16:44:53.758011 IP6 localhost.hbci > localhost.59405: Flags [F.], seq 1, ack 1, win 12743, options [nop,nop,TS val 896957275 ecr 896927289], length 0
16:44:53.758085 IP6 localhost.59405 > localhost.hbci: Flags [.], ack 2, win 12743, options [nop,nop,TS val 896957275 ecr 896957275], length 0
16:44:53.758101 IP6 localhost.hbci > localhost.59405: Flags [.], ack 1, win 12743, options [nop,nop,TS val 896957275 ecr 896957275], length 0
16:44:53.758188 IP6 localhost.59405 > localhost.hbci: Flags [F.], seq 1, ack 2, win 12743, options [nop,nop,TS val 896957275 ecr 896957275], length 0
16:44:53.758260 IP6 localhost.hbci > localhost.59405: Flags [.], ack 2, win 12743, options [nop,nop,TS val 896957275 ecr 896957275], length 0

查看第三条可看出客户端的确认序号与描述不符,查阅资料发现正确的telnet命令应该是’sudo tcpdump -i lo0 port 3000 -s’。

the ack sequence number is a small integer (1). The first time tcpdump sees a tcp ‘conversation’, it prints the sequence number from the packet. On subsequent packets of the conversation, the difference between the current packet’s sequence number and this initial sequence number is printed. This means that sequence numbers after the first can be interpreted as relative byte positions in the conversation’s data stream (with the first data byte each direction being ‘1’). ‘-S’ will override this feature, causing the original sequence numbers to be output.

改用浏览器直接请求,为避免不必要的干扰仅截取一部分进行说明:

17:31:09.583497 IP6 localhost.59700 > localhost.hbci: Flags [S], seq 608092820, win 65535, options [mss 16324,nop,wscale 5,nop,nop,TS val 899727695 ecr 0,sackOK,eol], length 0
17:31:09.583645 IP6 localhost.hbci > localhost.59700: Flags [S.], seq 2325301428, ack 608092821, win 65535, options [mss 16324,nop,wscale 5,nop,nop,TS val 899727695 ecr 899727695,sackOK,eol], length 0
17:31:09.583673 IP6 localhost.59700 > localhost.hbci: Flags [.], ack 2325301429, win 12743, options [nop,nop,TS val 899727695 ecr 899727695], length 0

现在让我们来逐条分析以上的输出来看一下为了建立一条TCP连接要经过那些步骤:

  • 客户端首先发送一个SYN段表明其打算连接的服务端的端口以及初始序号ISN(seq 608092820), 此为报文段1。
  • 服务器发回包含服务器的初始序号的SYN(seq 2325301428)(报文段2), 同时设置确认序号客户端初始序号ISN + 1, ACK = ISN + 1(ack 608092821), 对客户端发送的SYN进行确认。
  • 客户端设置确认序号为服务端初始序号ISN + 1, ACK = ISN + 1(ack 2325301429), 对服务端的SYN报文段进行确认。

RST复位标志

TCP首部中的RST比特是用于”复位”的,发送RST包关闭连接时,不必等缓冲区的包都发出去,直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

TCP Reset Attacks

  • 首先攻击者需要劫持TCP session。
  • 攻击者发送RST标志位置1的包到主机A和主机B,或者二者之一。
  • 因为主机A和主机B并不知道这些包是由攻击者发出的,所以A,B正常的处理这些包。
  • 因为这些包包含RST为1的标志位,所以A和B之间的连接就关闭了。

下面我们来讲一下攻击者如何劫持TCP session。

假设主机A和主机B之间已经建立了TCP连接,那么一个攻击者能够监控主机A和主机B之间的数据包, 那么劫持TCP session就可以采用如下步骤:

  • 攻击者使用Dos攻击主机B,中断主机B和主机A之间的通信。
  • 现在攻击者就能够预测主机A期望主机B发送的包所包含的序列号。
  • 攻击者准备一个这样的包发送给主机A。
  • 主机A不知道这个包是假冒的,仍然认为该包来自主机B。
  • 攻击者可以利用这个包做出各种神奇的攻击。

参考文献

字符串与指针

字符串常量是一个字符数组,在字符串的内部表示中,字符数组以空字符\0结尾。所以程序可以通过检查空字符找到字符数组的结尾,因此字符串常量存储空间比双引号内的字符数大1。
字符串常量和字符数组的区别仅仅在于最后有没有\0。当我们访问字符串时,实际上是通过字符指针进行访问的。
Code:

#include <iostream>
using namespace std;
int
main()
{
  char c[6] = {'h', 'e', 'l', 'l', 'o', '\0'}
  char *pc = c;
  cout<<c<<endl;
  cout<<pc<<endl;
  return 0;
}

这里的cout<<c<<endl; cout<<pc<<endl实际上是打印出整个字符串数组,而不是该字符串数组的地址,原因是cout对字符串的输出做了特殊的处理,如果我们要打印出该字符串数组的首地址,应该采用cout<<static_cast<void*>(c)<<endl; cout<<static_cast<void*>(pc)<<endl

# 字符串指针举例;

Code:
#include <iostream>
using namespace std;
int
main()
{
  char buffer[10] = "ABC";
  char *pc;
  pc = "hello";
  cout<<pc<<endl;
  pc++;
  cout<<pc<<endl;
  cout<<*pc<<endl;
  pc = buffer;
  cout<<pc;
  return 0
}

我们可以直接把字符串赋给指针变量pc="hello",但是不能把字符串赋值给字符数组,除了在初始化的时候。在这里,因为字符串是一个常量,所以我们只能读取该字符串的值,但是不能改变它的值。在程序运行时,所有的常量都是放在一块特殊的内存区域中,这块区域是不允许修改的。在进行pc++后,指针指向下一个位置,因此pc++;cout<<pc<<endl打印出ello, cout<<*pc<<endl打印出e

指向二维数组的指针

首先我们通过一道例题来认识一下指向二维数组的指针。
Code:

#include <iostream>
using namespace std;
int
main()
{
  int a[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23};
  int *p;
  for(p=&a[0][0]; p<&a[0][0]+12;p++)
  {
    cout<<p<<" "<<*p<<endl;
  }
  return 0;
}

我们把二维数组存储在内存里面的时候,实际上是把二维数组拉成了一条直线存储在内存里面的。数组里面的每个元素都是连续的存储在内存中的,二维数组也一样,所以我们通过对指针进行++运算的时候,指针会指向下一个元素。

# 输入i,j;输出a[i][j];

Code:
#include <iostream>
using namespace std;
int
main()
{
  int a[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23};
  int (*p)[4],i,j;
  p = a;
  cin>>i>>j;
  cout<<setw(4)<<*(*(p+i)+j);
  return 0;
}

首先定义一个二维数组a[3][4],我们从p=a开始,a相当于指向a[3][4]的第一个元素的指针;所谓的第一个元素是指一个包涵4个int型元素的一维数组;所以a相当于一个包涵4个int型元素的一维数组的地址;如果要定义p的话,p的基类型就是包涵4个int型类型的一维数组。所以我们要定义一个包涵4个int型类型的一维数组的指针变量int (*p)[4]

# 利用指针变量引用多维数组中的数组

  • *(*(p+i)+j)
    p指向一个包涵4个int型元素的一维数组,p+i是第i+1个包涵4个int型元素的一维数组的地址,所以p+i等价于&a[i]。那么很明显的*(p+i)等价于a[i]*(p+i)+j等价于a[i]+j,因为a[i]+j等价于&a[i][j],所以*(*(p+i)+j)等价于a[i][j]

# 指针变量的++操作

Code:
#include <iostream>
using namespace std;
int
main()
{
  int a[4] = {1,3,5,7}:
  cout<<a<<endl;
  cout<<a+1<<endl;
  cout<<&a<<endl;
  cout<<&a+1<<endl;
  cout<<*(&a)<<endl;
  cout<<*(&a)+1<<endl;
  return 0;
}

对于上面的程序,cout<<a<<endl打印出数组的首地址,+1操作后,打印出首地址的值加4。当我们对一个指向整型的指针变量进行++操作的时候,指针变量的值会增加4,也就是指针变量的基类型的大小。但是当我们对a进行&操作的时候,会将a的管辖范围提升至整个数组,并返回一个指针,所以现在该指针变量的基类型是整个数组,我们再进行+1操作的时候,该指针变量的值会跨越整个数组至下一个地址。

# 总结

  • 数组名相当于指向数组的第一个元素的指针
  • &a是指向整个数组的指针,相当于管辖范围上升了一级
  • *a是数组的第一个元素a[0],即’*a’相当于a[0],相当于管辖范围降了一级
  • 假定有二维数组a[3][4],二维数组名相当于指向首元素的指针,二维数组的第一个元素是a[0],a[0]是一个包涵四个整型元素的一维数组。

理解JavaScript中的原型继承

原型是JavaScript的核心概念之一,理解JavaScript的原型就显得尤为重要。在JavaScript中,类的所有对象都从同一个原型对象上继承属性,因此,原型对象是类的核心。

除了null对象之外,每一个JavaScript对象都和另一个对象关联,”另一个”对象就是原型。
根据创建对象的方式的不同,对象的原型也不同。举例来讲,通过对象直接创建的对象拥有同一个原型,可以通过Object.prototype来获得对原型对象的引用。通过关键字new和构造函数调用创建的对象就是构造函数的prototype的值。

这里需要指出的一点就是并不是每一个JavaScript对象都有原型,数量虽然不多,但是确实存在,Object.prototype就是其中的一个。

下面让我们用实际的例子来看一下,原型继承到底是如何工作的

Code:
  function Parent(name, job) {
    this.name = name;
    this.job  = job;
  }

  Parent.prototype.getName = function() {
    return this.name;
  }

  Parent.prototype.getJob = function() {
    return this.job;
  }

  var parent = new Parent("Jack", "developer");
  console.log(parent.getName()); //Jack
  console.log(parent.getJob()); //developer

  function Child(name, job) {
    this.name = name;
    this.job  = job;
  }
  Child.prototype = new Parent();

  var child = new Child("Jones", "manager");
  console.log(child.getName());//Jones
  console.log(child.getJob());//manager

基本的原型继承是相当简单的,但是这也有一个明显的问题,就是会重复构造函数中的初始化操作,这显然不是我们想看到的。
为了简化操作,我们可以在子类的构造函数上做文章。

Code:
function Child(name. job) {
  Parent.apply(this, arguments);
}

利用create函数

这是来自JavaScript大师Douglas Crockford的方法,具体做法是给Object对象增加一个create方法,创建一个使用原对象作为原型的新对象。

Code:
Object.create = function(o) {
  var F = function () {};
  F.prototype = o;
  return new F();
}

原型关系是一种动态的关系,如果我们给一个原型添加新的属性,那么新创建的属性会对所有的基于该原型创建的对象可见。

使用JSSDK实现微信支付

最近开发微信支付功能时,遇到很多坑,很是一番折腾,本文将记录那些踩过的坑和一些心得。

在文章的开头讲解一些在微信公众平台的配置,这是成功实现微信支付的前提。
在微信的支付授权目录页面的提示说只要精确到二级或者三级目录就可以了,但是实际上是不对的。
翻阅支付开发文档时就会发现,支付的授权目录要和调起支付的页面精确匹配,文档和表单提示的差异实在让人困惑,
这一点不得不让人吐槽。而我在实际开发中发现,要同时配置精确支付目录和精确支付目录的上一级目录才能正确的调起支付。

另外一个需要注意的配置项是在实现签名时要传入的参数,包括appidkeymch_id。其中app_id为接受支付的公众号的id,
‘key’为商户平台的key, mch_id为商户平台的id,一个常见的错误是误将app_secret作为key传入。

为了开发的方便,选择已有的轮子weixin_authorizewx_pay,关于这两个gem的使用可以参阅项目文档,比较详细了。

这里主要讲一下可能会踩的坑。
在生成prepay_id的时候,需要传入notify_url,也就是支付成功后的回调地址。notify_url是不能带有参数的,因为微信会
在通知支付结果时将notify_url的传入参数给截掉。比如传入的是a.html?id=3,最后变成了a.html。如果需要传递参数可以在参数
放在out_trade_no中,我在开发的时候将多个不同场景下的支付都交给同一个action处理,支付回调也是。这个时候就需要区分到底是
来自哪一个场景下的支付,解决方案是在out_trade_no中加一个前缀来进行区分。

另外一个不太常见的场景就是多个公众号公用同一个支付号的场景。这个时候就要获取用户在支付号下的open_id,这里可以选择通过网页授权的形式
获取open_id。这里也有一个回调地址,同样的,微信在返回的时候也会截取掉redirect_uri后面的参数,如果你传入多个参数,返回时只剩下一个。
所以如果你在支付的页面需要传递多个参数的话,可以将open_id参数的获取提前到一个不需要传递参数的页面进行。在多公众号公用同一个支付号的场景下,所有的参数如app_idapp_secretmch_idkey都必须是支付号。

理解Rails中的try方法

随着ruby-2.3.0-preview1的发布,ruby语言原生的支持安全运算子。那么之前为了解决nil问题创建出来的诸如NullObjecttry是不是就没有用了呢?答案是否定的。

Rails中,经常会有很多的关联关系,这样就会用到链式方法的调用,其中一个不可忽视的问题就是对nil问题的处理。我们来看下面的一个例子,假设我们有一个product,我们要查询该product所属公司的名称,为了防止nil的错误,可能会采用如下写法:

product.try(:company).try(:name)

那么使用安全运算符的方式product&.company&.name

我们来看一下其它的例子:

Ruby Code:
require "rails/all"
class Name
  def say
    p "hello"
  end

  def talk word
    p word
  end
end

分别采用安全运算子和try方法进行调用的例子:

Ruby Code:
name = Name.new
name.try(:say) #"hello"
name.try(:talk, "hello") #"hello"
name.try(:cry) #nil
name&.say #"hello"
name&.cry #NoMethodError: undefined method 'cry' for #<Name:0x000000018ada08>
nil&.cry #nil
#带参数的方法没法调用

通过以上例子可以发现,当调用者不为nil,而又不能响应被调用的方法时,会抛出NoMethodError的异常。

那么为什么try就能够处理这种情形呢?

Ruby Code:
class Object
def try(a, &b)
try!(
a, &b) if a.empty? || respond_to?(a.first)
end

def try!(*a, &b)
  if a.empty? && block_given?
    if b.arity.zero?
      instance_eval(&b)
    else
      yield self
    end
  else
    public_send(*a, &b)
  end
end

end

阅读源码可以发现,Rails扩展了Object对象,提供了try方法,可以接受多个参数和一个代码块。
参数为空或者调用者能够响应第一个参数时,就交给try!方法处理。

  1. try方法首先判断参数为空和给定了代码块的情况
    • 如果代码块不带参数,则直接执行代码块
    • 如果代码块带参数则把调用者作为参数传递给代码块
  2. 否则直接交给public_send方法处理,把第一个参数作为被调用的方法,其余的参数作为被调用方法的参数

Rails同时扩展了NilClass,调用try方法直接返回nil