【区块链安全 | 第十九篇】类型之映射类型

在这里插入图片描述

映射类型

映射类型的语法为 mapping(KeyType KeyName? => ValueType ValueName?),变量声明形式为 mapping(KeyType KeyName? => ValueType ValueName?) VariableName。

其中:

  • KeyType 可以是任意内置值类型、bytes、string,或任何合约类型或枚举类型。用户自定义的复杂类型(如映射、结构体或数组)不能作为键。

  • ValueType 可以是任意类型,包括映射、数组和结构体。

  • KeyName 和 ValueName 是可选的,只用于标识 getter 函数的输入输出名,不能是类型名称。

我们可以将映射视为哈希表:每个可能的键在内部自动初始化,并映射到该值类型的默认值(例如,uint 类型默认值为 0)。不过与典型哈希表不同,映射本身并不存储键,只使用键的 keccak256 哈希值来查找对应的值。

因此:

  • 映射无法获取长度,也无法检查键是否存在。

  • 不具备键的可遍历性,除非通过额外数据结构实现。

  • 映射只能存在于 storage 中,不能用作 memory 变量,也不能作为函数参数或返回值(包括嵌套在数组或结构体中时)。

我们可以将映射类型的状态变量声明为 public,Solidity 会为其自动生成 getter 函数。若 ValueType 是结构体或值类型,getter 会返回完整的值;若 ValueType 是数组或映射,getter 会为每一层的 KeyType 添加一个参数,并递归生成 getter。

示例如下:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

// 定义一个包含地址和余额的映射的合约
contract MappingExample {
    // 声明一个公共的映射,address => uint,记录每个地址的余额
    mapping(address => uint) public balances;

    // 更新函数:允许发送者更新他们的余额
    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;  // 将调用者的余额更新为 newBalance
    }
}

// 定义一个合约用于与 MappingExample 合约进行交互
contract MappingUser {
    // 一个函数,用来调用 MappingExample 合约的 update 方法,并返回当前合约地址的余额
    function f() public returns (uint) {
        // 创建 MappingExample 合约的实例
        MappingExample m = new MappingExample();
        
        // 调用 update 函数,将余额设置为 100
        m.update(100);
        
        // 返回当前合约地址的余额
        return m.balances(address(this));  // 返回 MappingExample 合约中当前合约地址的余额
    }
}

下面的示例是一个简化版的 ERC20 代币。_allowances 是一个嵌套映射类型。我们为映射提供了可选的 KeyName 和 ValueName,这不会影响合约功能或字节码,仅用于在 ABI 中为 getter 的输入和输出命名。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.18;

// 定义一个包含映射的智能合约
contract MappingExampleWithNames {
    
    // 定义一个 public 映射,映射地址(address)到余额(uint)。映射的键为 `user`,值为 `balance`。
    // 这个映射会自动生成一个 getter 函数,可以根据地址(address)查询对应的余额。
    mapping(address user => uint balance) public balances;

    // 更新余额的函数,接受一个 `newBalance` 参数。
    // 这个函数会将调用者(msg.sender)的余额更新为 `newBalance`。
    function update(uint newBalance) public {
        // 使用调用者的地址(msg.sender)作为键,更新其对应的余额值。
        balances[msg.sender] = newBalance;
    }
}

在下例中,_allowances 映射用于记录某个地址被授权从你的账户中提取的金额:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract MappingExample {

    // 定义一个私有映射 `_balances`,将每个地址映射到一个无符号整数(余额)。
    mapping(address => uint256) private _balances;
    
    // 定义一个私有映射 `_allowances`,它是一个二层映射,记录每个地址(owner)允许另一个地址(spender)提取的金额。
    mapping(address => mapping(address => uint256)) private _allowances;

    // 定义事件,当转账发生时触发。
    event Transfer(address indexed from, address indexed to, uint256 value);

    // 定义事件,当批准时触发,表明某个地址被授权从另一个地址提取一定金额。
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // 查询某个地址被授权的提取金额
    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }

    // 从一个账户向另一个账户转账,同时检查授权金额是否足够
    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
        // 确保 sender 允许 msg.sender(调用者)提取足够的金额
        require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");
        
        // 减少授权金额
        _allowances[sender][msg.sender] -= amount;
        
        // 执行转账
        _transfer(sender, recipient, amount);
        
        return true;
    }

    // 批准另一个地址(spender)从调用者的账户中提取指定金额(amount)
    function approve(address spender, uint256 amount) public returns (bool) {
        // 确保 spender 地址不是零地址
        require(spender != address(0), "ERC20: approve to the zero address");

        // 设置授权金额
        _allowances[msg.sender][spender] = amount;
        
        // 触发批准事件
        emit Approval(msg.sender, spender, amount);
        
        return true;
    }

    // 内部转账函数,用于更新账户余额,并触发转账事件
    function _transfer(address sender, address recipient, uint256 amount) internal {
        // 确保 sender 和 recipient 不是零地址
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

        // 确保 sender 有足够的余额
        require(_balances[sender] >= amount, "ERC20: Not enough funds.");

        // 执行转账:从 sender 减去金额,给 recipient 增加金额
        _balances[sender] -= amount;
        _balances[recipient] += amount;
        
        // 触发转账事件
        emit Transfer(sender, recipient, amount);
    }
}

可迭代映射

映射类型无法直接迭代,即无法枚举其键。但可以在其基础上构建一个数据结构,从而实现迭代功能。以下代码实现了一个 IterableMapping 库,User 合约通过该库添加数据,并使用 sum 函数对所有值进行求和。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

// 定义 IndexValue 结构体,用于存储每个键对应的索引和数值
struct IndexValue { 
    uint keyIndex; // 键的索引位置
    uint value;    // 键对应的值
}

// 定义 KeyFlag 结构体,用于标记键是否被删除
struct KeyFlag { 
    uint key;      // 键的值
    bool deleted;  // 是否已删除标志
}

// 定义 itmap 结构体,包含了一个映射和一个存储键的数组,以及当前大小
struct itmap {
    mapping(uint => IndexValue) data; // 存储键值对的映射
    KeyFlag[] keys;                   // 存储键的数组
    uint size;                         // 数据大小
}

// 定义一个类型 Iterator,实质上是 uint 类型,用于遍历
type Iterator is uint;

// 定义 IterableMapping 库,提供操作 itmap 类型数据的函数
library IterableMapping {
    // 插入数据到 itmap 中,如果已存在则更新数据
    function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
        uint keyIndex = self.data[key].keyIndex; // 获取当前键的索引
        self.data[key].value = value;            // 更新该键对应的值
        
        if (keyIndex > 0) {
            return true; // 如果键已经存在,返回 true,表示数据已替换
        } else {
            // 如果键不存在,分配一个新的索引
            keyIndex = self.keys.length;
            self.keys.push(); // 在数组末尾添加一个新元素
            self.data[key].keyIndex = keyIndex + 1; // 设置新键的索引
            self.keys[keyIndex].key = key; // 将键添加到键数组中
            self.size++; // 增加数据大小
            return false; // 返回 false,表示插入了新的键值对
        }
    }

    // 从 itmap 中删除指定的键
    function remove(itmap storage self, uint key) internal returns (bool success) {
        uint keyIndex = self.data[key].keyIndex; // 获取该键的索引
        
        if (keyIndex == 0) {
            return false; // 如果键不存在,返回 false
        }
        
        delete self.data[key]; // 删除数据映射中的键值对
        self.keys[keyIndex - 1].deleted = true; // 将对应的 KeyFlag 标记为已删除
        self.size--; // 减小数据大小
        return true; // 返回 true,表示删除成功
    }

    // 检查 itmap 中是否包含指定的键
    function contains(itmap storage self, uint key) internal view returns (bool) {
        return self.data[key].keyIndex > 0; // 如果该键存在,返回 true
    }

    // 初始化遍历,返回一个迭代器
    function iterateStart(itmap storage self) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, 0); // 跳过已删除的项,返回起始迭代器
    }

    // 检查当前迭代器是否有效
    function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
        return Iterator.unwrap(iterator) < self.keys.length; // 如果迭代器位置小于键数组长度,则有效
    }

    // 获取下一个迭代器,跳过已删除的项
    function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
        return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1); // 跳到下一个有效项
    }

    // 获取当前迭代器对应的键和值
    function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
        uint keyIndex = Iterator.unwrap(iterator); // 获取迭代器的索引
        key = self.keys[keyIndex].key;             // 获取键
        value = self.data[key].value;              // 获取值
    }

    // 跳过已删除的项,返回有效的迭代器位置
    function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted) // 如果该项被标记为删除,则跳过
            keyIndex++;
        return Iterator.wrap(keyIndex); // 返回跳过已删除项后的迭代器
    }
}

// User 合约使用 IterableMapping 库进行数据操作
contract User {
    itmap data; // 声明一个 itmap 类型的变量来保存数据

    // 使用 IterableMapping 库来操作 itmap 类型的数据
    using IterableMapping for itmap;

    // 插入数据到 itmap 中
    function insert(uint k, uint v) public returns (uint size) {
        // 调用 IterableMapping 库的 insert 函数插入数据
        data.insert(k, v);
        
        // 返回当前数据的大小
        return data.size;
    }

    // 计算所有存储数据的总和
    function sum() public view returns (uint s) {
        // 遍历 itmap 中的所有数据并计算总和
        for (
            Iterator i = data.iterateStart(); // 初始化迭代器
            data.iterateValid(i); // 检查迭代器是否有效
            i = data.iterateNext(i) // 获取下一个有效项
        ) {
            (, uint value) = data.iterateGet(i); // 获取当前项的值
            s += value; // 将当前值累加到总和
        }
    }
}
<think> 我们面对的是将高版本(19c)的导出文件导入低版本(11g)数据库的问题。根据引用内容,关键点在于导出时指定version参数为11.2.0.1.0(或类似11g版本号),但用户现在只有19c导出的dmp文件,并未在导出时指定version参数。因此,我们需要考虑在没有原始数据库的情况下如何处理这个已有的19c导出文件。 然而,Oracle数据泵导出文件(dmp文件)是向下兼容的,但不支持向上兼容。也就是说,11g的expdp导出的文件可以导入19c,但19c导出的文件默认不能导入11g,除非导出时指定了低版本的version参数。 现有情况:用户只有19c导出的dmp文件,且该文件导出时未指定version参数(或者即使指定了,我们也不得而知)。那么,这个dmp文件可能包含11g不支持的对象或数据类型,导致直接导入11g失败。 解决方案: 1. 尝试直接导入:虽然大概率会失败,但可以尝试在11g上使用impdp导入,并观察错误日志。如果导出时恰好没有使用19c特有的特性,可能成功。但需注意,19c默认导出的版本是19c,因此失败的可能性很大。 2. 使用中间版本转换:由于没有原始19c数据库,我们无法重新导出指定version参数,因此需要借助一个中间环境(如12c或18c)先将19c的dmp文件导入,然后再从这个中间环境导出为11g兼容的dmp文件。 步骤: a. 准备一个中间版本的Oracle数据库(建议12.1.0.2以上,但低于19c,且支持导入19c的dmp文件)。注意:Oracle允许将高版本的dmp文件导入到相同或更高版本的数据库中,但导入到低版本数据库则不行。因此,中间数据库的版本必须不低于19c?实际上,通常我们导入的数据库版本应该不低于导出数据库版本。但这里我们是要把19c的dmp文件导入到中间数据库,所以中间数据库必须是19c或更高?不对,实际上Oracle允许将高版本的dmp文件导入到相同或更高版本的数据库,但也可以导入到低版本数据库,只要导出时指定了version参数。但我们的情况是,这个dmp文件是19c导出的,而且没有指定version参数,所以只能导入到19c或更高版本的数据库。 因此,中间数据库必须是19c或更高版本。 流程: 已有19c的dmp文件 -> 导入到19c(或更高)的中间库 -> 然后从中间库导出时指定version=11.2 -> 得到兼容11g的dmp文件 -> 导入11g 详细步骤: 1) 在中间服务器(安装有Oracle 19c或更高版本)上创建数据库实例(如果已有则跳过)。 2) 将待转换的dmp文件上传到中间服务器的目录(如DATA_PUMP_DIR对应的操作系统目录)。 3) 在中间数据库中创建必要的用户和表空间(根据dmp文件中的原用户信息)。 4) 使用impdp将dmp文件导入到中间数据库(此时导入是正常的,因为版本相同或更高)。 impdp system/password@mid_db directory=DATA_PUMP_DIR dumpfile=your_19c_file.dmp logfile=imp_mid.log 5) 导入完成后,再从中间数据库使用expdp导出,并指定version=11.2: expdp system/password@mid_db directory=DATA_PUMP_DIR dumpfile=for_11g.dmp version=11.2 logfile=exp_for_11g.log 6) 将导出的for_11g.dmp文件复制到11g服务器,然后使用impdp导入11g数据库。 3. 如果无法搭建中间环境,可以尝试使用第三方工具(如Oracle SQL Developer)导出为SQL脚本,但这种方法对于大数据量或包含大量对象的情况可能不太适用,并且需要手动处理不兼容的对象。 4. 使用数据泵的SQLFILE参数将dmp文件转换为SQL脚本(在19c或更高版本的环境上执行),然后手动修改脚本使其兼容11g,最后在11g上执行。步骤: 在19c环境中执行: impdp system/password directory=DATA_PUMP_DIR dumpfile=your_19c_file.dmp sqlfile=export.sql 这将生成一个export.sql文件,其中包含所有DDL和DML(INSERT语句)。然后,我们需要手动编辑这个sql文件,移除11g不支持的特性(如某些数据类型、语法等)。编辑完成后,在11g上执行这个sql文件。 注意:这种方法只适用于数据量不大的情况,因为insert语句可能非常多,执行效率低。 5. 如果原始19c导出文件是使用exp(传统导出工具)而不是expdp(数据泵)生成的,那么可以使用11g的imp工具尝试导入,因为传统导出工具对版本兼容性稍微好一些。但根据引用内容,用户使用的是expdp。 因此,综合以上,推荐使用中间数据库转换的方法,这是最可靠的方式。 具体步骤(使用中间19c数据库): 1. 在中间服务器(已安装Oracle 19c)上操作: a. 创建必要的目录对象(如果DATA_PUMP_DIR不存在,则需要创建): CREATE OR REPLACE DIRECTORY DATA_PUMP_DIR AS '/path/to/dumpdir'; GRANT READ, WRITE ON DIRECTORY DATA_PUMP_DIR TO SYSTEM; b. 将19c的dmp文件(假设为source_19c.dmp)上传到该目录。 c. 使用impdp导入到中间库(假设原导出文件是全库导出或按用户导出): 如果是按用户导出,我们需要在中间库创建相应用户: CREATE USER original_user IDENTIFIED BY password; GRANT CONNECT, RESOURCE TO original_user; 然后导入: impdp system/password@mid_db DIRECTORY=DATA_PUMP_DIR DUMPFILE=source_19c.dmp LOGFILE=imp_mid.log (如果原导出是全库,则可能需要使用FULL=Y,但需要系统权限) 2. 导入完成后,使用expdp从中间库导出为兼容11g的dmp文件: expdp system/password@mid_db DIRECTORY=DATA_PUMP_DIR DUMPFILE=for_11g.dmp VERSION=11.2 LOGFILE=exp_for_11g.log 如果需要按用户导出,可以加上SCHEMAS=original_user 3. 将for_11g.dmp文件复制到11g服务器,并在11g服务器上导入: impdp system/password@11g_db DIRECTORY=DATA_PUMP_DIR DUMPFILE=for_11g.dmp LOGFILE=imp_11g.log 如果需要改变用户,可以使用REMAP_SCHEMA参数,例如: impdp system/password@11g_db DIRECTORY=DATA_PUMP_DIR DUMPFILE=for_11g.dmp REMAP_SCHEMA=original_user:new_user LOGFILE=imp_11g.log 注意:字符集问题,确保中间库和11g库的字符集一致(或兼容)。 另外,引用[1]中提到在19c导出时指定version=11.2.0.1.0,这样导出的文件可以直接导入11g。但用户现在只有导出文件,所以只能通过中间库转换。 如果用户没有条件搭建中间19c数据库,可以考虑使用云数据库实例(如Oracle Cloud提供的19c数据库)作为中间环境,完成转换后删除实例。 相关问题: 1. 如何解决导入过程中出现的"ORA-39142: 版本号 X.X 无效"错误? 2. 在没有原始数据库的情况下,如何判断一个dmp文件的导出版本? 3. 使用中间数据库转换时,如何快速搭建一个临时的Oracle 19c环境? 4. 如何确保在转换过程中数据不丢失? </think> ### 将Oracle 19c导出文件导入Oracle 11g的解决方案 您需要将现有的19c导出文件(未指定兼容版本)导入11g数据库。以下是分步操作方法: #### ✅ 核心解决方案:使用中间库转换版本 1. **搭建中间环境(Oracle 19c)** - 在独立服务器安装Oracle 19c(可使用虚拟机或Docker容器) - 创建目录对象: ```sql CREATE OR REPLACE DIRECTORY DATA_PUMP_DIR AS '/path/to/dump'; GRANT READ, WRITE ON DIRECTORY DATA_PUMP_DIR TO SYSTEM; ``` 2. **将原始文件导入中间库** ```bash # 在19c服务器执行 impdp system/password@mid_db DIRECTORY=DATA_PUMP_DIR \ DUMPFILE=your_19c_file.dmp \ LOGFILE=imp_mid.log ``` 3. **从中间库导出兼容11g的文件** ```bash # 关键步骤:添加VERSION参数 expdp system/password@mid_db DIRECTORY=DATA_PUMP_DIR \ DUMPFILE=for_11g.dmp \ VERSION=11.2.0.1.0 \ # 指定11g兼容版本[^1][^5] LOGFILE=exp_for_11g.log ``` 4. **导入11g数据库** ```bash # 在11g服务器执行 impdp system/password@11g_db DIRECTORY=DATA_PUMP_DIR \ DUMPFILE=for_11g.dmp \ REMAP_SCHEMA=原用户:新用户 \ # 按需修改用户映射[^2] LOGFILE=imp_11g.log ``` #### ️ 关键注意事项 1. **字符集一致性检查** - 在中间库和11g库查询字符集: ```sql SELECT value FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET'; ``` - 若不同需转换(如AL32UTF8 → ZHS16GBK) 2. **对象兼容性处理** - 自动排除的19c特性:区块链表、PDB元数据、JSON增强功能等 - 导入前预览DDL: ```bash impdp ... SQLFILE=check_ddl.sql ``` 3. **跨平台处理技巧(Windows→Linux)** - 导出时添加参数:`CLUSTER=NO TRANSPORTABLE=ALWAYS`[^1][^5] - 使用`DBMS_FILE_TRANSFER`传输文件 #### 📊 性能优化建议 | 场景 | 优化方案 | |------|----------| | 大数据量(>50GB) | 添加`PARALLEL=4`参数 | | 网络传输慢 | 使用`NETWORK_LINK`直连[^3] | | 磁盘空间不足 | 启用压缩:`COMPRESSION=ALL` | | 频繁失败 | 分次导入:`INCLUDE=TABLES:"IN ('T1','T2')"` | #### ❌ 直接导入风险 若跳过中间库强行导入11g: ```bash impdp system/password@11g_db DUMPFILE=your_19c_file.dmp ``` 可能引发错误: - `ORA-39142`: 版本不兼容 - `ORA-31684`: 对象类型不支持 - `ORA-02374`: 数据类型转换错误 --- ### 替代方案(无需中间库) 若无法搭建中间环境: 1. **使用SQL Developer转换** ```mermaid graph LR A[19c导出文件] --> B[SQL Developer导入] B --> C{选择11g兼容导出} C --> D[生成SQL脚本] D --> E[在11g执行脚本] ``` 2. **手工提取数据** ```bash # 生成CSV文件 sqlplus system/password@mid_db SET pagesize 0 SPOOL /tmp/data.csv SELECT /*csv*/ * FROM your_table; SPOOL OFF # 在11g用SQL*Loader导入 sqlldr userid=scott/tiger control=load.ctl ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋说

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值