Solidity中的CRUD — Part 2

摘要:Ethereum是Block Chain的一种实现,而Block Chain在存储方面跟接近于一种分布式账本。每个加入到Ethereum网络中的节点都会有一份COPY。数据是存储在网络中的所有节点中。在这个方面,与传统的关系数据库有着完全不一样的特点。那么在基于Solidity的编程中,也会有着不一样的思路。这篇文章,主要是分享一下编程思路上与传统关系数据库的CRUD(Create, Read, Update, Delete)处理上的区别。

本篇是一个系列:
第一篇谈一谈Ethereum的数据(变量)存储的位置(Data Location),以及几个常用的数据类型
第二篇会谈一谈Create,Read和Update的方法
第三篇会谈一谈如何Delete.

Create & Insert

下面要完成的内容是:

  1. 给Struct里面赋值。
  2. 将Struct的对象保存在mapping中。
  3. 将Key插入到Array中。
    将interface定义成下面的样子:
1
2
3
function insertUser(address userAddress, bytes32 userEmail, uint userAge)
public
returns(bool success) {}

步骤1和2由下面的代码来完成:

1
2
3
4
5
6
7
8
function insertUser(address userAddress, bytes32 userEmail, uint userAge)
public
returns(bool success)
{
userStructs[userAddress].userEmail = userEmail;
userStructs[userAddress].userAge = userAge;
return true;
}

第三步骤,将key插入到Array中:

1
userIndex.push(userAddress);

Read

下一步要演示一下如何在mapping中通过Key重现拿回value.

这个function本身并不返回key。

1
2
3
4
function getUser(address userAddress)
public
constant
returns(bytes32 userEmail, uint userAge) {}

这个function比较简单的展示了返回userEmail和userAge。Sample Code可以比较简单的做如下的实现:

1
2
3
4
5
6
7
8
function getUser(address userAddress)
constant
returns(bytes32 userEmail, uint userAge)
{
return(
userStructs[userAddress].userEmail,
userStructs[userAddress].userAge);
}

Update

下一步,按照输入的Key来更新mapping中的一个value. Interface可以简单的做如下定义:

1
2
3
function updateUserEmail(address userAddress, bytes33 userEmail)
public
returns(bool success) {}

或者update整个entity:

1
2
3
function updateUser(address userAddress, bytes32 userEmail, uint userAge)
public
returns(bool success) {}

如何判断Key是否存在?

在Create, Read, Update的时候,为了确保操作争取,我们第一步总是要验证这个Key是否已经存在,然后才能放心的做下一步操作。例如,在Update的时候,先检查Update的Key是否存在,如果存在,那么就可以进行更新。如果不存在,则可以选择插入数据生成一条新的,或者抛出异常。

在C#中,可以用Hashtable.ContainsKey,这样的method来判断Key是否存在。那么在 Solidity中,如何检查mapping里是否由某个Key已经 存在?Part 1 已经解释过,”Key本身并不会保存在mapping中,而是保存了Key值对应的keccak256 hash值,用来查找对应的value“。所以不可能遍历所有的已知KEY来判断某个KEY是否存在。

Ethereum的整个storage space在初始化的时候都回被置为0。如果想通过判断value是否为0,预判KEY是否存在,也是不可行的。因为从逻辑上说,value为0也有它自身的意义。

为了解决这个问题,就需要在UserStruct中加入一个flag来标识里面的数据是否已经被初始化过。然后通过判断这个flag是否为true,可以知道是否真实的有这个值存在(被赋值过)。

1
2
3
4
5
struct UserStruct {
bytes32 userEmail;
uint userAge;
bool isUser;
}

在创建这个UserStruct的对象的时候,就对这个flag赋值为true. 那么在读取这个value的时候,我们也可以通过这个flag来判断是否已经赋值过,或者是一个default值。

1
2
3
4
5
6
7
function isUser(address userAddress)
public
constant
returns(bool isIndeed)
{
return userStructs[userAddress].isUser;
}

通过这个方法,我们可以给代码逻辑添加更多的logical constraints, 保证代码逻辑的健壮性:

  1. 不能Insert一个已经存在的Key
  2. 不能Update一个不存在的Key
  3. 不能Read一个不存在的Key

这种设计在并不需要DELETE的时候是可以正常工作的。如果需要DELETE的功能,那么需要进行一些修改。我会在下一篇进行这方面的讨论。

用于替代这个flag ,可以加入一个Pointer。这个Pointer的作用,不仅仅可以确定是否已经做过初始化,同时也可以标识key所在的位置。命名为userIndex。代码如下:

1
2
3
4
5
struct UserStruct {
bytes32 userEmail;
uint userAge;
uint index; // == this
}

Pointer的作用

为了达到目的,在insert的时候要增加一些步骤。在UserStruct中增加的index pointer,在将UserStruct的对象插入到mapping的时候,同时将key所保存在Array的位置也一同赋值给UserStruct的index。总是将index赋值为userIndex.length。

1
2
userStructs[newKey].index = userIndex.length;
userIndex.push(newKey);

这种方式,Index的起始值为0,标识为Array的第一个位置。当userIndex –Array– , 压入一个新的KEY的时候,同时也将userIndex.Lenght – 1, 就是这个KEY所在的位置。明白了这个道理,为了减少gas的开销,上面的代码也可以优化为下面这种写法:

1
userStructs[newKey].index = userIndex.push(userAddress)-1;

参照的完整性

这个设计下,我们有了两个结构来存储数据。Array (userIndex) 和 Mapping (userStructs)。Array 用来作为KEY的索引,保存了我们所有已知的KEYs。Mapping用来存放UserStruct的记录。UserStruct对象的index,用来记录userIndex中KEY所存放的位置,从而能够取回KEY的内容。

然而这种设计存在一个重大的问题。一旦索引与存储的记录不同步,我们将面临严重的问题,可能会造成严重的BUG。Solidity本身特别擅长确保数据的完整行,代码执行也不会再这方面出错。所有我们要专注的在自己的代码逻辑方面的设计。

  1. 映射结构与索引之间存在双向绑定。
  2. 不允许任何操作违反规则#1。业务逻辑必须是原子性的。 必须以”全部”的方式执行。 没有完全成功的操作要进行rollback。

同时建议将索引和映射设置为private,以确不会被child contracts意外的overwrite.

1
2
mapping (address => UserStruct) private userStructs;
address[] private userIndex;

检查KEY是否存在

如果说给定了一个KEY值(userAddress), 怎么来判断mapping中,它是否存在。为了要验证这个KEY是否存在,我们可以采用下面的步骤:

  1. 先从mapping中通过KEY拿到这个structure的对象,从里面拿到index pointer。
    注意,这个时候,无论KEY是否真的存在,都会获得返回值。如果是真事存在的KEY,那么会返回有意义的值。如果是不存在的KEY,返回的会是0 (默认值)。但,反过来,如果这个时候读取到的是0,无法判断是否是存在或者不存在,因为原本保存的值可能就是0. 所以需要做下一步的判断。
  2. 通过index的值,在Array中获取保存的KEY值。
  3. 比较从Array中获取的KEY和输入的KEY,是否一致。

下面来举个栗子,比如说在mapping中减少是否有”G”这个KEY的值真实存在:

  1. 首先我们假设mapping不是空的,Array也不是空的,G真的存在。
  2. 在mapping中查找KEY为G的value。这个value的index的值是3.
  3. 在Array中查找userIndex[3]里面保存的内容。这里能够拿到它保存的值是”G”。
  4. 比较一个,我们要查找的KEY和Array里面保存的KEY的内容是否一致。”G” == “G”。可以证明G是存在的。

img

同样还是上面这张图。键入说我们要验证一个不存在的KEY—“X”。

  1. 如果Array是空的,那么这个值肯定不存在。
  2. 在mapping中查找KEY为X的value。由于X实际是不能存在的,这个value的index的值是0, 一个默认值。
  3. 在Array中查找userIndex[0], 这个时候返回的是KEY的内容—”A”。
  4. 比较A和X,不匹配。那么X是不存在的。

下面是示例代码:

1
2
3
4
5
6
7
8
function isUser(address userAddress)
public
constant
returns(bool isIndeed)
{
if(userIndex.length == 0) return false;
return (userIndex[userStructs[userAddress].index] == userAddress);
}

在调用这个function的时候,如果检测到KEY不存在,我们可以抛出一个异常,让应用程序第一时间注意到这个问题:

1
if(!isUser(userAddress) throw; // missing key

Iterable Interface

总共插入了多少条数据在mapping中,这是一个比较常见的需求。Mapping不能获得已经插入了多少条数据,但是Array可以。这是我们设计Array这个结构在这里的目的之一。

1
2
3
4
5
6
7
function getUserAtIndex(uint index)
public
constant
returns(address userAddress)
{
return userIndex[index];
}

对于一个web3 client代码,可以这样调用:

先获得index的大小:UserCrud.getUserCount.call()*。然后,在for循环中,获取KEY值。在调用对应的function,从mapping中获取struct的对象值。

1
2
3
4
for(row=0; row < count; row++) {
UserCrud.getUserAtIndex.call(row)
UserCrud.getUser.call(theAddressFromStep2)
}

Event Emitters

Smart Contract的一个特点就是可以回调contract外部的一些方法。在Smart Contract中嵌入一个event, 当contract内部的state发生变化的时候,调用这个事件。可以让外部的client application得到响应。这样,client app跟on-chain storage的数据进行双向互动了。为了方便理解,可以认为是数据库的数据发生改变的时候,会同时通知到client app。Client app可以同步更新本地的cache,而不需要时时刻刻去请求数据库。

为了实现这个目的,我们可以进行如下的实现:

  1. 定义一个getter,获取当前的状态
  2. 嵌入event 用了来update state changes
1
2
event LogNewUser (address indexed userAddress, uint index, bytes32 userEmail, uint userAge);
event LogUpdateUser(address indexed userAddress, uint index, bytes32 userEmail, uint userAge);

跟C#/JAVA的event一样。先是定义某种意义的事件。在状态发生相应变化的时候,回调这个事件。那么注册了这些事件的client application就会得到事件相应。例如,这里声明了一个LogNewUser, 在插入新用户的时候会被触发。LogUpdateUser在用户信息被更新的时候会触发。

DELETE

这篇的内容只是正对没有DELETE的情况下来进行讨论。userStructs 和userIndex的设计,在没有delete的时候可以很好的工作。但是一旦发生了DELETE动作,userStructs和记录的UserStrct对象的index就必须做相应的改变才能保持userStructs和userIndex中记录保持一致性。

对于这一点,将会在Part 3中进行讨论.

SAMPLE CODE :

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
pragma solidity ^0.4.6;
contract UserCrud {

struct UserStruct {
bytes32 userEmail;
uint userAge;
uint index;
}

mapping(address => UserStruct) private userStructs;
address[] private userIndex;
event LogNewUser (address indexed userAddress, uint index, bytes32 userEmail, uint userAge);
event LogUpdateUser(address indexed userAddress, uint index, bytes32 userEmail, uint userAge);

function isUser(address userAddress)
public
constant
returns(bool isIndeed)
{
if(userIndex.length == 0) return false;
return (userIndex[userStructs[userAddress].index] == userAddress);
}

function insertUser(address userAddress, bytes32 userEmail, uint userAge)
public
returns(uint index)
{
if(isUser(userAddress)) throw;
userStructs[userAddress].userEmail = userEmail;
userStructs[userAddress].userAge = userAge;
userStructs[userAddress].index = userIndex.push(userAddress)-1;
LogNewUser(
userAddress,
userStructs[userAddress].index,
userEmail,
userAge);
return userIndex.length-1;
}

function getUser(address userAddress)
public
constant
returns(bytes32 userEmail, uint userAge, uint index)
{
if(!isUser(userAddress)) throw;
return(
userStructs[userAddress].userEmail,
userStructs[userAddress].userAge,
userStructs[userAddress].index);
}

function updateUserEmail(address userAddress, bytes32 userEmail)
public
returns(bool success)
{
if(!isUser(userAddress)) throw;
userStructs[userAddress].userEmail = userEmail;
LogUpdateUser(
userAddress,
userStructs[userAddress].index,
userEmail,
userStructs[userAddress].userAge);
return true;
}

function updateUserAge(address userAddress, uint userAge)
public
returns(bool success)
{
if(!isUser(userAddress)) throw;
userStructs[userAddress].userAge = userAge;
LogUpdateUser(
userAddress,
userStructs[userAddress].index,
userStructs[userAddress].userEmail,
userAge);
return true;
}

function getUserCount()
public
constant
returns(uint count)
{
return userIndex.length;
}

function getUserAtIndex(uint index)
public
constant
returns(address userAddress)
{
return userIndex[index];
}
}

Sonic Guo