# MaxCompute SDK - Complete Documentation (Java / Python / Go) --- # changelog.md # 更新日志 ## [0.57.2-public] - 2026-05-07 ### 🐛 问题修复 * **[Instance]**: **为 MaxQA 实例添加凭据确保机制** - 当通过 `Instances.get(id)` 获取 MCQA 2.0 实例时,自动从响应头获取 `x-odps-mcqa-conn` 和 `x-odps-mcqa-query-cookie` 凭据,确保后续 `/mcqa` 前缀请求能正确路由。 * *相关 API*: `Instance.ensureMaxQACredential()` ## [0.57.1-public] - 2026-04-08 ### 📦 维护 * **[Storage API]**: 添加 Storage API 模块文档 * **[Build]**: 修复 POM 配置问题 ## [0.57.0-public] - 2026-03-25 ### ✨ 新功能 * **[Storage API][Preview]**: **全新 `odps-sdk-storage-api` 模块** - 引入高性能 Storage API 客户端 `MaxStorageClient`,基于 Arrow 列式格式对 MaxCompute 表进行读写。支持通过 InputSplit 分片实现分布式并行读取、写入会话的 commit/abort 生命周期管理、表数据预览、Blob 下载以及 Instance 结果读取。 * *相关 API*: `MaxStorageClient`, `MaxStorageClient.Builder`, `TableReadSession`, `TableWriteSession`, `InstanceReadSession`, `BlobManager` * **[Arrow Helper]**: **全新 `odps-arrow-helper` 模块** - 将 Arrow 相关工具类抽取为独立模块,包含 `TableIdentifier`、`InstanceIdentifier`、`StreamIdentifier`、各类型 Arrow Accessor、`ArrowReaderBuilder`、`ArrowStreamRecordReader`、`SchemaUtils` 等。 * *相关 API*: `TableIdentifier`, `InstanceIdentifier`, `StreamIdentifier`, `ArrowReaderBuilder` * **[SQLExecutor]**: **Storage API 结果集集成** - `SQLExecutorImpl` 在查询结果包含 `BLOB` 列时,自动通过 Storage API(`StorageAPIResultSet`)下载结果,提升兼容性与数据传输效率。 * *相关 API*: `StorageAPIResultSet`, `InternalBlobHelper` * **[SQLExecutor]**: **MaxQA Fallback 配置** - 新增 `FallbackInfo` 和 `MaxQAConnInfo`,支持配置 MaxQA Quota 的回退策略,当主 Quota 不可用时可自动回退到指定 Quota。 * *相关 API*: `FallbackInfo`, `MaxQAConnInfo`, `SQLExecutorBuilder.maxQAConnInfo()`, `SQLExecutorBuilder.enableMaxQA()` * **[RestClient]**: **请求/响应拦截器链** - 新增 `InterceptorChain`、`RequestInterceptor`、`ResponseInterceptor`、`InterceptorContext`,支持可插拔的 HTTP 请求响应拦截。 * *相关 API*: `InterceptorChain`, `RequestInterceptor`, `ResponseInterceptor` * **[Commons]**: **新增 `JsonString` 类型** - 新增 `JsonString`,作为轻量级 `JsonValue` 实现,封装原始 JSON 字符串,适用于无需解析 JSON 结构的场景。 * *相关 API*: `JsonString` * **[Commons]**: **新增 `RecordReader` / `RecordWriter` 接口** - 将通用的 `RecordReader` 和 `RecordWriter` 接口下沉至 `odps-sdk-commons`,便于跨模块复用。 * *相关 API*: `RecordReader`, `RecordWriter` * **[Odps]**: **Catalog API Host 及过期元数据读取配置** - `Odps` 新增 `setCatalogApiHost()` / `getCatalogApiHost()` 和 `setAllowStaleMetadataRead()` / `isAllowStaleMetadataRead()`,分别用于配置 Catalog API 端点和允许读取过期元数据。 * *相关 API*: `Odps.setCatalogApiHost()`, `OdpsOptions.allowStaleMetadataRead` * **[Quota]**: **获取 MaxQA 连接信息** - 新增 `Quotas.getMaxQAConnInfo(quotaName)`,支持获取指定 Quota 的 MaxQA 连接信息。 * *相关 API*: `Quotas.getMaxQAConnInfo()` * **[Partition]**: **分区状态管理** - 新增 `Partition.State` 枚举和 `setState()` 方法,支持设置分区状态。 * *相关 API*: `Partition.State`, `Partition.setState()` ### 🚀 功能增强与性能优化 * **[OdpsType]**: **`OdpsType` 枚举新增数字编码** - 每个 `OdpsType` 枚举值携带稳定的整型 code,并支持通过 `OdpsType.fromCode(int)` 反向查找,便于序列化和协议兼容。 * **[Arrow Helper]**: **Arrow Accessor 层重构** - 将所有按类型实现的 Arrow 列访问器迁移至新的 `odps-arrow-helper` 模块,提升模块化程度和可复用性。 * **[TunnelBufferedWriter]**: **异步 flush 支持** - `TunnelBufferedWriter` 新增非阻塞异步 flush 能力,采用双 buffer 交换机制(`flush(boolean blocking)`),提供背压控制,提升流式上传的写入吞吐量。 * *相关 API*: `TunnelBufferedWriter.flush(boolean blocking)` * **[UpsertStream]**: **异步 flush 及 Buffer 优化** - `UpsertStreamImpl` 支持通过可配置的 `ExecutorService` 执行异步 flush,采用按 bucket 双 buffer 交换机制,并新增 `sync()` 方法支持显式屏障同步。 * *相关 API*: `UpsertStream.Builder.setAsyncFlushService()`, `UpsertStreamImpl.sync()` ### 📦 依赖更新 * **新增**: `com.squareup.okhttp3:okhttp:4.12.0`(在 `odps-sdk-storage-api` 中已 shade) ## [0.56.1-public] - 2026-02-04 ### 🐛 问题修复 * **[CVE]**: **更新 commons-io 至 2.20.0** - 通过将 commons-io 库升级到 2.20.0 版本,修复了安全漏洞。 * **[CVE]**: **使用 at.yawk.lz4 替代 org.lz4.pure-java** - 将 org.lz4.pure-java 依赖替换为 at.yawk.lz4,以解决安全漏洞问题。 ## [0.56.0-public] - 2026-01-30 ### ✨ 新功能 * **[Account]**: **重构认证体系**,采用新的 `Credentials` 类替代旧版 `Credential`,并新增 `getRegionId()` 方法支持区域 ID 配置,使认证机制更加灵活和标准化。 * *相关 API*: `Account.getCredentials()`, `Account.getRegionId()` * **[Instance]**: **增强 MaxQA 查询结果状态管理**,新增 `SelectResultStatus` 枚举以区分查询结果的完整状态(完整/截断/无结果),便于用户判断数据是否完整返回。 * *相关 API*: `Instance.ResultDescriptor.SelectResultStatus`, `Instance.ResultDescriptor.getSelectResultStatus()` * **[ProxyConfig]**: **新增 Netty DNS 解析器控制选项**,允许通过 `withDisableNettyLocalResolver()` 禁用本地 Netty DNS 解析器,适用于需要代理服务器进行 DNS 解析的场景。 * *相关 API*: `ProxyConfig.Builder.withDisableNettyLocalResolver()` ### 🚀 功能增强与性能优化 * **[Arrow]**: **优化 Arrow 数据读取器**,统一使用 `CommonsCompressionFactory` 处理压缩数据,提升大规模数据读取性能和兼容性。 * **[Tunnel]**: **改进 Tunnel Endpoint 解析逻辑**,优先使用 `odps.getTunnelEndpoint()` 配置项,简化端点配置流程。 * **[RestClient]**: **优化重试等待时间计算**,仅使用连接超时时间而非连接+读取超时时间总和,使重试策略更加精准。 * **[TableSchema]**: **新增批量分区列配置方法** `withPartitionColumns()`,简化多分区列的创建流程。 * *相关 API*: `TableSchema.Builder.withPartitionColumns()` ### 🐛 问题修复 * **[Instance]**: **修复任务失败时的错误处理**,在 `waitForSuccess()` 和 `isSelect()` 方法中增加 `checkTaskFailed()` 检查,确保在任务失败时能及时抛出异常并提供详细的失败信息。 ### 📦 依赖更新 * **升级**: `commons-io`: `2.11.0` → `2.14.0` * **替换**: `commons-lang:2.6` → `org.apache.commons:commons-lang3:3.18.0` * **升级**: `io.netty:netty-all`: `4.1.119.Final` → `4.1.130.Final` * **新增**: `org.locationtech.jts:jts-core:1.19.0` * **替换**: `com.aliyun:aliyun-java-auth:0.2.16-beta` → `com.aliyun:credentials-api:1.0.0` ## [0.55.1-public] - 2026-01-22 ### 🐛 问题修复 * **Arrow 字典解码** 修复了 ArrowBatchNonReusedReader 中字典解码未使用压缩工厂的问题,提升 Arrow 数据读取的稳定性和性能。 ## [0.55.0-public] - 2025-12-12 ### ✨ 新增功能 * **Account 凭证信息获取** 所有 Account 实现类新增 `getCredential()` 方法,支持获取当前账号的 AccessKey ID、AccessKey Secret 和 Security Token 信息,便于账号信息管理和安全审计。 * **Tunnel 动态分区写入** 新增 `DynamicPartitionRecordPack` 和 `PartitionRecord`,支持单次写入多个分区,大幅提升多分区数据写入效率。通过 `TableTunnel.StreamUploadSession.Builder.setDynamicPartition()` 启用。 * **Tunnel Arrow 缓冲读取器** 为非 Delta 表新增基于 Arrow 格式的高性能缓冲读取能力,优化大规模数据读取性能。通过 `DownloadSession.openBufferedArrowRecordReader()` 使用。 * **Tunnel 批次追踪** `FlushResult` 接口新增 `getBatchId()` 方法,`StreamUploadSession` 新增 `getLastBatchId()` 和 `getLastBatchCommitTime()` 方法,支持追踪 flush 批次 ID 和提交时间。 * **Arrow 异步流式读取** 新增 `ArrowStreamAsyncReader` 类,支持在独立线程中异步读取 Arrow 数据流,提升 IO 密集型场景下的读取性能。通过 `ReaderOptions.Builder.withAsync()` 和 `withAsyncQueue()` 配置。 * **性能监控指标** 新增 `RateLimitCost` 和 `ServerProcessCost` 两个计数器指标,用于监控限流成本和服务器处理时间,帮助用户优化读取性能。 * **Quota 自动扩容配置** `Quota` 类新增 `autoScaleCPULimit`、`autoScaleMemoryLimit` 和 `autoScaleGPULimit` 字段,支持获取项目的自动扩容配额信息。 * **Project 属性获取改进** `Project.getAllProperties()` 方法现在会从服务器获取包含从 group 继承来的完整配置信息,提供更全面的项目属性视图。 ### 🚀 功能优化 * **Arrow 字典解码优化** `ArrowBatchNonReusedReader` 和 `ArrowBatchReusedReader` 改进了字典编码的处理逻辑,修复了字典解码未使用压缩工厂的问题,提升 Arrow 数据读取的稳定性和性能。 * **TunnelRecordReader 增强** `DownloadSession` 新增 `openRecordReader()` 方法,支持通过 `sizeLimit` 参数控制读取的数据大小限制。 * **缓冲读取器参数优化** `openBufferedRecordReader()` 方法新增 `bufferSize` 参数,允许用户更精细地控制缓冲区大小,优化内存使用。 * **按原始大小读取支持** `DownloadSession` 新增 `isSupportReadByRawSize()` 方法,支持判断当前会话是否支持按原始数据大小进行读取。 ### 🐛 问题修复 * **Instance 异步任务结果获取** 修复了在异步场景下调用 `getTaskResult()` 方法时,如果任务结果列表为空会返回 null 的问题,现在会抛出明确的异常信息。 * **Arrow 字典编码字段处理** 改进了 Arrow Reader 对嵌套结构中字典编码字段的处理逻辑,确保字典数据正确加载和解码。 ## [0.54.0-public] - 2025-10-21 ### ✨ 新增功能 * **Java 21 支持** 新增对 Java 21 的支持,添加了 JDK 21 配置文件和相关构建配置 * **新数据类型支持** 新增 GEOGRAPHY 和 BLOB 数据类型支持,扩展了数据处理能力 * **AspectJ 支持** 添加 AspectJ Maven 插件支持,为面向切面编程提供基础 * **类加载器增强** 改进了类加载器在不同 Java 版本中的兼容性,添加了 getLoadedJars 方法 * **Blob 类** 新增 Blob 类,用于处理存储服务中的大对象引用 * **GeographyObject 接口** 添加 GeographyObject 接口,用于处理地理数据类型 * **Proxy 支持** 新增 ProxyConfig 类,支持 HTTP、HTTPS、SOCKS4、SOCKS5 等多种代理配置 * **增量读取支持** Table API 新增对增量读取的支持,支持基于版本或时间戳的增量数据读取 ### 🐞 问题修复 * **PartitionSpec** 修复了分区规范解析问题,改进了分割逻辑 * **安全权限** 移除了对 sun.security.util.SecurityConstants 的直接依赖,改用标准的 RuntimePermission * **类加载器兼容性** 修复了类加载器在不同 Java 版本中的兼容性问题 * **OdpsOptions** 修复了 OdpsOptions 实例化问题,确保正确传递 Odps 实例引用 ### 📦 依赖升级 * aspectjrt: `1.8.9` → `1.9.7` * mockito-core: `1.10.8` → `4.11.0` * maven-shade-plugin: `3.2.1` → `3.5.1` * maven-compiler-plugin: `3.1` → `3.13.0` (JDK 21) * maven-surefire-plugin: `2.22.2` → `3.2.5` (JDK 21) * maven-javadoc-plugin: `2.10.4` → `3.11.2` (JDK 21) * 移除了 org.codehaus.jackson:jackson-mapper-asl 依赖 ## [0.53.2-public] - 2025-09-11 ### 🐞 问题修复 * **LogView** 修复了 LogView 主机配置并改进了版本处理逻辑 * **UpsertRecord** 修复了列名处理中的大小写敏感性问题 * **CSVRecordParser** 移除了掩盖解析错误的不必要 try-catch 块 * **TableTunnel** 增强了 upsert 操作中的错误处理并改进了 Arrow 选项配置 * **ArrowWriterImpl** 添加了 flush 方法实现以更好地管理资源 ### ✨ 新增功能 * **MaxStorageDownloadOption** 新增对时间戳和日期时间单位的配置支持,以更好地处理数据类型 * **SQLExecutorBuilder** 优化了获取结果的配置并改进了线程管理 * **BatchWriter** 添加了默认的 flush 方法以更好地符合接口规范 * **SessionRecordSetIterator** 恢复了 SessionRecordSetIterator 类以保持向后兼容性 ### 📄 文档更新 * 更新了附录文档并添加了 MaxCompute 类型的映射指南 ## [0.53.1-public] - 2025-08-20 ### ✨ 新增功能 * **SQLExecutorImpl** 删除了内部"SessionRecordSetIterator"类并将其提取为独立类 ### 📦 依赖升级 * snappy-java: `1.1.10.3` → `1.1.10.7` * guava: `32.1.1-jre` → `33.4.8-jre` * netty-all: `4.1.86.Final` → `4.1.119.Final` ## [0.53.0-public] - 2025-07-25 ### ✨ 新增功能 * **VectorizedOutputer** 新增 `getWriteBytes()` 方法,支持获取写入字节数统计。 * **TableBatchReadSession** 新增基于 JSON 的序列化/反序列化方法,提升数据交互灵活性。 * **CreateProjectParam** 新增 `defaultQuota()` 方法,支持设置项目默认配额。 * **Aliyun V4 签名** 支持配置 `corporation` 参数,适配专有云环境部署需求。 * **AklessAccount** 完整支持 V4 签名协议。 * **TableTunnel** 新增 ZSTD (Zstandard) 压缩算法支持,优化数据传输效率。 * **MaxCompute Query Acceleration (MaxQA)** 支持并发读取超大结果集,显著提升性能(*注意:并发操作将增加内存消耗,需根据集群容量合理配置并发度*)。 * **Preview 功能** 支持通过 Tunnel 执行标签(Tag)操作。 * **InstanceTunnel** 新增 `getDownloadSession(String projectName, String instanceID, String sessionId)` 方法,简化下载会话获取流程。 ### 🐞 问题修复 * 修复 `Predicate` 对 `DateTime` 类型处理时未使用本地时区的问题,确保时区一致性。 * 修复 `Table` 类在调用 `reloadExtended()` 时可能覆盖 `reload()` 数据的问题。 ### 📦 依赖升级 * commons-codec: `1.13` → `1.18.1` * jackson-databind: `2.15.2` → `2.18.2` * commons-compress: `1.4` → `1.20` * Apache Arrow: `4.0.0` → `17.0.0` * 新增 zstd-jni 依赖: `1.5.7-2`(支持 ZSTD 压缩) ## [0.52.3-public] - 2025-06-14 ### 🎉 新增功能 - **OdpsOptions** 新增 Odps 实例级别的一些变量,可以通过 `odps.options()` 获取。现有两个方法: - `setUseLegacyLogview` = true/false/null 当为 true,使用 logview,当为 false,使用 jobinsight(logview v2),当为 null(默认值),智能判断当前 region 是否能够使用 jobinsight,如是使用 jobinsight,否则使用 logview。 ⚠️ **兼容性提示**:此前版本默认使用的是 logview,更新到此版本后,获取logview时可能获取到 jobinsight 地址,注意这点以避免兼容性问题。 - `setSkipCheckIfEpv2` = true/false 默认为 false,在 0.51.7 版本中,getTable 等接口增加了对 EPv2 项目的支持,但会影响接口性能。可以通过将此配置设置 true,会跳过Epv2的项目,提高性能。 - **ArrayRecord** 在主要的初始化 Record 场景,比如通过构造函数初始化ArrayRecord,通过Tunnel Session newRecord 方法生成 Record,都新增了 caseSensitive 参数,用来标识使用该 Record setByName 时,是否区分大小写。 ⚠️ **历史兼容说明**:在 0.51.8 版本中,我们让 Record 不再区分大小写(因为 MaxCompute 引擎不区分大小写),但这会导致一些性能损失。因此在本版本,我们提供了方式来恢复原行为。 - **SchemaMismatchRuntimeException** 新增了一种异常类型,来试图 try best 的告诉用户:在 Tunnel 写入过程中,传入的数据和表模式不匹配 可能是表模式发生了变化,请重建 Tunnel Session。该类是 `IllegalArgumentException` 的子类。 ## [0.52.2-public] - 2025-06-03 ### 问题修复 - **CommandApi** 修复了在 SQLExecutor 开启 CommandApi 功能时,在处理 desc extended table 返回结果中 Comment 字段错误的为 Boolean 类型的问题。 ## [0.52.1-public] - 2025-05-08 ### 🎉 新增功能 - **SQLExecutor** SQLExecutorBuilder 新增参数 `skipCheckIfSelect`(仅非 MaxQA 场景生效),默认为 false,当为 true 时,会跳过 select 语句的校验,提高极限性能。 可以在请求场景主要为查询语句时使用,在处理非查询语句时,处理延时会变长。 ## [0.52.0-public] - 2025-04-17 ### 🎉 新增功能 - **TableAPI** - `TableWriteSessionBuilder` 新增 `enhanceWriteCheck` 参数,增强写入校验能力 - `TableCreator` 新增 `Append2 Table` 预览功能(🚧 Preview) - **DownloadSession** - 新增 `enableMaxStorage` 配置,支持通过 StorageAPI 下载 Delta Table(🚧 Preview) - **MaxQA** - 实现 CSV 数据解析到强类型记录功能(`Parse CSV To Record`) - 服务端 Ready 后,`getResult`(非 Tunnel 模式)将返回类型化数据(原全量 String 类型)(🚧 Preview) - **SQL** - 新增 `SQLTaskOption` 和 `CreateInstanceOption` 配置类,简化 `SQLTask.run()` 方法重载 - 支持通过正则表达式提交 Merge Task 作业 - 新增 UniqueId 机制,确保同 ID 作业幂等提交 - **ObjectConverter** - 新增 `BINARY_FORMAT_QUOTED_PRINTABLE` 格式解析支持 ### 🛠️ 功能优化 - **ArrowStreamRecordReader** 重构类实现,支持将任意来源的 `ArrowReader` 转换为 `RecordReader` - **ArrayRecord** - 优化类型校验逻辑: ✅ 将 `set` 方法可能抛出的 `ClassCastException` 改为 `IllegalArgumentException` ✅ 增强错误信息可读性 ✅ 避免 JVM 对异常的隐式优化(如错误信息被截断为 null) ## [0.51.11-public] - 2025-03-18 ### 主要改动 - **结构优化**:`Instance` 类中 `ResultDescriptor` 子类由 `Map` 升级为独立的 POJO 对象,专门适配 **MCQA 2.0 场景** - **兼容保障**:完全兼容旧版逻辑,现有代码无需修改即可正常运行 ### 注意事项 - 未来服务端可能调整 `ResultDescriptor` 数据格式,建议 **MCQA 2.0 用户升级到本版本** ## [0.51.10-public] - 2025-03-11 ### 功能增强 - **TableTunnel 指标支持** Upload/Download 方法新增指标收集能力 [文档参考](link_to_document) - **TunnelBufferedReader 实现** 新增 `TunnelBufferedReader` 类,支持通过短连接下载表/实例数据 - **可排序数据结构** 新增 `ReorderableRecord` 和 `ReorderableStruct` 实体类 [设计说明](https://github.com/aliyun/aliyun-odps-java-sdk/releases/tag/v0.51.10-SNAPSHOT) ## [0.51.9-public] - 2025-02-26 ### 问题修复 - **结构体字段转义修复** 修复 `TypeInfo` 中 `getName(true)` 方法未对嵌套结构体字段名添加反引号的问题 ## [0.51.8-public] - 2025-02-20 ### 变更 - **Record** `set(String columnName, Object value)` 方法现在会忽略 columnName 的大小写。`getColumn` 方法返回的列名将始终为小写。 ⚠️ **兼容性提示**: 注意,这项改动会影响 ArrayRecord 初始化和 setByName 时的性能,用户应当相应的性能测试,我们在 0.52.3 版本中增加了开关来关闭这项功能。 ### 功能 - **Table** 新增`getMetadataJson`和`getExtendedInfoJson`方法 - **Partition** 新增`getMetadataJson`,`getExtendedInfoJson`,`getCdcSize`,`getCdcRecordNum`方法 - **CommandApi** 增强 DescribeTableCommand,现在将额外返回 `MetadataJson` 和 `ExtendedInfoJson` 字段 - **PartitionSpec** 改进构建失败时的报错信息,使报错更加明晰 ## [0.51.7-public] - 2025-02-13 ### 功能 - **MCQA** 在通过 InstanceTunnel 取结果,发生失败回退的场景,加入回退日志 - **EPV2** 新增对 EPV2(External Project V2)的支持,包括`ListTable`, `ListSchema`, `DescribeTable` 等接口。 ⚠️ **兼容性提示**:这会略微影响这些接口的性能(功能不受影响),需要用户注意,我们在 0.52.3 版本中增加了开关来关闭这项功能。 ## [0.51.6-public] - 2025-01-26 ### 修复 - **TypeInfo** 修复了当 `StructTypeInfo` 嵌套在 `ArrayTypeInfo` 或 `MapTypeInfo` 内时,`getTypeName(true)` 方法不会对嵌套内字段名进行 quote 的问题。 ## [0.51.5-public] - 2025-01-14 ### 修复 - **MCQA2** 修复了 MCQA2 作业,可能会使用tunnel取结果时,无法正确抛出异常的问题 ## [0.51.4-public] - 2025-01-14 ### 功能 - **MCQA2** 增加若干项优化,提升了 MCQA2 作业的执行效率。MCQA2 作业的模式变为 `ExecuteMode.INTERACTIVE_V2`,与 MCQA1 的 `ExecuteMode.INTERACTIVE` 区别开 - **SQLExecutor** 新增 `getExecuteMode` 方法,用于获取作业执行模式 ### 变更 - **UpsertStream** 在 0.51.0 版本,修改了 `close` 方法的函数签名(不再抛出 `TunnelException`),在本版本中恢复,以保证接口兼容性。 - **ClusterInfo** 在 0.51.0 版本,toString 方法有所变更,在本版本中恢复,以保证接口兼容性。 - **TunnelRetryStrategy**,**ConfigurationImpl** 类在 0.48.6 版本被移除,在本版本中恢复(但不会起到任何效果!),以保证接口兼容性。 ## [0.51.3-public] - 2025-01-07 ### 功能 - **MCQA2** SQLExecutorImpl 新增 `setProject` 方法,用于指定提交作业使用的默认项目 ### 变更 - **StreamTunnel** 在调用 append 方法时,当 Record 列数量大于 Session Schema 列数量,现在将抛出 `SchemaMismatchException(extend IOExcption)`,而不是抛出 `IOException`,并优化了错误信息 ## [0.51.2-public] - 2024-12-20 ### 功能 - **Authorization** 引入`credential-java`鉴权包,现在能够使用`AlibabaCloudCredentialsProvider`进行鉴权 - **StreamUploadSession** 新增对 Slot 更新的感知和自动重试逻辑 - **table-api** 引入`TableRetryHandler`类,为`table-api`添加重试逻辑 - **udf** 中`InputSplitter`新增方法 `setLimit` ### 变更 - **TypeInfo** `StructTypeInfo` 新增方法 `getTypeName(boolean quote)`,在 `0.51.0-public (rc0)` 版本,`StructTypeInfo` 默认会对字段名使用反引号进行 quote,我们怀疑这项变更对用户有影响,因此决定恢复原行为(默认不进行quote) 而是当用户需要 quote 时,可以调用 `getTypeName(true)` ### 修复 - **TypeInfo** 当对字段使用反引号进行 quote 时,现在会正确对字段名进行转义 - **MCQA2** 修复了 MCQA2 作业调用 `getRawTaskResults` 接口取不到结果的问题 ## [0.51.0-public] - 2024-12-05 ### 功能 - **MapReduce** 支持多重管道输出 (multi pipeline output)。 - **VolumeBuilder** 新增 `accelerate` 方法,用于在 external volume 过大时,使用 dragonfly 加速下载过程。 - **Table** 新增 `TableType OBJECT_TABLE` 和判断方法 `isObjectTable`。 - **Project** `list` 方法增加过滤条件 `enableDr`,用于过滤项目是否开启存储容灾。 - **Cluster** 新增字段 `clusterRole`、`jobDataPath`、`zoneId`。 ### 变更 - **TableBatchReadSession** 类变量 `predicate` 现在设置为 transient。 - **Attribute** 增加转义逻辑,并不再会 double quote。 - **SQLTask** 恢复了在 0.49.0 版本移除的 `SQLTask.run(Odps odps, String project, String sql, String taskName, Map hints, Map aliases, int priority)` 方法,以解决用户的 MR 作业依赖老版本 SDK 时可能发生的接口冲突问题。 ### 修复 - **Table.changeOwner** 修复 SQL 拼写错误。 - **Instance.getTaskSummary** 移除自 0.50.2 版本开始的不合理打印的 debug 日志。 - **TruncTime** 在建表/toString 时,使用反引号对 `columnName` 进行 quote。 > **注意:** 此版本还包括"0.51.0-public.rc0"和"0.51.0-public.rc1"的所有更改。 ## [0.50.6-public] - 2024-11-27 - **Logview** 新增对 Logview V2 的支持,V2 版本保障了数据安全,更多信息参考 [2024年11月14日-MaxCompute Logview安全升级](https://help.aliyun.com/zh/maxcompute/product-overview/2024-service-notices) 。可以通过 `new Logview(odps, 2)` 创建,SQLExecutor 通过 `logviewVersion` 方法指定。 ## [0.51.0-public.rc1] - 2024-11-22 ### 功能与变更 - **Column** `ColumnBuilder` 新增 `withGenerateExpression` 方法,用于构造 auto-partition 列 - **TableSchema** - 新增 `generatePartitionSpec` 方法,用于从`Record`中生成分区信息 - `setPartitionColumns` 方法现在接收`List`,而不是`ArrayList` - **TableCreator** - 新增对`GenerateExpression`的支持,新增方法`autoPartitionBy`,现在可以创建 AutoPartition 表了 - 新增对`ClusterInfo`的支持,现在可以创建 Hash/Range Cluster 表了 - 新增指定 `TableFormat`,现在可以指定创建`APPEND`,`TRANSACTION`,`DELTA`,`EXTERNAL`,`VIEW`格式的表 - 新增`selectStatement`参数,用于`create table as` 和 `create view as` 场景 - 新增`getSql`方法,用于获取创建表的 SQL 语句 - 现在会对所有的 `Comment` 参数进行 quote,以支持包含特殊字符的 `Comment` 参数 - 将 DataHub 相关的建表参数(`hubLifecycle`, `shardNum`) 整合为 `DataHubInfo` - 重命名`withJars`方法为`withResources`,以表示不仅可以使用JAR类型资源 - 重命名`withBucketNum`方法为`withDeltaTableBucketNum`,以表示该方法仅用于 Delta Table - 修改了 `withHints`,`withAlias`,`withTblProperties`,`withSerdeProperties` 方法的逻辑,现在会覆盖之前设置的值,而不是合并 - 移除了`createExternal`方法,现在使用`create`方法即可 - **Table** - 新增 `getSchemaVersion` 方法,用户获取当前表结构的版本,用户每次进行 SchemaEvolution 都会更新版本号,目前该字段仅用于在创建 StreamTunnel 时指定 - 新增 `setLifeCycle`,`changeOwner`,`changeComment`,`touch`,`changeClusterInfo`,`rename`,`addColumns`,`dropColumns`方法,以支持对表结构进行修改 - **StreamTunnel** 修改初始化逻辑,当指定 `allowSchemaMismatch` 为 `false` 时,会自动重试直到使用最新版本的表结构(超时时间为5min) ### 修复 - **GenerationExpression** 修复了当建表时`TruncTime`为大写,reload table 会抛出异常的问题 - **TypeInfoParser** 能够正确处理 `Struct` 类型,字段被反引号quote的 `TypeInfo`了 ## [0.51.0-public.rc0] - 2024-11-18 ### 功能 - **GenerateExpression** 增加对分区列的生成列表达式功能的支持,和第一个生成列表达式`TruncTime`,使用方式请参考[Example](https://github.com/aliyun/aliyun-odps-java-sdk/blob/v0.51.0-public/odps-examples/basic-examples/src/main/java/GenerateExpressionSample.java) - **UpsertStream** 支持写入主键为 `TIMESTAMP_NTZ` 类型的值 - **Table** 新增对 cdc 相关数据的查询,`getCdcSize()`,`getCdcRecordNum()`,`getCdcLatestVersion()`,`getCdcLatestTimestamp()` - **SQLExecutor** MCQA 2.0 作业支持获取 InstanceProgress 信息 ### 变更 - **TypeInfo** 对 Struct 类型的 TypeInfo,和其他拼装 SQL 的方法,使用反引号对名字进行 quote - **AutoClosable** 为了提醒用户正确关闭资源,对下列资源类,增加了相应的 `close()` 方法,以提醒用户正确关闭资源。 - `odps-sdk-core` 包下的 `UpsertStream`, - `odps-sdk-impl` 包下的 `LocalOutputStreamSet`,`ReduceDriver.ReduceContextImpl`,`MapDriver.DirectMapContextImpl`,`LocalRecordWriter` - `odps-sdk-udf` 包下的 `VectorizedOutputer`,`VectorizedExtractor`,`RecordWriter`,`RecordReader`,`Outputer`,`Extractor` ## [0.50.5-public] - 2024-11-13 ### 功能 - **TableAPI** 为可以安全重试的网络请求类型的报错增加了相应的重试逻辑,从而提高了接口的稳定性。在 `RestOptions` 中增加了 `retryWaitTimeInSeconds` 配置项,用于设置重试等待时间。 - **SQLTask** 新增了 `run` 方法的重载,支持传入 `mcqaConnHeader` 参数,以便提交 MCQA 2.0 作业。 - **SQLExecutor** 支持通过指定 `hints` 中的 `odps.task.wlm.quota` 来设置提交 MCQA 2.0 作业时的 interactive quota。 - **RestClient** 新增了 `retryWaitTime` 参数,以及相应的 getter 和 setter 方法,以配置网络请求的重试等待时间。 - **Configuration** 新增了 `socketRetryTimes` 参数以及相应的 getter 和 setter 方法,用于配置 Tunnel 网络请求的重试等待时间。如果未设置,则使用 `RestClient` 中的配置,否则使用此配置。 ### 变更 - **Instances** 移除了 `get` 的重载方法 `get(String projectName, String id, String quotaName, String regionId)` ,该方法在 `0.50.2-public` 版本中新增,用于获取 MCQA 2.0 实例。现在,用户在使用 `get` 方法时无须区分作业是否为 MCQA 2.0 作业,因此移除该方法可以直接使用 `get(String projectName, String id)` 方法来获取实例。 ### 修复 - **Table.read()** 修复了在数据预览时,配置的网络相关参数(如超时时间、重试逻辑)无法正确生效的问题。 - **Streams** 修复了 `create` 方法中,如果指定了 `version` 会报错的问题。同时增加了 `version` 的默认值(1),表示表的初始版本。 ## [0.50.4-public] - 2024-10-29 ### 功能 - **PartitionSpec** 新增`(String, boolean)` 构造方法,通过布尔参数指定是否对分区值进行trim操作,以满足某些场景(如使用char类型作为分区字段)用户不希望trim的需求。 ### 变更 - **Instance** 在调用stop方法时,抛出的OdpsException将不再被二次包装。 ### 修复 - **SQLExecutor** - 修复了在MCQA 1.0模式下,用户指定`fallbackPolicy.isFallback4AttachError`时未正确生效的问题。 - 修复了在MCQA 2.0模式下,作业失败时`cancel`方法抛出异常的问题。 - 修复了在MCQA 2.0模式下,当isSelect判断错误时,通过instanceTunnel取结果报错的问题。 - **Table** 修复了`getPartitionSpecs`方法会trim分区值,导致无法获取存在的分区的问题。 ## [0.50.3-public] - 2024-10-23 ### 功能 - **SQLExecutor** 在 MCQA 1.0 模式下,允许增加自定义回退策略,新增类`FallbackPolicy.UserDefinedFallbackPolicy`。 ## [0.50.2-public] - 2024-10-23 ### 功能 - **SQLExecutor** 增强 MCQA 2.0 功能: - `isActive` 将返回 false,指示在 MCQA 2.0 模式下没有活跃的 Session。 - 新增 `cancel` 方法,用于中止正在执行的作业。 - `getExecutionLog` 现在返回当前日志的深拷贝并清空当前日志,避免重复获取。 - 在 `SQLExecutorBuilder` 新增 `quota` 方法,支持复用已加载的 `Quota`,减少加载时间。 - 在 `SQLExecutorBuilder` 新增 `regionId` 方法,允许指定 quota 所在的 region。 - **Quotas** 新增带 `regionId` 参数的 `getWlmQuota` 方法,用于获取指定 regionId 的 quota。 - **Quota** 新增 `setMcqaConnHeader` 方法,支持用户通过自定义的 McqaConnHeader 重载 quota,以适配 MCQA 2.0。 - **Instances** 新增适用于 MCQA 2.0 的 `get` 方法,需额外传入 MCQA 2.0 的 QuotaName 和 RegionId。 - **Instance** 进一步适配 MCQA 2.0 作业。 - **TableSchema** `basicallyEquals` 方法将不再严格检查两个类的 Class 类型一致性。 ### 优化 - **SQLExecutor** `run` 方法中的 hints 现在会进行深拷贝,保护用户传入的 Map,支持不可变类型(如 `ImmutableMap`)。 ### 修复 - **Stream** 修复 `create` 方法中的潜在 SQL 语法错误。 ## [0.50.1-public] - 2024-10-11 ### 修复 - **TableAPI** 修复了使用`SplitRecordReaderImpl`获取结果时,拿到了`ArrayRecord`无法正确`toString`的问题。 - **TableAPI** 修复了使用`SplitRecordReaderImpl`获取结果时,如果`Split`对应的`Record`数量为0,在`get` 操作时会抛出数组越界异常的问题。 - **TableAPI** 修复了复合谓词`CompositePredicate`在遇到空谓词时,可能额外增加一次操作符的问题。 ## [0.50.0-public] - 2024-10-09 ### 功能 - 新增 `SchemaMismatchException` 异常:当使用 `StreamUploadSession` 时,如果用户上传的 Record 结构与表结构不匹配,将抛出该异常。此异常将额外携带最新的 schema version,方便用户重建 Session 并进行重试操作。 - 在 `StreamUploadSession.Builder` 中新增 `allowSchemaMismatch` 方法,用于指定是否容忍用户上传的 Record 结构与表结构不匹配时是否抛出异常。默认值为 `true`。 ### 修复 - 修复了在 Odps 中指定 `tunnelEndpoint` 时,使用 `StreamUploadSession` 无法生效的问题。 - 修复了 `TunnelRetryHandler` 潜在的 NPE 问题。 ## [0.50.0-rc1] - 2024-09-19 ### 功能 - **SQLExecutor** 新增 `isUseInstanceTunnel` 方法: - 用来判断是否使用 instanceTunnel 取结果 ### 修复 - 修复了使用 SQLExecutor 执行 MCQA 2.0 作业时,执行 CommandApi 任务会影响下一次作业,导致取结果时抛出NPE的问题。 ## [0.50.0-rc0] - 2024-09-18 ### 功能 - **SQLExecutor** 支持提交 MCQA 2.0 作业 - SQLExecutorBuilder 新增方法 `enableMcqaV2` - SQLExecutorBuilder 新增对字段的 getter 方法 - SQLExecutor 新增 `getQueryId` 方法: - 对于离线作业和 MCQA 2.0 作业,会返回当前执行的作业 InstanceId - 对于 MCQA 1.0 作业,会返回 InstanceId 和 SubQueryId - **TableAPI** `EnvironmentSettings` 新增 `SharingQuotaToken` 参数,以支持提交作业时携带Quota资源共享临时凭证 - **Quotas** 新增 `getWlmQuota` 方法: - 能够根据 projectName 和 quotaNickName 获取到 quota 的详细信息,比如是否属于交互式 quota - **Quota 类**新增 `isInteractiveQuota` 方法,用来判断 quota 是否属于交互式 quota(适用于 MCQA 2.0) - 新增 `getResultByInstanceTunnel(Instance instance, String taskName, Long limit, boolean limitEnabled)` 方法: - 用来无限制地通过 instanceTunnel 获取结果(解除限制需要更高的权限) - **UpsertSession.Builder** 新增 `setLifecycle` 方法,用来配置 Session 生命周期 ### 修复 - 修复了使用 SQLExecutor 执行离线作业时,指定 `limitEnabled` 取结果但不生效的问题 - 修改了 SQLExecutor 执行离线作业时,`getQueryId` 方法会返回作业的 instanceID 而非 null - 修复了 SQLExecutor 执行离线作业时,当遇到非 select 语句时,使用 instanceTunnel 取结果不再抛出异常,而是回退到非 tunnel 逻辑 - 修复了使用 DownloadSession 下载数据时,发生错误且读取数量刚好等于要读取记录的数量 - 1 时重建漏掉一条数据的问题 - **Odps 类**的 `clone` 方法现在能正确克隆包括 `tunnelEndpoint` 等其他字段 - **Instance** 的 `getRawTaskResults` 方法现在在处理同步作业时不会多次发起请求 ## [0.49.0-public] - 2024-09-12 ### 功能 - **OdpsRecordConverter 功能增强**:现在支持将数据转换为 SQL 兼容格式,比如对于 `LocalDate` 类型,数据可以转换为 `"DATE 'yyyy-mm-dd'"` 格式。同时对于 `Binary` 类型,现在支持了 hex 表示格式。 - **开放存储谓词下推常量增强**:改进了 `Constant` 类行为,新增了 `Constant.of(Object, TypeInfo)` 方法。现在当设定或识别出类型为时间类型时,可以正确转变为 SQL 兼容格式(也就是可以正确下推时间类型了)。同时修复了一些其他类型的问题,当无法转换成 SQL 兼容模式时,会在创建 `Session` 的时候抛出 `IllegalArgumentException`。 - **UpsertSession 实现 Closable 方法**:提醒用户应当正确释放 UpsertSession 的本地资源。 - **SQLExecutorBuilder 新增方法** `offlineJobPriority`:用来设置当作业发生回退时,离线作业的优先级。 - **Table 类新增方法** `getLastMajorCompactTime`:用来获取表最后一次 major compact 的时间。 - **Instance 类新增方法** `create(Job job, boolean tryWait)`:当用户执行 `tryWait` 为 `true` 时,作业会尝试在服务端等待一段时间,以更快获取结果。 - **Resource 类增强**:现在能够判断对应的资源是否属于临时资源。 - **CreateProjectParma 类增强** 新增`defaultCtrlService`参数,用来指定项目的默认控制集群。 ### 修复 - **UpsertStream NPE 修复**:修复了在 flush 时,当发生本地错误时抛出 NPE 而无法正确重试的问题。 - **Varchar/Char 类型修复**:修复了 `Varchar/Char` 类型获取其长度时,当遇到中文符号或表情等特殊字符,会错误的计算两次的问题。 ## [0.48.8-public] - 2024-08-12 ### 增强 - 引入了对复合谓词表达式的内部验证,修复了处理无效或总是真/假谓词时的逻辑,增强了测试覆盖,确保了在复杂查询优化中的稳定性和准确性。 ## [0.48.7-public] - 2024-08-07 ### 增强 - **TableTunnel 配置优化**:引入 `tags` 属性至 `TableTunnel Configuration` ,旨在允许用户为隧道相关操作附上自定义标签。这些标签会被记录在租户层级的 `information schema` 中,便于日志追踪与管理。 ```java Odps odps; Configuration configuration= Configuration.builder(odps) .withTags(Arrays.asList("tag1","tag2")) // 使用 Arrays.asList 以提升代码规范性 .build(); TableTunnel tableTunnel=odps.tableTunnel(configuration); // 继续执行隧道相关操作 ``` - **Instance 增强**:在 `Instance` 类中新增 `waitForTerminatedAndGetResult` 方法,此方法整合了 0.48.6 及 0.48.7 版本中对 `SQLExecutor` 接口的优化策略,提升了操作效率。使用方式可参考 `com.aliyun.odps.sqa.SQLExecutorImpl.getOfflineResultSet` 方法。 ### 优化 - **SQLExecutor 离线作业处理优化**:显著减少了端到端延迟,通过改进使得由 `SQLExecutor` 执行的离线作业能在关键处理阶段完成后即刻获取结果,无需等待作业全部完成,提高了响应速度和资源利用率。 ### 修复 - **TunnelRetryHandler NPE修复**:修正了 `getRetryPolicy` 方法中在错误码 (`error code`) 为 `null` 的情况下潜在空指针异常问题。 ## [0.48.6-public] - 2024-07-17 ### 新增 - **支持序列化**: - 主要数据类型如 `ArrayRecord`、`Column`、`TableSchema` 和 `TypeInfo` 现在支持序列化和反序列化,能够进行缓存和进程间通信。 - **谓词下推**: - 新增 `Attribute` 类型的谓词,用于指定列名。 ### 变更 - **Tunnel 接口重构**: - 重构了 Tunnel 相关接口,加入了无感知的重试逻辑,大大增强了稳定性和鲁棒性。 - 删除了 `TunnelRetryStrategy` 和 `ConfigurationImpl` 类,分别被 `TunnelRetryHandler` 和 `Configuration` 所取代。 ### 优化 - **SQLExecutor 优化**: - 在使用 `SQLExecutor` 接口执行离线 SQL 作业时进行优化,减少每个作业获取结果时的一次网络请求,从而减少端到端延时。 ### 修复 - **Table.read Decimal 读取**: - 修复了 `Table.read` 接口在读取 `decimal` 类型时,后面补零不符合预期的问题。 ## [0.48.5-public] - 2024-06-17 ### 新增 - `Table` 接口新增 `getPartitionSpecs` 方法, 相比 `getPartitions` 方法,该方法无需获取分区的详细信息,进而获得更快的执行速度 ### 变更 - 移除了`Column`类中的`isPrimaryKey` 方法。这个方法最初是为了支持用户在创建表时指定某些列为主键。然而,在读取场景下,这个方法容易引起误解,因为它并不会与服务端通信,所以当用户希望知道某列是否为主键时,这个方法并不适用。此外,在使用该方法建表时,主键应当是表级别的字段(因为主键是有序的),而该方法忽略了主键的顺序,设计上不合理。因此,在0.48.5版本中移除了该方法。 在读取场景,用户应当使用`Table.getPrimaryKey()`方法来获取主键。 在建表场景,改为在`TableCreator`中增加`withPrimaryKeys`方法以达成建表时指定主键的目的。 ### 修复 修复了`RecordConverter`在format String类型的`Record`,当数据类型为`byte[]` 时,会抛出异常的问题 ## [0.48.4-public] - 2024-06-04 ### 新增 - 使用 `table-api` 写MaxCompute表,现在支持`JSON`和`TIMESTAMP_NTZ`类型 - `odps-sdk-udf` 功能继续完善 ### 变更 - Table.read() 接口在遇到 Decimal 类型时,目前将默认去掉尾部的 0(但不会使用科学计数法) ### 修复 - 修复了 ArrayRecord 针对 JSON 类型不支持 getBytes 方法的问题 ## [0.48.3-public] - 2024-05-21 ### 新增 - 在构建UpsertSession时,现在支持传入 `retryStrategy`。 ### 变更 - `UpsertStream.Listener` 的 `onFlushFail(String, int)` 接口被标记为了 `@Deprecated` ,使用 `onFlushFail(Throwable, int)` 接口替代。该接口将在 0.50.0 版本中移除。 - Tunnel upsert 的默认压缩算法更改为 `ODPS_LZ4_FRAME`。 ### 修复 - 修复了 Tunnel upsert 当压缩算法不为 `ZLIB` 时,数据无法正确写入的问题。 - 修复了 UpsertSession 当用户未显式调用 `close` 时,资源长时间无法释放的问题。 - 修复了 Tunnel 获取数据相关接口(`preview`,`download`),当遇到表内存在不合法 `Decimal` 类型时(如 `inf`,`nan`),会抛出异常的问题,现在会返回 `null`(与 `getResult` 接口一致)。 ## [0.48.2-public] - 2024-05-08 ### 重要修复 - 修复了Tunnel upsert时,对DATE、DATETIME类型的主键进行分桶时,依赖用户本地时区的问题。这可能导致分桶有误,导致数据查询异常。强烈建议依赖该特性的用户升级到0.48.2版本。 ### 新增 - `Table`增加获取分层存储的lifecycle配置的方法`getTableLifecycleConfig()`。 - `TableReadSession` 现支持谓词下推了 ## [0.48.1-public] - 2024-05-07 ### 新增 Arrow和ANTLR库:在 Maven Shade 插件配置中添加了新的包含项,以更好地处理和打包特定库。这些包含项确保某些关键库被正确地打包进最终的遮蔽( Shaded)构件中。新加入的库包括: - org.apache.arrow:arrow-format:jar - org.apache.arrow:arrow-memory-core:jar - org.apache.arrow:arrow-memory-netty:jar - org.antlr:ST4:jar - org.antlr:antlr-runtime:jar - org.antlr:antlr4:jar - org.antlr:antlr4-runtime:jar ### 位置调整 ANTLR和StringTemplate的遮蔽重定位:配置现在包括针对 org.antlr 和 org.stringtemplate.v4 包的更新重定位规则,以防止可能在类路径中存在的这些库的其他版本的潜在冲突。新的遮蔽模式是: org.stringtemplate.v4 重定位至 com.aliyun.odps.thirdparty.org.stringtemplate.v4 org.antlr 重定位至 com.aliyun.odps.thirdparty.antlr ## [0.48.0-public] - 2024-04-22 ### 新增 - 引入了`odps-sdk-udf`模块,支持在UDF中按批读取MaxCompute数据,能在大数据量场景下显著提高性能。 - `Table`现支持获取`ColumnMaskInfo`,用于数据脱敏场景,方便相关信息的获取。 - 新增通过`odps.getRestClient().setProxy(Proxy)`方法设置代理的支持。 - 实现了可迭代的`RecordReader`以及`RecordReader.stream()`方法,允许将其转换为`Record`对象的流。 - 在`TableAPI RestOptions`中新增`upsertConcurrentNum`和`upsertNetworkNum` 参数,为使用TableAPI进行upsert操作的用户提供更细致的控制。 - 支持使用`Builder`模式来构建`TableSchema`。 - `ArrayRecord`支持`toString`方法。 ### 变更 - 现在,当用户使用`StsAccount`但不传递`StsToken`时,将被视作使用`AliyunAccount`。 ### 改进 - `UploadSession`现支持配置`GET_BLOCK_ID`参数,当客户端不需要`blockId`时,可以加速创建Session的速度。 - 使用`builder`模式(`TableCreator`)加强了表的创建方法,现在可以更简单地创建表了。 ### 修复 - 修复了`Upsert Session`获取连接时,超时时间配置错误的问题。 - 修复了`TimestampWritable`在纳秒为负数时计算出错一秒的问题。 ## [0.47.0-public] - 2024-04-08 ### 新增 - 对 Stream 新类型的支持,可用于进行增量查询。 - 在 `TableTunnel` 中增加了 `preview` 方法,用于数据预览。 - 引入 `OdpsRecordConverter`,用于对 Record 进行解析和格式化。 - `Projects` 类增加了 `create`(创建)和 `delete`(删除)方法,`update` 方法现已公开。`group-api` 包下的相关操作已被标记为弃用。 - `Schemas` 类增强,支持通过设置 `SchemaFilter` 来过滤 schema,支持 `listSchema` 以及获取 schema 的详细元信息。 - `DownloadSession` 新增参数 `disableModifiedCheck`,用于跳过修改检查。新增参数 `fetchBlockId`,用于跳过获取 block ID 列表。 - `TableWriteSession` 支持写入 `TIMESTAMP_NTZ` / `JSON` 类型,新增参数 `MaxFieldSize`。 - `TABLE_API` 新增 `predicate` 相关类,用于后续支持谓词下推。 ### 变更 - `Table` 类的 `read` 方法实现现已更换为 `TableTunnel.preview` 方法,会支持 MaxCompute 新类型,时间类型切换为 Java 8 无时区类型。 - 默认的 `MapWritable` 实现从 `HashMap` 改为 `LinkedHashMap`,以确保有序。 - `Column` 类现支持使用建造者模式(Builder pattern)进行创建。 ### 改进 - `TableReadSession` 新增参数 `maxBatchRawSize` 和 `splitMaxFileNum`。 - `UpsertSession` 现支持: - 写入部分列。 - 设置 Netty 线程池的数量(默认更改为 1)。 - 设置最大并发量(默认值更改为 16)。 - `TableTunnel` 支持设置 `quotaName` 选项。 --- # faq.md # 常见问题 ## 问题排查 ### 如何判断是服务端还是客户端问题? **非 SQL 作业**:检查是否有 requestId,参考[如何获取 requestId](#如何获取-requestid)。 **SQL 作业**:查看 Logview 日志,参考[如何获取 Logview](#如何获取-logview)。 - 服务端问题请咨询 MaxCompute 技术支持 - 客户端问题请提交 [GitHub Issue](https://github.com/aliyun/aliyun-odps-java-sdk/issues) :::tip 如果未升级 SDK,客户端通常不会出现突发问题。如遇异常,可尝试升级 SDK,问题可能在新版本中已修复,详见[更新日志](changelog.md)。 ::: ### 如何获取 Logview? 每个 MaxCompute Instance 都可以生成 Logview,用于查看运行日志。 **Logview V2(推荐)**: ```java Instance i = SQLTask.run(odps, "select 1;"); Logview logview = new Logview(odps, 2); String logviewUrl = logview.generateLogView(i, 3 * 24); System.out.println(logviewUrl); ``` **Logview V1**: ```java Instance i = SQLTask.run(odps, "select 1;"); String logviewUrl = odps.logview().generateLogView(i, 3 * 24); System.out.println(logviewUrl); ``` ### 如何获取 requestId? 当请求发生异常时,可以从 `OdpsException` 中获取 requestId: ```java try { // 业务逻辑 } catch (OdpsException e) { System.out.println(e.getRequestId()); } ``` --- ## 版本与兼容性 ### SDK 支持哪些 Java 版本? | Java 版本 | 支持状态 | |-----------|---------| | Java 8 | 完全支持(编译目标版本) | | Java 11 | 完全支持 | | Java 17 | 支持,需要添加 JVM 参数开放模块访问 | | Java 21 | 支持,需要添加 JVM 参数开放模块访问 | Java 17+ 需要添加的 JVM 参数示例: ``` --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED ``` ### Shaded JAR 和普通 JAR 有什么区别? SDK 发布了两种 artifact: - **`odps-sdk-core`**:普通 JAR,依赖的第三方库(如 Guava、Jackson、Protobuf 等)需要由用户项目管理,可能产生版本冲突。 - **`odps-sdk-core` (classifier: shaded)**:将主要第三方依赖重命名打包,避免与用户项目中的同名依赖冲突。推荐在依赖冲突场景下使用。 --- ## 数据读写 ### 应该使用 Tunnel 还是 Storage API? | 对比维度 | Tunnel | Storage API | |---------|--------|-------------| | 数据格式 | SDK 自有格式 | Apache Arrow 列式格式 | | 适用场景 | 通用数据上传/下载 | 高性能批量读写、与大数据生态集成 | | 成熟度 | 成熟稳定 | 较新,性能更优 | | 学习成本 | 较低 | 需了解 Arrow 格式 | **建议**:新项目优先使用 Storage API 获得更好的性能;已有 Tunnel 代码且运行稳定的项目无需迁移。 --- ## 连接与配置 ### 如何配置连接超时时间? 通过 `RestClient` 设置连接和读取超时: ```java Odps odps = new Odps(account); odps.getRestClient().setConnectTimeout(60); // 连接超时,单位秒 odps.getRestClient().setReadTimeout(300); // 读取超时,单位秒 ``` ### 如何配置重试策略? SDK 默认会对网络异常进行重试,可自定义重试次数: ```java odps.getRestClient().setRetryTimes(5); // 设置重试次数 ``` --- ## 常见错误 ### ODPS-0410051: Invalid credentials AccessKey ID 或 AccessKey Secret 不正确。请检查: 1. AccessKey 是否正确复制(注意首尾空格) 2. AccessKey 是否已被禁用 3. 是否使用了正确的 Endpoint ### ODPS-0422155: Query timed out SQL 执行超时。可能原因: 1. 数据量过大,建议添加过滤条件或分区裁剪 2. 集群资源不足,请联系管理员 ### NoSuchMethodError / ClassNotFoundException 通常是依赖冲突导致。解决方案: 1. 使用 `mvn dependency:tree` 排查冲突依赖 2. 使用 shaded 版本的 SDK 3. 在 pom.xml 中通过 `` 排除冲突的传递依赖 --- ## 更多资源 - [阿里云官方 FAQ](https://help.aliyun.com/zh/maxcompute/user-guide/faq-about-sdk-for-java) - [GitHub Issues](https://github.com/aliyun/aliyun-odps-java-sdk/issues) - [更新日志](changelog.md) --- # getting-started/authentication.md # 认证方式 MaxCompute SDK 支持多种认证方式,适用于不同的使用场景。本文详细介绍每种认证方式的用法和适用场景。 ## 如何选择 | 认证方式 | 适用场景 | 安全性 | 复杂度 | |---------|---------|--------|--------| | [AccessKey](#accesskey) | 开发测试、简单脚本 | 中 | 低 | | [STS Token](#sts-token) | 临时授权、跨账号访问 | 高 | 中 | | [CredentialProvider](#credentialprovider) | 生产环境、ECS/容器部署 | 高 | 中 | | [双重签名](#双重签名) | 应用代理访问、多租户 | 高 | 高 | | [Bearer Token](#bearer-token) | 短期访问、细粒度权限控制 | 高 | 中 | **推荐**:生产环境建议使用 CredentialProvider,它支持凭据自动轮换和多种凭据来源(环境变量、配置文件、ECS RAM 角色等)。 ## AccessKey 最常用的认证方式,使用阿里云 AccessKey ID 和 AccessKey Secret 进行身份验证。 ### 示例代码 ```java import com.aliyun.odps.Account; import com.aliyun.odps.Odps; import com.aliyun.odps.account.AliyunAccount; public class AccessKeyExample { public static void main(String[] args) { // 推荐从环境变量读取,避免硬编码 String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); Account account = new AliyunAccount(accessKeyId, accessKeySecret); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); } } ``` ```python from odps import ODPS # AccessKey 认证 o = ODPS( access_id='your-access-id', secret_access_key='your-access-key', project='your-project', endpoint='http://service.odps.aliyun.com/api', ) # 环境变量自动检测 from odps import ODPS o = ODPS(project='your-project', endpoint='http://service.odps.aliyun.com/api') ``` ```go import ( "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" ) // AccessKey 认证 aliAccount := account.NewAliyunAccount("your-access-id", "your-access-key") odpsIns := odps.NewOdps(aliAccount, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("your-project") // 从环境变量加载 acc := account.AccountFromEnv() odpsIns := odps.NewOdps(acc, endpoint) // 从配置文件加载 conf, _ := odps.NewConfigFromIni("./config.ini") odpsIns := conf.GenOdps() ``` ### 环境变量配置 强烈建议通过环境变量管理 AccessKey,避免将敏感信息写入代码或配置文件: ```bash export ALIBABA_CLOUD_ACCESS_KEY_ID="your_access_key_id" export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your_access_key_secret" ``` ### 获取 AccessKey 参考文档:[如何创建和获取 AccessKey](https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair) :::warning 不要将 AccessKey 硬编码在代码中或提交到版本控制系统。生产环境请使用 CredentialProvider 方式。 ::: ## STS Token STS(Security Token Service)提供临时安全凭证,适用于临时授权场景,如移动端访问、跨账号操作等。STS Token 具有有效期,过期后自动失效。 ### 示例代码 ```java import com.aliyun.odps.Account; import com.aliyun.odps.Odps; import com.aliyun.odps.account.StsAccount; public class StsTokenExample { public static void main(String[] args) { String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); String stsToken = System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN"); Account account = new StsAccount(accessKeyId, accessKeySecret, stsToken); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); } } ``` ```python from odps import ODPS from odps.accounts import StsAccount # STS Token 认证 account = StsAccount('sts-access-id', 'sts-access-key', 'sts-token') o = ODPS( account, project='your-project', endpoint='http://service.odps.aliyun.com/api', ) ``` ```go import ( "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" ) // STS Token 认证 stsAccount := account.NewStsAccount("sts-access-id", "sts-access-key", "sts-token") odpsIns := odps.NewOdps(stsAccount, "http://service.odps.aliyun.com/api") ``` ### 适用场景 - 移动应用临时访问 MaxCompute - 第三方应用临时授权 - 跨账号资源访问 - 需要限时访问的场景 ## CredentialProvider :::info 此认证方式仅 Java SDK 支持。 ::: 使用阿里云 Credential SDK 的 `ICredentialsProvider` 接口,支持多种凭据来源和自动轮换。这是生产环境的推荐方式。 ### 添加依赖 除了 MaxCompute SDK 外,还需要引入阿里云 Credential SDK: ```xml com.aliyun credentials-java 0.3.12 ``` ### 示例代码 ```java import com.aliyun.credentials.Client; import com.aliyun.credentials.models.Config; import com.aliyun.odps.Odps; import com.aliyun.odps.account.AklessAccount; public class CredentialProviderExample { public static void main(String[] args) { // 方式一:使用默认凭据链(推荐) // 按顺序尝试:环境变量 > 配置文件 > ECS RAM 角色 Client credentialClient = new Client(); AklessAccount account = new AklessAccount(credentialClient); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); } /** * 方式二:显式指定 ECS RAM 角色 */ public static Odps buildWithEcsRamRole(String roleName) { Config config = new Config(); config.setType("ecs_ram_role"); config.setRoleName(roleName); Client credentialClient = new Client(config); AklessAccount account = new AklessAccount(credentialClient); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); return odps; } /** * 方式三:使用环境变量中的 AK(由 Credential SDK 管理) */ public static Odps buildWithEnvCredential() { Config config = new Config(); config.setType("access_key"); config.setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")); config.setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")); Client credentialClient = new Client(config); AklessAccount account = new AklessAccount(credentialClient); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); return odps; } } ``` ### 说明 `ICredentialsProvider` 是 [credentials-api](https://github.com/aliyun/alibabacloud-credentials-api/tree/master/java) 包定义的接口,[credentials-java](https://github.com/aliyun/credentials-java) 包提供了多种实现。 使用 CredentialProvider 的优势: - 支持凭据自动刷新和轮换 - 支持多种凭据来源(环境变量、配置文件、ECS RAM 角色、OIDC 等) - 统一的凭据管理方式,与其他阿里云 SDK 一致 :::note `ICredentialsProvider` 实现需要自行管理凭据缓存和轮换,以确保性能不发生回退。 ::: ### 参考链接 - [阿里云 CredentialProvider 使用文档](https://help.aliyun.com/zh/sdk/developer-reference/v2-manage-access-credentials) ## 双重签名 :::info 此认证方式仅 Java SDK 支持。 ::: 双重签名认证使用两组 AccessKey:一组标识应用身份,一组标识用户身份。适用于应用代理用户访问 MaxCompute 的场景。 ### 示例代码 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.account.AppAccount; public class DualSignatureExample { public static void main(String[] args) { // 用户身份凭证 String userAccessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String userAccessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); // 应用身份凭证 String appAccessKeyId = System.getenv("APP_ACCESS_KEY_ID"); String appAccessKeySecret = System.getenv("APP_ACCESS_KEY_SECRET"); // 创建应用账号(用于应用标识) AppAccount appAccount = new AppAccount(new AliyunAccount(appAccessKeyId, appAccessKeySecret)); // 创建用户账号(用于用户标识) AliyunAccount userAccount = new AliyunAccount(userAccessKeyId, userAccessKeySecret); // 使用双重签名创建 Odps 实例 Odps odps = new Odps(userAccount, appAccount); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); } } ``` ### 适用场景 - 多租户 SaaS 应用代理用户访问 - 需要同时追踪应用和用户身份的场景 - 应用层面的访问控制和审计 ## Bearer Token Bearer Token 通常用于短期访问授权,配合 MaxCompute 的 Policy 权限控制使用。Token 具有时效性,过期后需要重新生成。 ### 示例代码 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.account.BearerTokenAccount; public class BearerTokenExample { public static void main(String[] args) { String bearerToken = "your_bearer_token"; BearerTokenAccount account = new BearerTokenAccount(bearerToken); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("your_project"); } } ``` ```python from odps import ODPS from odps.accounts import BearerTokenAccount # Bearer Token 认证 account = BearerTokenAccount('your_bearer_token') o = ODPS( account=account, project='your-project', endpoint='http://service.odps.aliyun.com/api', ) ``` ### 生成 Bearer Token Bearer Token 通过 SecurityManager 生成,需要指定 Policy 规则: :::info Bearer Token 的生成仅 Java SDK 支持。 ::: ```java import com.aliyun.odps.Odps; import com.aliyun.odps.security.SecurityManager; public class GenerateBearerToken { public static String generateToken(Odps odps, String policy) throws Exception { SecurityManager sm = odps.projects().get().getSecurityManager(); return sm.generateAuthorizationToken(policy, "BEARER"); } } ``` ### 适用场景 - 需要细粒度权限控制的短期访问 - 为特定操作生成限时凭证 - 配合 Policy 实现最小权限原则 ### 参考链接 - [Policy 权限控制文档](https://help.aliyun.com/zh/maxcompute/user-guide/policy-based-access-control-1) ## 通用配置 无论使用哪种认证方式,创建客户端实例后都需要配置 Endpoint 和默认项目: ```java Odps odps = new Odps(account); // 设置 Endpoint(必需) odps.setEndpoint("http://service.odps.aliyun.com/api"); // 设置默认项目(必需) odps.setDefaultProject("your_project"); ``` ```python from odps import ODPS # Endpoint 和项目在构造时指定 o = ODPS( access_id='your-access-id', secret_access_key='your-access-key', project='your-project', endpoint='http://service.odps.aliyun.com/api', ) ``` ```go // Endpoint 在构造时指定,项目通过 SetDefaultProjectName 设置 odpsIns := odps.NewOdps(aliAccount, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("your-project") ``` ### Endpoint 列表 不同地域使用不同的 Endpoint,常用地域如下: | 地域 | Endpoint | |------|----------| | 华东2(上海) | `http://service.cn-shanghai.maxcompute.aliyun.com/api` | | 华北2(北京) | `http://service.cn-beijing.maxcompute.aliyun.com/api` | | 华南1(深圳) | `http://service.cn-shenzhen.maxcompute.aliyun.com/api` | | 华东1(杭州) | `http://service.cn-hangzhou.maxcompute.aliyun.com/api` | 完整列表请参考:[MaxCompute 开服地域和 Endpoint](https://help.aliyun.com/zh/maxcompute/user-guide/endpoints) ## 参考链接 - [阿里云 RAM 用户指南](https://help.aliyun.com/zh/maxcompute/getting-started/prepare-a-ram-user) - [阿里云 MaxCompute 官方文档](https://help.aliyun.com/zh/maxcompute) --- # getting-started/first-program.md # 第一个程序 本文通过一个完整的示例程序,带你快速体验 MaxCompute SDK 的基本用法:连接 MaxCompute 服务并读取表中的数据。 ## 前置条件 - 已完成 [SDK 安装](./installation.md) - 已配置 [认证凭据](./authentication.md) - 拥有一个 MaxCompute 项目和至少一张包含数据的表 ## 完整代码 ```java import com.aliyun.odps.Account; import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.Table; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.RecordReader; import java.io.IOException; public class FirstProgram { public static void main(String[] args) throws OdpsException, IOException { // 1. 从环境变量读取凭据 String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); if (accessKeyId == null || accessKeySecret == null) { System.err.println("请设置环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET"); System.exit(1); } // 2. 创建认证账号并初始化客户端 Account account = new AliyunAccount(accessKeyId, accessKeySecret); Odps odps = new Odps(account); odps.setEndpoint("http://service.cn-shanghai.maxcompute.aliyun.com/api"); odps.setDefaultProject("your_project"); // 3. 获取表实例 Table table = odps.tables().get("your_table"); // 4. 读取前 10 行数据 RecordReader reader = table.read(10); Record record; while ((record = reader.read()) != null) { System.out.println(formatRecord(record)); } reader.close(); System.out.println("读取完成"); } /** * 将一行记录格式化为可读字符串 */ private static String formatRecord(Record record) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < record.getColumnCount(); i++) { if (i > 0) { sb.append("\t"); } Object value = record.get(i); if (value instanceof byte[]) { sb.append(new String((byte[]) value)); } else { sb.append(value != null ? value.toString() : "NULL"); } } return sb.toString(); } } ``` ```python from odps import ODPS o = ODPS( access_id='your-access-id', secret_access_key='your-access-key', project='your-project', endpoint='http://service.odps.aliyun.com/api', ) # 列出表 for table in o.list_tables(): print(table.name) # 执行 SQL with o.execute_sql('SELECT * FROM my_table LIMIT 10').open_reader() as reader: for record in reader: print(record) ``` ```go package main import ( "fmt" "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" ) func main() { aliAccount := account.NewAliyunAccount("your-access-id", "your-access-key") odpsIns := odps.NewOdps(aliAccount, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("your-project") // 列出表 tables := odpsIns.Tables() tables.List(func(t *odps.Table, err error) { fmt.Println(t.Name()) }) // 执行 SQL ins, _ := odpsIns.ExecSQl("SELECT * FROM my_table LIMIT 10;") ins.WaitForSuccess() } ``` ## 运行前配置 设置环境变量: ```bash export ALIBABA_CLOUD_ACCESS_KEY_ID="your_access_key_id" export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your_access_key_secret" ``` 修改代码中的以下内容: - Endpoint - 替换为你的项目所在地域的 Endpoint - 项目名称 - 替换为你的项目名称 - 表名 - 替换为你要读取的表名 ## 代码解析 ### 第一步:读取凭据 ```java String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"); String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); ``` 从环境变量读取 AccessKey,避免将敏感信息硬编码在代码中。更多认证方式参见[认证方式](./authentication.md)。 ### 第二步:初始化客户端 ```java Account account = new AliyunAccount(accessKeyId, accessKeySecret); Odps odps = new Odps(account); odps.setEndpoint("http://service.cn-shanghai.maxcompute.aliyun.com/api"); odps.setDefaultProject("your_project"); ``` `Odps` 是 SDK 的入口类,通过它可以访问 MaxCompute 的所有资源:表、函数、资源、任务等。创建时需要提供认证账号、服务 Endpoint 和默认项目名。 ### 第三步:获取表实例 ```java Table table = odps.tables().get("your_table"); ``` 通过 `odps.tables()` 获取表集合管理器,再通过 `get()` 获取指定表的实例。此操作是惰性的,不会立即发起网络请求。 ### 第四步:读取数据 ```java RecordReader reader = table.read(10); Record record; while ((record = reader.read()) != null) { System.out.println(formatRecord(record)); } reader.close(); ``` `table.read(10)` 从表中读取前 10 行数据。`RecordReader` 以迭代器模式逐行返回 `Record` 对象,每个 `Record` 包含一行数据的所有列值。 ### 第一步:初始化客户端 ```python from odps import ODPS o = ODPS( access_id='your-access-id', secret_access_key='your-access-key', project='your-project', endpoint='http://service.odps.aliyun.com/api', ) ``` `ODPS` 是 PyODPS 的入口类,构造时传入认证信息、项目名称和 Endpoint。也可以通过环境变量自动检测凭据。 ### 第二步:列出表 ```python for table in o.list_tables(): print(table.name) ``` `list_tables()` 返回当前项目下所有表的迭代器。 ### 第三步:执行 SQL 并读取结果 ```python with o.execute_sql('SELECT * FROM my_table LIMIT 10').open_reader() as reader: for record in reader: print(record) ``` `execute_sql()` 提交 SQL 任务并等待完成,`open_reader()` 打开结果读取器,以迭代器模式逐行返回数据。 ### 第一步:初始化客户端 ```go aliAccount := account.NewAliyunAccount("your-access-id", "your-access-key") odpsIns := odps.NewOdps(aliAccount, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("your-project") ``` 创建 Account 实例和 Odps 客户端,设置 Endpoint 和默认项目。 ### 第二步:列出表 ```go tables := odpsIns.Tables() tables.List(func(t *odps.Table, err error) { fmt.Println(t.Name()) }) ``` 通过 `Tables()` 获取表管理器,`List` 以回调方式遍历所有表。 ### 第三步:执行 SQL ```go ins, _ := odpsIns.ExecSQl("SELECT * FROM my_table LIMIT 10;") ins.WaitForSuccess() ``` `ExecSQl` 提交 SQL 任务并返回实例,`WaitForSuccess()` 等待任务执行完成。 ## 运行程序 使用 Maven 编译并运行: ```bash mvn compile exec:java -Dexec.mainClass="FirstProgram" ``` 或者先编译再运行: ```bash mvn compile java -cp target/classes:$(mvn dependency:build-classpath -q -DincludeScope=runtime -Dmdep.outputFile=/dev/stdout) FirstProgram ``` 直接运行 Python 脚本: ```bash python first_program.py ``` 使用 Go 运行: ```bash go run main.go ``` 预期输出(具体内容取决于你的表数据): ``` value1 value2 value3 value4 value5 value6 ... 读取完成 ``` ## 常见问题 ### 连接超时 确认 Endpoint 地址正确,且网络可以访问该地址。如果在 VPC 内,需要使用 VPC Endpoint。 ### 权限不足 确认 AccessKey 对应的 RAM 用户拥有目标项目和表的访问权限。 ### 表不存在 确认表名拼写正确,且表属于指定的项目。如果表在其他项目中,需要指定项目名称。 ## 下一步 - [构建 ODPS 客户端](./authentication.md) - 了解更多客户端配置选项 - [数据预览](../guides/read-data/preview.md) - 更多数据读取方式 - [SQL 执行](../guides/execute-sql/index.md) - 通过 SDK 执行 SQL 语句 --- # getting-started/installation.md # 安装与配置 本文介绍如何在项目中引入 MaxCompute SDK 依赖。 ## 安装 SDK ### SDK 模块说明 MaxCompute Java SDK 由多个模块组成,可按需引入: | 模块 | artifactId | 说明 | |------|-----------|------| | Core | `odps-sdk-core` | 核心模块,包含客户端、表操作、SQL 执行、Tunnel 数据通道等功能 | | Table API | `odps-sdk-table-api` | 表数据读写高级 API,提供流式读写能力 | | Storage API | `odps-sdk-storage-api` | 存储层访问接口,用于直接读写底层存储数据 | | UDF | `odps-sdk-udf` | 用户自定义函数开发框架 | 大多数场景只需引入 `odps-sdk-core` 即可。 ### Maven #### 基本依赖 在项目的 `pom.xml` 中添加依赖: ```xml com.aliyun.odps odps-sdk-core 0.57.2-public ``` #### 引入其他模块 根据需要添加其他模块: ```xml com.aliyun.odps odps-sdk-table-api 0.57.2-public com.aliyun.odps odps-sdk-storage-api 0.57.2-public com.aliyun.odps odps-sdk-udf 0.57.2-public ``` #### 仓库配置 SDK 发布在 Maven Central,可直接使用。如需加速下载,可配置阿里云 Maven 镜像仓库: ```xml aliyun-public https://maven.aliyun.com/repository/public ``` 也可以在 Maven 的全局配置文件 `~/.m2/settings.xml` 中配置镜像: ```xml aliyun-mirror central https://maven.aliyun.com/repository/public ``` #### 关于 BOM 当前 MaxCompute Java SDK 未提供 BOM(Bill of Materials)。如果项目中同时使用多个模块,请确保所有模块版本保持一致。 ### Gradle #### 基本依赖 在 `build.gradle` 中添加: ```groovy dependencies { implementation 'com.aliyun.odps:odps-sdk-core:0.57.2-public' } ``` #### 引入其他模块 ```groovy dependencies { implementation 'com.aliyun.odps:odps-sdk-core:0.57.2-public' implementation 'com.aliyun.odps:odps-sdk-table-api:0.57.2-public' implementation 'com.aliyun.odps:odps-sdk-storage-api:0.57.2-public' implementation 'com.aliyun.odps:odps-sdk-udf:0.57.2-public' } ``` #### 仓库配置 ```groovy repositories { mavenCentral() // 阿里云镜像(可选,用于加速下载) maven { url 'https://maven.aliyun.com/repository/public' } } ``` 如果使用 Kotlin DSL(`build.gradle.kts`): ```kotlin repositories { mavenCentral() maven { url = uri("https://maven.aliyun.com/repository/public") } } dependencies { implementation("com.aliyun.odps:odps-sdk-core:0.57.2-public") } ``` ### Java 版本要求 | SDK 版本 | 最低 Java 版本 | 说明 | |---------|--------------|------| | 0.54.0 及以上 | Java 8 | 同时支持 Java 21 | | 0.54.0 以下 | Java 8 | 仅支持 Java 8 | - **最低要求**:Java 8(JDK 1.8) - **推荐版本**:Java 8 或 Java 21 - 自 0.54.0 版本起,SDK 完整支持 Java 21 运行时 ### 安装 使用 pip 安装 PyODPS: ```bash pip install pyodps ``` ### 可选依赖 如需使用 DataFrame 功能,建议同时安装 pandas: ```bash pip install pyodps[pandas] ``` ### Python 版本要求 - **最低要求**:Python 3.6 - **推荐版本**:Python 3.8+ ### 安装 使用 `go get` 安装 Go SDK: ```bash go get github.com/aliyun/aliyun-odps-go-sdk ``` ### Go 版本要求 - **最低要求**:Go 1.18 - **推荐版本**:Go 1.20+ ## 验证安装 创建一个简单的测试类来验证 SDK 是否正确引入: ```java import com.aliyun.odps.Odps; public class VerifyInstallation { public static void main(String[] args) { System.out.println("MaxCompute Java SDK 引入成功"); System.out.println("Odps class: " + Odps.class.getName()); } } ``` 如果编译运行成功,说明 SDK 已正确安装。 在 Python 中验证安装: ```python import odps print("PyODPS 安装成功") print(f"版本: {odps.__version__}") ``` 创建一个简单的程序验证安装: ```go package main import ( "fmt" "github.com/aliyun/aliyun-odps-go-sdk/odps" ) func main() { fmt.Println("MaxCompute Go SDK 引入成功") _ = odps.NewOdps } ``` 运行 `go run main.go`,如果没有报错则安装成功。 ## 下一步 - [认证方式](./authentication.md) - 了解如何配置身份认证 - [第一个程序](./first-program.md) - 编写第一个 MaxCompute 程序 --- # guides/command-mapping.md # 控制台命令与 SDK 映射 本文列出 MaxCompute 控制台(odpscmd)最常用的命令,以及如何通过 SDK 实现相同功能。如果你习惯使用 odpscmd,可以快速找到 SDK 中对应的 API。 ## 项目与会话 ### use project — 切换默认项目 ```java odps.setDefaultProject("my_project"); ``` ```python odps.project = 'my_project' ``` ```go odpsIns.SetDefaultProjectName("my_project") ``` ### describe project / desc project — 查看项目信息 对应 SDK 中 Project 对象的 reload 和属性读取方法。 ```java Project project = odps.projects().get("my_project"); project.reload(); System.out.println("Owner: " + project.getOwner()); System.out.println("Comment: " + project.getComment()); System.out.println("创建时间: " + project.getCreatedTime()); // 获取项目属性 Map properties = project.getProperties(); for (Map.Entry entry : properties.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); } ``` ```python project = odps.get_project('my_project') print("Owner:", project.owner) print("Comment:", project.comment) print("创建时间:", project.creation_time) ``` ```go project := odpsIns.Projects().Get("my_project") err := project.Load() fmt.Println("Owner:", project.Owner()) fmt.Println("Comment:", project.Comment()) ``` ### setproject — 修改项目属性 setproject 命令可以查看或修改项目级别的属性,如 `odps.sql.type.system.odps2`、`odps.sql.decimal.odps2` 等。对应 SDK 中 Project 的 updateProject 方法。 ```java // 查看所有项目属性(等价于不带参数的 setproject) Project project = odps.projects().get(); Map properties = project.getAllProperties(); for (Map.Entry e : properties.entrySet()) { System.out.println(e.getKey() + "=" + e.getValue()); } // 修改项目属性(等价于 setproject key=value) Map newProps = new HashMap<>(); newProps.put("odps.sql.type.system.odps2", "true"); newProps.put("odps.sql.decimal.odps2", "true"); odps.projects().updateProject(project.getName(), newProps); ``` ```python # 查看项目属性 project = odps.get_project() print(project.properties) # 修改项目属性 project.update_project(properties={ 'odps.sql.type.system.odps2': 'true', 'odps.sql.decimal.odps2': 'true', }) ``` ```go project := odpsIns.Projects().Get("my_project") err := project.Load() // 查看属性 props := project.Properties() for k, v := range props { fmt.Printf("%s=%s\n", k, v) } // 修改属性 newProps := map[string]string{ "odps.sql.type.system.odps2": "true", } err = project.UpdateProperties(newProps) ``` ### set key=value — 设置会话级标志 set 命令设置的是会话级别的标志(hint),影响后续 SQL 执行行为,并不修改服务端配置。SDK 中通过在提交 SQL 任务时附加 hints 实现。 ```java // 在 SQL 执行时附加 hints Map hints = new HashMap<>(); hints.put("odps.sql.mapper.split.size", "256"); hints.put("odps.sql.reducer.instances", "10"); Instance instance = SQLTask.run(odps, odps.getDefaultProject(), sql, hints, null); instance.waitForSuccess(); ``` ```python # 在 SQL 执行时附加 hints hints = { 'odps.sql.mapper.split.size': '256', 'odps.sql.reducer.instances': '10', } odps.execute_sql(sql, hints=hints) ``` ```go hints := map[string]string{ "odps.sql.mapper.split.size": "256", "odps.sql.reducer.instances": "10", } task := odps.NewSQLTask("query", sql, "", hints) instance, err := odpsIns.ExecTask(task) ``` ### list projects — 列出项目 ```java ProjectFilter filter = new ProjectFilter(); filter.setOwner("ALIYUN$user@example.com"); Iterator it = odps.projects().iteratorByFilter(filter); while (it.hasNext()) { Project p = it.next(); System.out.println(p.getName()); } ``` ```python for project in odps.list_projects(): print(project.name) ``` ```go projects := odpsIns.Projects() err := projects.List(func(p *odps.Project, err error) { fmt.Println(p.Name()) }) ``` ### whoami — 查看当前用户 ```java SecurityManager sm = odps.projects().get().getSecurityManager(); String result = sm.runQuery("whoami", false); System.out.println(result); ``` ```python result = odps.run_security_query("whoami") print(result) ``` ## 表操作 ### describe table / desc table — 查看表结构 desc 命令是最常用的元数据查看命令。对应 SDK 中 Table 对象的 reload 和 getSchema 方法。 ```java Table table = odps.tables().get("my_project", "my_table"); table.reload(); System.out.println("Owner: " + table.getOwner()); System.out.println("创建时间: " + table.getCreatedTime()); System.out.println("最后修改: " + table.getLastMetaModifiedTime()); System.out.println("生命周期: " + table.getLife()); System.out.println("注释: " + table.getComment()); // 列信息 for (Column col : table.getSchema().getColumns()) { System.out.printf("%-20s %-15s %s%n", col.getName(), col.getTypeInfo(), col.getComment()); } // 分区列 for (Column col : table.getSchema().getPartitionColumns()) { System.out.printf("[分区列] %-20s %-15s%n", col.getName(), col.getTypeInfo()); } ``` ```python table = odps.get_table('my_table') table.reload() print("Owner:", table.owner) print("Comment:", table.comment) print("Schema:", table.table_schema) for col in table.table_schema.columns: print(f"{col.name:20s} {col.type:15s} {col.comment or ''}") ``` ```go table := odpsIns.Tables().Get("my_table") err := table.Load() fmt.Println("Owner:", table.Owner()) fmt.Println("Comment:", table.Comment()) schema := table.Schema() for _, col := range schema.Columns { fmt.Printf("%-20s %-15s %s\n", col.Name, col.Type, col.Comment) } ``` ### describe table partition / desc table partition — 查看分区详情 ```java // desc table my_table partition(dt='20250101') Table table = odps.tables().get("my_table"); Partition partition = table.getPartition(new PartitionSpec("dt='20250101'")); partition.reload(); System.out.println("创建时间: " + partition.getCreatedTime()); System.out.println("大小: " + partition.getSize()); System.out.println("记录数: " + partition.getRecordNum()); ``` ```python table = odps.get_table('my_table') partition = table.get_partition("dt='20250101'") print("大小:", partition.size) print("记录数:", partition.record_num) ``` ```go table := odpsIns.Tables().Get("my_table") partition := table.GetPartition(odps.NewPartitionSpec("dt='20250101'")) err := partition.Load() fmt.Println("大小:", partition.Size()) ``` ### show tables — 列出表 ```java // show tables Iterator it = odps.tables().iterator("my_project"); while (it.hasNext()) { Table t = it.next(); System.out.println(t.getName()); } // show tables like 'user%'(按前缀过滤) TableFilter filter = new TableFilter(); filter.setName("user"); Iterator
filtered = odps.tables().iterator("my_project", filter); ``` ```python # show tables for table in odps.list_tables(): print(table.name) # show tables like 'user%' for table in odps.list_tables(prefix='user'): print(table.name) ``` ```go tables := odpsIns.Tables() err := tables.List(func(t *odps.Table, err error) { fmt.Println(t.Name()) }) ``` ### show partitions — 列出分区 ```java Table table = odps.tables().get("my_table"); Iterator it = table.getPartitionIterator(); while (it.hasNext()) { Partition p = it.next(); System.out.println(p.getPartitionSpec()); } ``` ```python table = odps.get_table('my_table') for partition in table.partitions: print(partition.name) ``` ```go table := odpsIns.Tables().Get("my_table") partitions, err := table.GetPartitions() for _, p := range partitions { fmt.Println(p.PartitionSpec()) } ``` ### read table — 预览表数据 read 命令用于快速预览表中的少量数据,最多返回 1 万行。对应 SDK 中 Table 的 read 方法。 ```java // read my_table 100 Table table = odps.tables().get("my_table"); RecordReader reader = table.read(100); Record record; while ((record = reader.read()) != null) { System.out.println(record.get(0)); } ``` ```python # 等价于 read my_table 100 table = odps.get_table('my_table') with table.open_reader() as reader: for record in reader[:100]: print(record[0]) ``` 详见 [数据预览](./read-data/preview.md)。 ### drop table — 删除表 ```java // drop table my_table odps.tables().delete("my_table"); // drop table if exists my_table odps.tables().delete("my_table", true); ``` ```python odps.delete_table('my_table', if_exists=True) ``` ```go err := odpsIns.Tables().Delete("my_table", true) ``` ## 实例操作 ### show instances / show p — 列出实例 show instances(简写 show p)列出当前项目下的实例。对应 SDK 中的实例迭代器。 ```java InstanceFilter filter = new InstanceFilter(); filter.setStatus(Instance.Status.RUNNING); Iterator it = odps.instances().iterator(filter); while (it.hasNext()) { Instance inst = it.next(); System.out.printf("%s %s %s%n", inst.getId(), inst.getStatus(), inst.getStartTime()); } ``` ```python for instance in odps.list_instances(status='Running'): print(instance.id, instance.status, instance.start_time) ``` ```go instances := odpsIns.Instances() err := instances.List(func(inst *odps.Instance, err error) { fmt.Printf("%s %s\n", inst.Id(), inst.Status()) }) ``` ### status instance — 查看实例状态 ```java Instance instance = odps.instances().get("instance_id"); System.out.println("状态: " + instance.getStatus()); // 查看各 Task 的状态 Map taskStatus = instance.getTaskStatus(); for (Map.Entry entry : taskStatus.entrySet()) { System.out.printf("Task %s: %s%n", entry.getKey(), entry.getValue().getStatus()); } ``` ```python instance = odps.get_instance('instance_id') print("状态:", instance.status) print("任务状态:", instance.get_task_statuses()) ``` ```go instance := odpsIns.Instances().Get("instance_id") err := instance.Load() fmt.Println("状态:", instance.Status()) ``` ### kill instance — 终止实例 ```java Instance instance = odps.instances().get("instance_id"); instance.stop(); ``` ```python instance = odps.get_instance('instance_id') instance.stop() ``` ```go instance := odpsIns.Instances().Get("instance_id") err := instance.Terminate() ``` ### wait instance — 等待实例完成 ```java Instance instance = odps.instances().get("instance_id"); instance.waitForSuccess(); // 获取结果 Map results = instance.getTaskResults(); System.out.println(results.get("AnonymousSQLTask")); ``` ```python instance = odps.get_instance('instance_id') instance.wait_for_success() print(instance.get_task_results()) ``` ```go instance := odpsIns.Instances().Get("instance_id") err := instance.WaitForSuccess() ``` ## 资源与函数 ### add resource / add file / add jar / add py — 上传资源 add 命令用于上传文件、JAR 包或 Python 脚本作为 MaxCompute 资源。对应 SDK 中的 Resources.create 方法。 ```java // add file my_file.txt FileResource resource = new FileResource(); resource.setName("my_file.txt"); try (FileInputStream in = new FileInputStream("/path/to/my_file.txt")) { odps.resources().create(resource, in); } // add jar my_udf.jar -f(-f 表示覆盖更新) JarResource jarRes = new JarResource(); jarRes.setName("my_udf.jar"); try (FileInputStream in = new FileInputStream("/path/to/my_udf.jar")) { odps.resources().update(jarRes, in); } ``` ```python # add file my_file.txt odps.create_resource('my_file.txt', 'file', fileobj=open('/path/to/my_file.txt', 'rb')) # add jar my_udf.jar -f odps.create_resource('my_udf.jar', 'jar', fileobj=open('/path/to/my_udf.jar', 'rb')) ``` ```go resource := odps.NewFileResource("my_file.txt") err := odpsIns.Resources().Create(resource, "/path/to/my_file.txt") ``` 详见 [资源管理](./manage-resources/resources.md)。 ### create function — 创建函数 ```java // create function my_udf as 'com.example.MyUDF' using 'my_udf.jar' Function function = new Function(); function.setName("my_udf"); function.setClassPath("com.example.MyUDF"); function.setResources(Collections.singletonList(odps.resources().get("my_udf.jar"))); odps.functions().create(function); ``` ```python odps.create_function('my_udf', class_type='com.example.MyUDF', resources=['my_udf.jar']) ``` ```go function := odps.NewFunction("my_udf", "com.example.MyUDF", []string{"my_udf.jar"}) err := odpsIns.Functions().Create(function) ``` 详见 [函数管理](./manage-resources/functions.md)。 ### show resources / show functions — 列出资源和函数 ```java // show resources Iterator resIt = odps.resources().iterator(); while (resIt.hasNext()) { Resource r = resIt.next(); System.out.printf("%s %s %s%n", r.getName(), r.getType(), r.getLastModifiedTime()); } // show functions Iterator funcIt = odps.functions().iterator(); while (funcIt.hasNext()) { Function f = funcIt.next(); System.out.printf("%s %s%n", f.getName(), f.getClassPath()); } ``` ```python # show resources for resource in odps.list_resources(): print(resource.name, resource.type) # show functions for func in odps.list_functions(): print(func.name, func.class_type) ``` ```go // show resources odpsIns.Resources().List(func(r *odps.Resource, err error) { fmt.Println(r.Name(), r.Type()) }) // show functions odpsIns.Functions().List(func(f *odps.Function, err error) { fmt.Println(f.Name(), f.ClassPath()) }) ``` ### drop resource / drop function — 删除资源和函数 ```java // drop resource my_file.txt odps.resources().delete("my_file.txt"); // drop function my_udf odps.functions().delete("my_udf"); ``` ```python odps.delete_resource('my_file.txt') odps.delete_function('my_udf') ``` ```go odpsIns.Resources().Delete("my_file.txt") odpsIns.Functions().Delete("my_udf") ``` ## 权限与安全 ### grant / revoke — 授权与撤销 grant 和 revoke 命令用于管理 ACL 权限。对应 SDK 中 SecurityManager 的 runQuery 方法。 ```java SecurityManager sm = odps.projects().get().getSecurityManager(); // grant Select on table my_table to user ALIYUN$user@example.com sm.runQuery("GRANT Select ON TABLE my_table TO USER ALIYUN$user@example.com", false); // revoke Select on table my_table from user ALIYUN$user@example.com sm.runQuery("REVOKE Select ON TABLE my_table FROM USER ALIYUN$user@example.com", false); ``` ```python # grant odps.run_security_query("GRANT Select ON TABLE my_table TO USER ALIYUN$user@example.com") # revoke odps.run_security_query("REVOKE Select ON TABLE my_table FROM USER ALIYUN$user@example.com") ``` ### show grants — 查看权限 ```java SecurityManager sm = odps.projects().get().getSecurityManager(); // show grants for current user String result = sm.runQuery("SHOW GRANTS", false); System.out.println(result); // show grants for specific user result = sm.runQuery("SHOW GRANTS FOR USER ALIYUN$user@example.com", false); System.out.println(result); ``` ```python # show grants result = odps.run_security_query("SHOW GRANTS") print(result) ``` 详见 [权限校验](./security/check-permission.md) 和 [ACL 查询](./security/acl-query.md)。 ### show securityconfiguration — 查看安全配置 ```java SecurityManager sm = odps.projects().get().getSecurityManager(); SecurityConfiguration config = sm.getSecurityConfiguration(); System.out.println("LabelSecurity: " + config.isLabelSecurityEnabled()); System.out.println("ProjectProtection: " + config.isProjectProtectionEnabled()); System.out.println("ObjectCreatorHasAccess: " + config.isObjectCreatorHasAccessEnabled()); System.out.println("ObjectCreatorHasGrant: " + config.isObjectCreatorHasGrantEnabled()); ``` ```python result = odps.run_security_query("SHOW SECURITYCONFIGURATION") print(result) ``` ## SQL 执行 ### SQL 查询 — 执行 SQL 语句 控制台中直接输入 SQL 语句即可执行。SDK 中通过 SQLTask 提交。 ```java Instance instance = SQLTask.run(odps, "SELECT * FROM my_table LIMIT 10;"); instance.waitForSuccess(); Map results = instance.getTaskResults(); System.out.println(results.get("AnonymousSQLTask")); ``` ```python with odps.execute_sql('SELECT * FROM my_table LIMIT 10').open_reader() as reader: for record in reader: print(record) ``` ```go instance, err := odpsIns.ExecSql("SELECT * FROM my_table LIMIT 10") err = instance.WaitForSuccess() results, err := instance.GetResult() fmt.Println(results) ``` 详见 [执行 SQL](./execute-sql/offline.md)。 ### cost sql — 预估 SQL 费用 ```java // 通过 dry run 模式估算费用 Instance instance = SQLTask.run(odps, odps.getDefaultProject(), "SELECT * FROM my_table;", "SQL", null, null, null, true); instance.waitForSuccess(); System.out.println(instance.getTaskResults().get("AnonymousSQLTask")); ``` ```python # 预估 SQL 费用 cost = odps.execute_sql_cost('SELECT * FROM my_table') print("Input bytes:", cost.input_size) ``` ## 数据通道 ### tunnel upload — 上传数据 ```java TableTunnel tunnel = new TableTunnel(odps); UploadSession session = tunnel.buildUploadSession() .setProjectName("my_project") .setTableName("my_table") .build(); try (TunnelRecordWriter writer = session.openRecordWriter(0)) { Record record = session.newRecord(); record.set(0, "value"); writer.write(record); } session.commit(new long[]{0}); ``` ```python with odps.get_table('my_table').open_writer() as writer: writer.write([['value1'], ['value2']]) ``` ```go tunnel := odps.NewTunnel(odpsIns) session, err := tunnel.CreateUploadSession("my_project", "my_table") writer, err := session.OpenRecordWriter(0) // write records... writer.Close() session.Commit([]int{0}) ``` 详见 [Tunnel 上传](./write-data/tunnel-upload.md)。 ### tunnel download — 下载数据 ```java TableTunnel tunnel = new TableTunnel(odps); DownloadSession session = tunnel.buildDownloadSession() .setProjectName("my_project") .setTableName("my_table") .build(); try (TunnelRecordReader reader = session.openRecordReader(0, session.getRecordCount())) { while (reader.hasNext()) { Record record = reader.next(); System.out.println(record.get(0)); } } ``` ```python with odps.get_table('my_table').open_reader() as reader: for record in reader: print(record[0]) ``` ```go tunnel := odps.NewTunnel(odpsIns) session, err := tunnel.CreateDownloadSession("my_project", "my_table") reader, err := session.OpenRecordReader(0, session.RecordCount(), nil) // read records... ``` 详见 [Tunnel 下载](./read-data/tunnel-download.md)。 ## Quota 管理 ### show quotas — 列出配额 ```java Iterator it = odps.quotas().iterator(); while (it.hasNext()) { Quota q = it.next(); System.out.println(q.getName()); } ``` ```python for quota in odps.list_quotas(): print(quota.name) ``` ### desc quota — 查看配额详情 ```java Quota quota = odps.quotas().get("quota_name"); System.out.println("名称: " + quota.getName()); System.out.println("集群: " + quota.getCluster()); ``` ```python quota = odps.get_quota('quota_name') print("名称:", quota.name) ``` ## 快速对照表 | 控制台命令 | SDK 对应 | 文档链接 | |-----------|---------|---------| | `use project` | `odps.setDefaultProject()` | — | | `desc project` | `Project.reload()` + 属性方法 | — | | `setproject key=val` | `Projects.updateProject()` | — | | `set key=val` | SQL hints 参数 | — | | `list projects` | `Projects.iteratorByFilter()` | — | | `whoami` | `SecurityManager.runQuery("whoami")` | — | | `desc table` | `Table.reload()` + `Table.getSchema()` | [修改表](./manage-tables/alter-table.md) | | `show tables` | `Tables.iterator()` | [创建表](./manage-tables/create-table.md) | | `show partitions` | `Table.getPartitionIterator()` | [分区管理](./manage-tables/partitions.md) | | `read table` | `Table.read()` | [数据预览](./read-data/preview.md) | | `drop table` | `Tables.delete()` | — | | `show instances` / `show p` | `Instances.iterator()` | — | | `status instance` | `Instance.getStatus()` | — | | `kill instance` | `Instance.stop()` | — | | `wait instance` | `Instance.waitForSuccess()` | — | | `add file/jar/py` | `Resources.create()` | [资源管理](./manage-resources/resources.md) | | `create function` | `Functions.create()` | [函数管理](./manage-resources/functions.md) | | `show resources` | `Resources.iterator()` | [资源管理](./manage-resources/resources.md) | | `show functions` | `Functions.iterator()` | [函数管理](./manage-resources/functions.md) | | `drop resource` | `Resources.delete()` | [资源管理](./manage-resources/resources.md) | | `drop function` | `Functions.delete()` | [函数管理](./manage-resources/functions.md) | | `grant/revoke` | `SecurityManager.runQuery()` | [ACL 查询](./security/acl-query.md) | | `show grants` | `SecurityManager.runQuery()` | [权限校验](./security/check-permission.md) | | SQL 语句 | `SQLTask.run()` | [执行 SQL](./execute-sql/offline.md) | | `cost sql` | SQLTask dry run | — | | `tunnel upload` | `TableTunnel` + `UploadSession` | [Tunnel 上传](./write-data/tunnel-upload.md) | | `tunnel download` | `TableTunnel` + `DownloadSession` | [Tunnel 下载](./read-data/tunnel-download.md) | | `show quotas` | `Quotas.iterator()` | — | | `desc quota` | `Quotas.get()` | — | --- # guides/execute-sql/index.md # 执行 SQL MaxCompute Java SDK 支持三种 SQL 执行模式,分别面向不同的延迟和吞吐场景。本文将帮助您了解各模式的特点并选择最合适的执行方式。 ## 模式对比 | 模式 | 延迟 | 适用场景 | 结果获取 | 入口类 | |------|------|----------|----------|--------| | 离线 (SQLTask) | 秒~分钟 | ETL / 大查询 / 批量计算 | InstanceTunnel | `SQLTask` / `SQLExecutor` | | MCQA v1 | 毫秒~秒 | 交互式查询 (基于 Session) | 直接返回 | `SQLExecutor` | | MCQA v2 (MaxQA) | 毫秒~秒 | 交互式查询 (无 Session) | 直接返回 | `SQLExecutor` | ## 如何选择 ### 使用离线模式的场景 - 数据量大(GB、TB 级别以上)的批量 ETL 作业 - 对延迟不敏感,追求最大吞吐量 - 需要复杂的多阶段 SQL 执行计划 - 需要使用 `SQLTask` 原始接口进行精细控制 ### 使用 MCQA(交互式)模式的场景 - 数据量中、小的即席查询 - 对延迟敏感,期望秒级返回 - BI 报表、数据探索等交互式分析场景 - 需要连续执行多条 SQL 查询 ### MCQA v1 与 v2 的区别 - **MCQA v1**:基于 Session 机制,需要先创建/连接 Session,后续查询在同一 Session 中执行。适合需要长时间保持会话的场景。 - **MCQA v2 (MaxQA)**:无需 Session,每次查询独立提交,使用更简单。通过 Virtual Warehouse 技术使用独享计算资源,减少加速失败。 :::tip 推荐 如果您的项目已开通 MaxQA(MCQA 2.0)资源组,推荐优先使用 MCQA v2 模式,配置更简单且稳定性更高。 ::: ## 统一入口:SQLExecutor SDK 提供了 `SQLExecutor` 作为统一的 SQL 执行接口,通过 `SQLExecutorBuilder` 配置不同的 `ExecuteMode` 即可切换执行模式: ```java // 离线模式 SQLExecutor offline = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.OFFLINE) .build(); // MCQA v1 模式 SQLExecutor mcqa = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .build(); // MCQA v2 (MaxQA) 模式 SQLExecutor maxqa = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .quotaName("your_interactive_quota") .enableMcqaV2(true) .build(); ``` ```python from odps import ODPS odps = ODPS('access_id', 'access_key', 'my_project', endpoint='http://service.odps.aliyun.com/api') # 离线模式:执行 SQL 并等待完成 instance = odps.execute_sql('SELECT * FROM my_table') with instance.open_reader() as reader: for record in reader: print(record) # 交互式模式(MCQA) instance = odps.execute_sql_interactive('SELECT * FROM my_table') with instance.open_reader() as reader: for record in reader: print(record) ``` ```go // 离线模式:执行 SQL instance, err := odpsIns.ExecSQl("SELECT * FROM my_table;") if err != nil { log.Fatalf("%+v", err) } err = instance.WaitForSuccess() ``` ## 相关文档 - [离线模式详细指南](./offline.md) - [MCQA 交互式查询指南](./mcqa.md) - [SQLExecutor 高级用法](./sql-executor.md) - [SQLExecutor API 参考](../../reference/SQLExecutor.md) - [MaxCompute SQL 概述(阿里云官方文档)](https://help.aliyun.com/zh/maxcompute/user-guide/overview-of-maxcompute-sql) - [查询加速 MCQA(阿里云官方文档)](https://help.aliyun.com/zh/maxcompute/user-guide/maxcompute-query-acceleration) --- # guides/execute-sql/mcqa.md # MCQA 交互式查询 MCQA(MaxCompute Query Acceleration)是 MaxCompute 提供的 SQL 查询加速服务,将分钟级查询缩减至秒级甚至毫秒级。SDK 支持 MCQA v1(基于 Session)和 MCQA v2 / MaxQA(无 Session)两种模式。 :::note Go SDK 暂不支持 MCQA 交互式查询。 ::: ## 前置条件 - 已创建 `Odps` 客户端实例并配置好 AccessKey 和 Endpoint - 项目已开通 MCQA 服务或已配置交互式资源组(MaxQA) - Maven 依赖中已引入 `odps-sdk-core >= 0.50.0`(MaxQA 需要 `>= 0.52.0`) ```xml com.aliyun.odps odps-sdk-core 0.52.0+ ``` ## 完整示例 ### MCQA v1(基于 Session) ```java import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.ResultSet; import com.aliyun.odps.utils.SQLExecutorBuilder; import com.aliyun.odps.utils.SQLExecutor; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class McqaV1Example { public static void main(String[] args) { Account account = new AliyunAccount( System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")); Odps odps = new Odps(account); odps.setDefaultProject("your_project"); odps.setEndpoint("http://service.cn-hangzhou.maxcompute.aliyun.com/api"); SQLExecutor sqlExecutor = null; try { // 创建 MCQA v1 模式的 Executor(基于 Session) sqlExecutor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .build(); // 提交查询 sqlExecutor.run("SELECT count(1) FROM my_table;", new HashMap<>()); // 获取 Logview System.out.println("Logview: " + sqlExecutor.getLogView()); // 获取结果 ResultSet resultSet = sqlExecutor.getResultSet(); while (resultSet.hasNext()) { System.out.println(resultSet.next()); } // 在同一 Session 中继续执行查询 sqlExecutor.run("SELECT * FROM my_table LIMIT 10;", new HashMap<>()); ResultSet resultSet2 = sqlExecutor.getResultSet(); while (resultSet2.hasNext()) { System.out.println(resultSet2.next()); } } catch (OdpsException | IOException e) { e.printStackTrace(); } finally { if (sqlExecutor != null) { sqlExecutor.close(); } } } } ``` ```python # MCQA 交互式查询(毫秒~秒级响应) instance = o.execute_sql_interactive('SELECT * FROM my_table LIMIT 100') # 读取结果 with instance.open_reader() as reader: for record in reader: print(record) # 读取为 DataFrame with instance.open_reader(tunnel=True) as reader: df = reader.to_pandas() ``` ### MCQA v2 / MaxQA(无 Session) ```java import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.ResultSet; import com.aliyun.odps.utils.SQLExecutorBuilder; import com.aliyun.odps.utils.SQLExecutor; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class McqaV2Example { public static void main(String[] args) { Account account = new AliyunAccount( System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")); Odps odps = new Odps(account); odps.setDefaultProject("your_project"); odps.setEndpoint("http://service.cn-hangzhou.maxcompute.aliyun.com/api"); SQLExecutor sqlExecutor = null; try { // 创建 MCQA v2 (MaxQA) 模式的 Executor sqlExecutor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .quotaName("your_interactive_quota_nickname") .enableMcqaV2(true) .build(); // 提交查询 Map hints = new HashMap<>(); sqlExecutor.run("SELECT count(1) FROM my_table;", hints); // 获取 Logview 和查询 ID System.out.println("Logview: " + sqlExecutor.getLogView()); System.out.println("QueryId: " + sqlExecutor.getQueryId()); // 获取结果 ResultSet resultSet = sqlExecutor.getResultSet(); while (resultSet.hasNext()) { System.out.println(resultSet.next()); } // 继续执行下一条查询(每次独立提交,无 Session 依赖) sqlExecutor.run("SELECT * FROM my_table LIMIT 10;", new HashMap<>()); ResultSet resultSet2 = sqlExecutor.getResultSet(); while (resultSet2.hasNext()) { System.out.println(resultSet2.next()); } } catch (OdpsException | IOException e) { e.printStackTrace(); } finally { if (sqlExecutor != null) { sqlExecutor.close(); } } } } ``` ```python # MCQA 交互式查询(毫秒~秒级响应) instance = o.execute_sql_interactive('SELECT * FROM my_table LIMIT 100') # 读取结果 with instance.open_reader() as reader: for record in reader: print(record) # 读取为 DataFrame with instance.open_reader(tunnel=True) as reader: df = reader.to_pandas() ``` ## 代码说明 ### MCQA v1 工作原理 1. **创建 Session**:`SQLExecutorBuilder.build()` 时会在服务端创建(或连接到已有的)Session 实例 2. **提交子查询**:`run()` 将 SQL 作为子查询提交到 Session 中执行 3. **获取结果**:`getResultSet()` 阻塞等待子查询完成并返回结果迭代器 4. **Session 复用**:同一个 `SQLExecutor` 的多次 `run()` 调用共享同一个 Session ### MCQA v2 (MaxQA) 工作原理 1. **无 Session**:每次 `run()` 独立提交查询,无需维护 Session 生命周期 2. **独享资源**:通过 `quotaName` 指定交互式资源组,使用 Virtual Warehouse 独享计算资源 3. **获取结果**:与 v1 相同,通过 `getResultSet()` 获取结果 ### 两种模式对比 | 特性 | MCQA v1 | MCQA v2 (MaxQA) | |------|---------|-----------------| | Session 管理 | 需要(自动) | 不需要 | | 资源隔离 | 共享资源 | 独享资源组 | | 配置复杂度 | 低 | 低(需指定 quotaName) | | 加速稳定性 | 可能因资源不足失败 | 更稳定 | | SDK 版本要求 | >= 0.50.0 | >= 0.52.0 | ## 配置选项 ### 交互式资源组配置(MaxQA 必选) MaxQA 模式必须指定交互式资源组名称: ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .quotaName("your_interactive_quota_nickname") // 必选 .enableMcqaV2(true) .build(); ``` ```python # PyODPS 通过 execute_sql_interactive 自动使用交互式资源组 instance = o.execute_sql_interactive('SELECT count(1) FROM my_table') ``` ### 回退策略(Fallback Policy) 当 MCQA 加速失败时,默认会回退到离线模式执行。可以通过 `fallbackPolicy` 自定义回退策略: ```java // 使用默认回退策略(加速失败时回退到离线) SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .fallbackPolicy(FallbackPolicy.alwaysFallbackPolicy()) .build(); // 禁用回退(加速失败直接报错) SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .fallbackPolicy(FallbackPolicy.neverFallbackPolicy()) .build(); ``` ```python # PyODPS 的 execute_sql_interactive 默认在加速失败时回退到离线模式 instance = o.execute_sql_interactive('SELECT * FROM my_table') # 禁用回退(加速失败直接报错) instance = o.execute_sql_interactive('SELECT * FROM my_table', fallback=False) ``` ### Session 相关配置(仅 MCQA v1) ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) // 指定 Session 名称(连接到已有 Session) .serviceName("my_session") // Session 连接超时时间(毫秒) .attachTimeout(30000L) // 启用 Session 断连后自动重连 .enableReattach(true) .build(); ``` ```python # PyODPS 自动管理 Session 生命周期,无需手动配置 instance = o.execute_sql_interactive('SELECT * FROM my_table') ``` ### 其他常用配置 ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .quotaName("your_quota") .enableMcqaV2(true) // 自定义 Tunnel Endpoint(网络受限时使用) .tunnelEndpoint("http://dt.cn-hangzhou.maxcompute.aliyun.com") // 是否使用 InstanceTunnel 获取结果(默认 true) .useInstanceTunnel(true) .build(); ``` ```python # PyODPS 通过 ODPS 对象配置 Tunnel Endpoint o = ODPS(access_id, secret_access_key, project='my_project', endpoint=endpoint, tunnel_endpoint='http://dt.cn-hangzhou.maxcompute.aliyun.com') instance = o.execute_sql_interactive('SELECT * FROM my_table') ``` ## 注意事项 1. **版本要求**:MCQA v1 需要 SDK >= 0.50.0,MaxQA 需要 SDK >= 0.52.0。低版本可能存在兼容性问题。 2. **资源组**:MaxQA 模式必须配置有效的交互式资源组(`quotaName`),否则创建 `SQLExecutor` 时会抛出异常。 3. **数据量限制**:MCQA 适用于中小数据量查询。数据量过大时可能加速失败并回退到离线模式。 4. **回退延迟**:当加速失败回退到离线时,总体延迟会增加(加速尝试时间 + 离线执行时间)。如果确定查询不适合加速,建议直接使用离线模式。 5. **Session 超时(v1)**:MCQA v1 的 Session 有超时机制,长时间未使用可能被回收。开启 `enableReattach(true)` 可自动重连。 6. **关闭 Executor**:使用完毕后务必调用 `sqlExecutor.close()` 释放资源。 ## 相关文档 - [执行 SQL 概览](./index.md) - [离线模式执行 SQL](./offline.md) - [SQLExecutor 高级用法](./sql-executor.md) - [SQLExecutor API 参考](../../reference/SQLExecutor.md) - [查询加速 MCQA(阿里云官方文档)](https://help.aliyun.com/zh/maxcompute/user-guide/maxcompute-query-acceleration) - [MaxQA(阿里云官方文档)](https://help.aliyun.com/zh/maxcompute/user-guide/maxcompute-query-acceleration2-0) --- # guides/execute-sql/offline.md # 离线模式执行 SQL 离线模式是 MaxCompute 的主要作业类型,适用于处理海量数据(GB、TB、EB 级别)的批量计算场景。本文介绍如何使用 `SQLTask` 提交离线 SQL 作业并获取结果。 ## 前置条件 - 已创建 `Odps` 客户端实例并配置好 AccessKey 和 Endpoint - 拥有目标项目的 SQL 执行权限 - Maven 依赖中已引入 `odps-sdk-core` ```xml com.aliyun.odps odps-sdk-core 0.47.0+ ``` ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.Instance; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.ResultSet; import com.aliyun.odps.task.SQLTask; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; public class OfflineSQLExample { public static void main(String[] args) throws OdpsException, IOException { // 1. 初始化 Odps 客户端 Account account = new AliyunAccount( System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")); Odps odps = new Odps(account); odps.setDefaultProject("your_project"); odps.setEndpoint("http://service.cn-hangzhou.maxcompute.aliyun.com/api"); // 2. 提交 SQL 作业 String sql = "SELECT * FROM my_table WHERE dt = '20240101';"; Instance instance = SQLTask.run(odps, sql); // 3. 获取 Logview URL(用于作业监控) String logview = odps.logview().generateLogView(instance, 7 * 24); System.out.println("Logview: " + logview); // 4. 等待作业完成 instance.waitForSuccess(); // 5. 获取结果(方式一:小结果集直接获取) List records = SQLTask.getResult(instance); for (Record record : records) { System.out.println(record); } // 6. 获取结果(方式二:大结果集使用 InstanceTunnel 迭代获取) ResultSet resultSet = SQLTask.getResultSet(instance); while (resultSet.hasNext()) { Record record = resultSet.next(); System.out.println(record); } } } ``` ```python from odps import ODPS o = ODPS(access_id, secret_access_key, project='my_project', endpoint=endpoint) # 同步执行(阻塞等待完成) instance = o.execute_sql('SELECT * FROM my_table') # 异步执行 instance = o.run_sql('SELECT * FROM my_table') instance.wait_for_success() # 获取 Logview print(o.get_logview_address(instance.id, 24)) # 读取结果 with instance.open_reader() as reader: for record in reader: print(record) # 读取为 pandas DataFrame with instance.open_reader(tunnel=True) as reader: df = reader.to_pandas() ``` ```go // 执行 SQL ins, err := odpsIns.ExecSQl("SELECT * FROM my_table;") if err != nil { panic(err) } // 等待完成 err = ins.WaitForSuccess() // 获取 Logview logview := odpsIns.LogView() logviewUrl, _ := logview.GenerateLogView(ins, 24) fmt.Println(logviewUrl) // 通过 Instance Tunnel 读取结果 project := odpsIns.DefaultProject() tunnelEndpoint, _ := project.GetTunnelEndpoint() tunnelIns := tunnel.NewTunnel(odpsIns, tunnelEndpoint) session, _ := tunnelIns.CreateInstanceResultDownloadSession(project.Name(), ins.Id()) reader, _ := session.OpenRecordReader(0, session.RecordCount(), 0, nil) reader.Iterator(func(record data.Record, err error) { fmt.Println(record) }) ``` ## 代码说明 ### 提交作业 `SQLTask.run()` 提交 SQL 作业到 MaxCompute 服务端,返回一个 `Instance` 对象代表该作业实例。该方法为异步调用,提交后立即返回。 ```java // 最简方式 Instance instance = SQLTask.run(odps, sql); // 指定项目、hints 和 aliases Instance instance = SQLTask.run(odps, project, sql, hints, aliases); // 指定 taskName(用于后续通过 taskName 获取结果) Instance instance = SQLTask.run(odps, project, sql, taskName, hints, aliases); // 指定优先级 Instance instance = SQLTask.run(odps, project, sql, taskName, hints, aliases, priority); ``` ```python # 最简方式(同步执行) instance = o.execute_sql('SELECT * FROM my_table') # 异步执行 instance = o.run_sql('SELECT * FROM my_table') instance.wait_for_success() # 带 Hints instance = o.execute_sql('SELECT * FROM my_table', hints={'odps.sql.type.system.odps2': 'true'}) ``` ```go // 最简方式 ins, err := odpsIns.ExecSQl("SELECT * FROM my_table;") err = ins.WaitForSuccess() // 带 Hints hints := map[string]string{"odps.sql.type.system.odps2": "true"} ins, err := odpsIns.ExecSQlWithHints("SELECT * FROM my_table;", hints) ``` ### 等待作业完成 `instance.waitForSuccess()` 会阻塞当前线程,直到作业成功或抛出异常(作业失败时)。 ### 获取 Logview / JobInsight URL Logview 是 MaxCompute 提供的作业监控页面,可以查看作业的执行计划、各阶段进度和资源消耗。 ```java // 生成 Logview URL,参数为有效时长(小时) String logview = odps.logview().generateLogView(instance, 7 * 24); ``` ```python # 获取 Logview URL,参数为有效时长(小时) print(o.get_logview_address(instance.id, 24)) ``` ```go // 获取 Logview URL,参数为有效时长(小时) logview := odpsIns.LogView() url, _ := logview.GenerateLogView(ins, 24) ``` ### 获取结果 SDK 提供三种获取结果的方式: | 方式 | 方法 | 特点 | |------|------|------| | 快速获取 | `SQLTask.getResult()` | 最多 1 万条,所有字段为 String 类型 | | InstanceTunnel 列表 | `SQLTask.getResultByInstanceTunnel()` | 带 Schema 信息,最多 1 万条 | | InstanceTunnel 迭代器 | `SQLTask.getResultSet()` | 分批读取,无条数限制 | ## 配置选项 ### Hints 配置 Hints 用于调整 SQL 执行行为,以键值对方式传入: ```java Map hints = new HashMap<>(); // 开启 2.0 数据类型系统 hints.put("odps.sql.type.system.odps2", "true"); // 调整 mapper split 大小 hints.put("odps.sql.mapper.split.size", "128"); // 设置动态分区最大数量 hints.put("odps.sql.reshuffle.dynamicpt", "true"); Instance instance = SQLTask.run(odps, odps.getDefaultProject(), sql, hints, null); ``` ```python hints = { 'odps.sql.type.system.odps2': 'true', 'odps.sql.mapper.split.size': '128', 'odps.sql.reshuffle.dynamicpt': 'true', } instance = o.execute_sql('SELECT * FROM my_table', hints=hints) ``` ```go hints := map[string]string{ "odps.sql.type.system.odps2": "true", "odps.sql.mapper.split.size": "128", "odps.sql.reshuffle.dynamicpt": "true", } ins, err := odpsIns.ExecSQlWithHints("SELECT * FROM my_table;", hints) ``` ### 优先级设置 优先级数字越小,优先级越高。默认优先级由项目配置决定。 ```java Integer priority = 3; Instance instance = SQLTask.run(odps, odps.getDefaultProject(), sql, "AnonymousSQLTask", null, null, priority); ``` ```python # 通过 priority 参数设置优先级 instance = o.execute_sql('SELECT * FROM my_table', priority=3) ``` ```go // Go SDK 通过 Hints 设置优先级 hints := map[string]string{"odps.instance.priority": "3"} ins, err := odpsIns.ExecSQlWithHints("SELECT * FROM my_table;", hints) ``` ### 大结果集获取(InstanceTunnel) 当结果集超过 1 万条时,需要使用 `getResultSet` 并设置 `limitHint` 为 `false` 获取全量数据: ```java // limitHint=false 表示获取全量结果(需要源表的下载权限) ResultSet resultSet = SQLTask.getResultSet(instance, "AnonymousSQLTask", null, false); while (resultSet.hasNext()) { Record record = resultSet.next(); // 处理记录 } ``` ```python # 使用 tunnel=True 获取全量结果 with instance.open_reader(tunnel=True) as reader: for record in reader: print(record) # 读取为 pandas DataFrame with instance.open_reader(tunnel=True) as reader: df = reader.to_pandas() ``` ```go // 通过 Instance Tunnel 读取全量结果 tunnelIns := tunnel.NewTunnel(odpsIns, tunnelEndpoint) session, _ := tunnelIns.CreateInstanceResultDownloadSession(projectName, ins.Id()) reader, _ := session.OpenRecordReader(0, session.RecordCount(), 0, nil) reader.Iterator(func(record data.Record, err error) { fmt.Println(record) }) ``` :::warning 当 `limitHint` 为 `false` 时,SDK 会对 SQL 涉及的每张表进行权限检查。如果项目开启了 Protection,需要提前为相应表添加 Policy Exception,否则会因权限不足而失败。 ::: ## 注意事项 1. **作业超时**:离线作业的默认超时时间由项目配置决定,长时间运行的作业建议设置合理的超时。 2. **结果生命周期**:作业完成后的结果有保留时间,过期后无法通过 InstanceTunnel 获取。 3. **并发限制**:同一项目的并发作业数有上限,大量提交时注意控制并发。 4. **taskName**:如果提交时指定了自定义 `taskName`,获取结果时也必须传入相同的 `taskName`,否则无法获取到结果。 5. **内存控制**:大结果集请使用 `getResultSet()` 迭代器方式获取,避免使用 `getResult()` 将所有数据加载到内存。 ## 相关文档 - [执行 SQL 概览](./index.md) - [MCQA 交互式查询](./mcqa.md) - [SQLExecutor 高级用法](./sql-executor.md) - [SQLExecutor API 参考](../../reference/SQLExecutor.md) - [MaxCompute SQL 概述](https://help.aliyun.com/zh/maxcompute/user-guide/overview-of-maxcompute-sql) --- # guides/execute-sql/sql-executor.md # SQLExecutor 高级用法 :::note SQLExecutor 是 Java SDK 特有的 MCQA 交互式查询接口。Python 用户请参考 [MCQA 交互式查询](./mcqa.md) 中的 Python 示例。Go SDK 暂不支持 MCQA。 ::: `SQLExecutor` 是 MaxCompute Java SDK 中统一的 SQL 执行接口,支持离线、MCQA v1、MaxQA 三种执行模式。本文介绍 `SQLExecutor` 的高级配置和使用技巧。 ## 前置条件 - 已创建 `Odps` 客户端实例并配置好 AccessKey 和 Endpoint - Maven 依赖中已引入 `odps-sdk-core >= 0.50.0` ```xml com.aliyun.odps odps-sdk-core 0.50.0+ ``` ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.Instance; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.ResultSet; import com.aliyun.odps.utils.SQLExecutorBuilder; import com.aliyun.odps.utils.SQLExecutor; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; public class SQLExecutorAdvancedExample { public static void main(String[] args) { Account account = new AliyunAccount( System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"), System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")); Odps odps = new Odps(account); odps.setDefaultProject("your_project"); odps.setEndpoint("http://service.cn-hangzhou.maxcompute.aliyun.com/api"); SQLExecutor sqlExecutor = null; try { // 使用 Builder 完整配置 sqlExecutor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .quotaName("your_interactive_quota") .enableMcqaV2(true) .useInstanceTunnel(true) .tunnelEndpoint("http://dt.cn-hangzhou.maxcompute.aliyun.com") .enableCommandApi(true) .enableOdpsNamespaceSchema(true) .build(); // 带 hints 执行查询 Map hints = new HashMap<>(); hints.put("odps.sql.type.system.odps2", "true"); sqlExecutor.run("SELECT * FROM my_table WHERE id > 100;", hints); // 获取查询元信息 System.out.println("Executor ID: " + sqlExecutor.getId()); System.out.println("Query ID: " + sqlExecutor.getQueryId()); System.out.println("Logview: " + sqlExecutor.getLogView()); System.out.println("Task Name: " + sqlExecutor.getTaskName()); // 获取执行进度 System.out.println("Progress: " + sqlExecutor.getProgress()); // 获取执行日志(如回退信息) List logs = sqlExecutor.getExecutionLog(); logs.forEach(System.out::println); // 迭代获取结果 ResultSet resultSet = sqlExecutor.getResultSet(); while (resultSet.hasNext()) { Record record = resultSet.next(); System.out.println(record); } // 使用 CommandApi 执行 DDL sqlExecutor.run("DESC my_table;", new HashMap<>()); ResultSet descResult = sqlExecutor.getResultSet(); while (descResult.hasNext()) { System.out.println(descResult.next()); } } catch (OdpsException | IOException e) { e.printStackTrace(); } finally { if (sqlExecutor != null) { sqlExecutor.close(); } } } } ``` ```python from odps import ODPS import os # 初始化 ODPS 客户端 o = ODPS( os.getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'), os.getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'), project='your_project', endpoint='http://service.cn-hangzhou.maxcompute.aliyun.com/api', ) # 带 hints 执行交互式查询 hints = {'odps.sql.type.system.odps2': 'true'} instance = o.execute_sql_interactive( 'SELECT * FROM my_table WHERE id > 100', hints=hints, ) # 获取 Logview print('Logview:', instance.get_logview_address()) # 迭代获取结果 with instance.open_reader() as reader: for record in reader: print(record) # 读取为 DataFrame with instance.open_reader(tunnel=True) as reader: df = reader.to_pandas() ``` ## 代码说明 ### Builder 配置选项 `SQLExecutorBuilder` 提供丰富的配置选项,以下按用途分类说明。 #### 基础配置 | 方法 | 说明 | 默认值 | |------|------|--------| | `odps(Odps)` | 设置 Odps 对象(必选) | - | | `executeMode(ExecuteMode)` | 执行模式:`OFFLINE` / `INTERACTIVE` | `OFFLINE` | | `taskName(String)` | 任务名称 | 按模式自动设置 | #### 结果获取配置 | 方法 | 说明 | 默认值 | |------|------|--------| | `useInstanceTunnel(boolean)` | 是否通过 InstanceTunnel 获取结果 | `true` | | `tunnelEndpoint(String)` | 指定 Tunnel Endpoint | 自动路由 | | `tunnelSocketTimeout(int)` | Tunnel 连接超时(毫秒) | SDK 默认值 | | `tunnelReadTimeout(int)` | Tunnel 读取超时(毫秒) | SDK 默认值 | | `tunnelGetResultMaxRetryTime(int)` | 获取结果最大重试次数 | SDK 默认值 | #### MCQA v1 专用配置 | 方法 | 说明 | 默认值 | |------|------|--------| | `serviceName(String)` | Session 名称 | 自动创建 | | `attachTimeout(Long)` | Session 连接超时(毫秒) | SDK 默认值 | | `enableReattach(boolean)` | Session 断连后自动重连 | `true` | | `properties(Map)` | Session 属性 | 空 | | `sessionSupportNonSelect(boolean)` | 允许执行非 SELECT 操作 | `false` | #### MaxQA 专用配置 | 方法 | 说明 | 默认值 | |------|------|--------| | `quotaName(String)` | 交互式资源组名称(必选) | - | | `enableMcqaV2(boolean)` | 启用 MaxQA | `false` | | `quota(Quota)` | 传入已获取的 Quota 实例(缓存优化) | - | | `regionId(String)` | Quota 所在 Region | 项目 Region | #### 通用高级配置 | 方法 | 说明 | 默认值 | |------|------|--------| | `fallbackPolicy(FallbackPolicy)` | 加速失败回退策略 | 默认回退 | | `enableCommandApi(boolean)` | 启用 CommandApi(DDL 支持) | `false` | | `enableOdpsNamespaceSchema(boolean)` | 启用三层模型 | `false` | | `offlineJobPriority(Integer)` | 离线作业优先级 | 项目默认 | | `recoverFrom(Instance)` | 从已有实例恢复 Executor | - | | `skipCheckIfSelect(boolean)` | 跳过 SELECT 语句校验 | `false` | ### 执行查询 #### 带 Hints 执行 ```java Map hints = new HashMap<>(); hints.put("odps.sql.type.system.odps2", "true"); hints.put("odps.sql.decimal.odps2", "true"); sqlExecutor.run("SELECT * FROM my_table;", hints); ``` ```python hints = { 'odps.sql.type.system.odps2': 'true', 'odps.sql.decimal.odps2': 'true', } instance = o.execute_sql_interactive('SELECT * FROM my_table', hints=hints) ``` #### 带 Aliases 执行 Aliases 用于在 SQL 中引用资源文件的别名,常用于 UDF 场景。Aliases 可以通过 hints 传入: ```java Map hints = new HashMap<>(); hints.put("odps.sql.udf.jars", "my_udf.jar"); sqlExecutor.run("SELECT my_udf(col) FROM my_table;", hints); ``` ```python hints = {'odps.sql.udf.jars': 'my_udf.jar'} instance = o.execute_sql_interactive( 'SELECT my_udf(col) FROM my_table', hints=hints, ) ``` ### 结果处理 #### getResultSet 迭代获取(推荐) `getResultSet()` 返回 `ResultSet` 迭代器,分批读取数据,内存友好: ```java // 基础用法 ResultSet resultSet = sqlExecutor.getResultSet(); while (resultSet.hasNext()) { Record record = resultSet.next(); // 处理 record } // 指定数量限制 ResultSet resultSet = sqlExecutor.getResultSet(1000L); // 指定 offset 和 limit ResultSet resultSet = sqlExecutor.getResultSet(0L, 5000L, null); // 获取全量结果(需要更高权限) ResultSet resultSet = sqlExecutor.getResultSet(0L, null, null, true); ``` ```python # 基础用法:迭代读取结果 with instance.open_reader() as reader: for record in reader: print(record) # 使用 Tunnel 获取结果(保留列类型信息) with instance.open_reader(tunnel=True) as reader: for record in reader: print(record) # 读取为 pandas DataFrame with instance.open_reader(tunnel=True) as reader: df = reader.to_pandas() ``` #### getResult 列表获取 :::info `getResult()` 是 Java SDK 特有的方法,一次性将结果加载到内存。Python 中请使用 `open_reader()` 迭代读取。 ::: ```java // 获取全部结果(最多受 limitEnabled 限制) List records = sqlExecutor.getResult(); // 限制数量 List records = sqlExecutor.getResult(100L); ``` :::warning `getResult()` 将所有结果一次加载到内存。结果集较大时请使用 `getResultSet()` 避免 OOM。 ::: #### 关闭 InstanceTunnel :::info `useInstanceTunnel` 是 Java SDK SQLExecutorBuilder 特有的配置。Python 中通过 `open_reader()` 的 `tunnel` 参数控制是否使用 Tunnel。 ::: ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.OFFLINE) .useInstanceTunnel(false) .build(); ``` ### 查询信息获取 ```java // 获取查询 ID(用于问题排查) String queryId = sqlExecutor.getQueryId(); // 获取 Logview URL(有效期 7 天) String logview = sqlExecutor.getLogView(); // 获取作业 Instance Instance instance = sqlExecutor.getInstance(); // 获取执行进度 List progress = sqlExecutor.getProgress(); // 获取执行日志(回退信息等) List logs = sqlExecutor.getExecutionLog(); // 获取作业摘要 String summary = sqlExecutor.getSummary(); // 判断是否有结果集 boolean hasResult = sqlExecutor.hasResultSet(); // 判断是否在交互模式中执行 boolean isInteractive = sqlExecutor.isRunningInInteractiveMode(); ``` ```python # 获取 Logview URL logview = instance.get_logview_address() # 获取 Instance ID(用于问题排查) instance_id = instance.id # 获取作业状态 status = instance.status print('Status:', status) # 判断是否成功 print('Success:', instance.is_successful()) ``` ### 取消查询 ```java try { sqlExecutor.run("SELECT * FROM large_table;", new HashMap<>()); // 在另一个线程中取消 sqlExecutor.cancel(); } catch (OdpsException e) { // 处理取消后的异常 } ``` ```python # 提交查询(不等待完成) instance = o.run_sql_interactive('SELECT * FROM large_table') # 取消查询 instance.stop() ``` ### CommandApi(DDL 命令) :::info CommandApi 是 Java SDK SQLExecutor 特有的功能。Python 中可直接使用 `o.execute_sql()` 执行 DDL 命令。 ::: 启用 `enableCommandApi(true)` 后,可以通过 `SQLExecutor` 执行 DDL 类命令: ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .quotaName("your_quota") .enableMcqaV2(true) .enableCommandApi(true) .build(); // DESC TABLE executor.run("DESC my_table;", new HashMap<>()); ResultSet descResult = executor.getResultSet(); while (descResult.hasNext()) { System.out.println(descResult.next()); } // SHOW TABLES executor.run("SHOW TABLES;", new HashMap<>()); ResultSet showResult = executor.getResultSet(); while (showResult.hasNext()) { System.out.println(showResult.next()); } ``` :::note 开启 CommandApi 后,部分扩展命令的行为可能与未开启时不一致。MaxCompute SQL 本身也在逐步支持类似扩展命令,请根据实际需要决定是否开启。 ::: ## 配置选项 ### 回退策略详解 `FallbackPolicy` 控制 MCQA 加速失败时的行为: ```java // 始终回退到离线(默认行为) .fallbackPolicy(FallbackPolicy.alwaysFallbackPolicy()) // 永不回退(加速失败直接报错) .fallbackPolicy(FallbackPolicy.neverFallbackPolicy()) ``` ```python # 默认行为:加速失败时回退到离线 instance = o.execute_sql_interactive('SELECT * FROM my_table', fallback=True) # 禁用回退(加速失败直接报错) instance = o.execute_sql_interactive('SELECT * FROM my_table', fallback=False) ``` 回退策略仅对 MCQA(v1 和 v2)模式生效。当回退发生时,可以通过 `getExecutionLog()` 查看回退原因。 ### InstanceTunnel 配置 :::info InstanceTunnel 精细配置是 Java SDK SQLExecutorBuilder 特有的功能。Python 中通过 ODPS 对象的 `tunnel_endpoint` 参数统一配置。 ::: 当网络环境受限或需要性能优化时,可以精细配置 Tunnel 参数: ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.OFFLINE) // 指定 Tunnel Endpoint(自动路由不可用时) .tunnelEndpoint("http://dt.cn-hangzhou.maxcompute.aliyun.com") // 连接超时 30 秒 .tunnelSocketTimeout(30000) // 读取超时 60 秒 .tunnelReadTimeout(60000) // 最大重试 3 次 .tunnelGetResultMaxRetryTime(3) .build(); ``` ### 从已有实例恢复 :::info 从已有实例恢复 SQLExecutor 是 Java SDK 特有的功能,用于断点续取结果。 ::: 可以从之前的 Instance 恢复 `SQLExecutor`,用于断点续取结果: ```java // 对于 MCQA:恢复到已有 Session Instance sessionInstance = ...; // 之前保存的 Session Instance SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE) .recoverFrom(sessionInstance) .build(); // 对于离线:恢复到已有 SQLTask Instance Instance taskInstance = ...; // 之前保存的 SQLTask Instance SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.OFFLINE) .recoverFrom(taskInstance) .build(); ``` ## 注意事项 1. **资源释放**:使用完毕后务必调用 `close()` 释放资源。推荐使用 try-finally 模式。 2. **线程安全**:`SQLExecutor` 不是线程安全的,不要在多线程间共享同一个实例。多线程场景请使用 `SQLExecutorPool`。 3. **InstanceTunnel 权限**:当 `useInstanceTunnel(true)` 且获取全量结果时,用户需要具有 SQL 涉及源表的下载权限。 4. **CommandApi 兼容性**:开启 CommandApi 后部分命令行为与 SQL 模式不一致,生产环境建议充分测试。 5. **skipCheckIfSelect**:开启后会跳过 SELECT 语句校验以提升性能,但处理非查询语句时延时会变长,建议仅在请求主要为查询语句的场景使用。 6. **Logview 有效期**:通过 `getLogView()` 获取的 Logview 默认有效期为 7 天。生成失败时返回 null 而非抛出异常。 7. **getResultSet 阻塞**:`getResultSet()` 是同步接口,会阻塞当前线程直到查询完成或失败。 ## 相关文档 - [执行 SQL 概览](./index.md) - [离线模式执行 SQL](./offline.md) - [MCQA 交互式查询](./mcqa.md) - [SQLExecutor API 参考](../../reference/SQLExecutor.md) --- # guides/manage-resources/functions.md # 函数管理 MaxCompute 支持用户自定义函数(UDF),包括 UDF(标量函数)、UDTF(表值函数)和 UDAF(聚合函数)。通过 Java SDK 的 `Functions` 接口,可以对函数进行创建、查询、更新和删除等操作。 ## 前置条件 - 已完成 [认证配置](../../getting-started/authentication.md) - 已添加 `odps-sdk-core` 依赖 - 已将 UDF 对应的 JAR 包上传为资源(参见 [资源管理](./resources.md)) ## 完整示例 以下示例演示如何从已上传的 JAR 资源创建一个 UDF 函数,并对其进行查询和删除操作: ```java import com.aliyun.odps.Function; import com.aliyun.odps.Functions; import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.AliyunAccount; import java.util.Arrays; import java.util.Iterator; public class FunctionManagementExample { public static void main(String[] args) throws OdpsException { // 初始化 Odps 客户端 Odps odps = new Odps(new AliyunAccount("accessId", "accessKey")); odps.setDefaultProject("my_project"); odps.setEndpoint("http://service.odps.aliyun.com/api"); Functions functions = odps.functions(); // 1. 创建函数 Function function = new Function(); function.setName("my_upper_udf"); function.setClassPath("com.example.udf.UpperCase"); function.setResources(Arrays.asList("my-udf-lib.jar")); functions.create(function); System.out.println("函数创建成功: my_upper_udf"); // 2. 检查函数是否存在 boolean exists = functions.exists("my_upper_udf"); System.out.println("函数是否存在: " + exists); // 3. 获取函数详情 Function loaded = functions.get("my_upper_udf"); loaded.reload(); System.out.println("函数类路径: " + loaded.getClassPath()); System.out.println("关联资源: " + loaded.getResourceNames()); // 4. 遍历所有函数 System.out.println("项目中的所有函数:"); for (Function f : functions.iterable()) { System.out.println(" - " + f.getName()); } // 5. 更新函数(例如更换 JAR 资源版本) Function updated = new Function(); updated.setName("my_upper_udf"); updated.setClassPath("com.example.udf.UpperCase"); updated.setResources(Arrays.asList("my-udf-lib-v2.jar")); functions.update(updated); System.out.println("函数更新成功"); // 6. 删除函数 functions.delete("my_upper_udf"); System.out.println("函数删除成功"); } } ``` ```python from odps import ODPS # 初始化 ODPS 客户端 odps = ODPS('access_id', 'access_key', 'my_project', 'http://service.odps.aliyun.com/api') # 1. 创建函数 odps.create_function( 'my_upper_udf', class_type='com.example.udf.UpperCase', resources=['my-udf-lib.jar'] ) print('函数创建成功: my_upper_udf') # 2. 检查函数是否存在 exists = odps.exist_function('my_upper_udf') print('函数是否存在:', exists) # 3. 获取函数详情 func = odps.get_function('my_upper_udf') func.reload() print('函数类路径:', func.class_type) print('关联资源:', [r.name for r in func.resources]) # 4. 遍历所有函数 print('项目中的所有函数:') for f in odps.list_functions(): print(f' - {f.name}') # 5. 更新函数(例如更换 JAR 资源版本) func = odps.get_function('my_upper_udf') func.resources = ['my-udf-lib-v2.jar'] func.update() print('函数更新成功') # 6. 删除函数 odps.delete_function('my_upper_udf') print('函数删除成功') ``` ```go package main import ( "fmt" "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" ) func main() { // 初始化 ODPS 客户端 acc := account.NewAliyunAccount("accessId", "accessKey") odpsIns := odps.NewOdps(acc, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("my_project") functions := odps.NewFunctions(odpsIns) // 1. 创建函数 fb := odps.NewFunctionBuilder() function := fb.Name("my_upper_udf"). ClassPath("com.example.udf.UpperCase"). Resources([]string{"my-udf-lib.jar"}). Build() err := functions.Create("", "", function) if err != nil { panic(err) } fmt.Println("函数创建成功: my_upper_udf") // 2. 检查函数是否存在 f, _ := functions.Get("my_upper_udf") exists, err := f.Exist() if err != nil { panic(err) } fmt.Println("函数是否存在:", exists) // 3. 获取函数详情 f, _ = functions.Get("my_upper_udf") err = f.Load() if err != nil { panic(err) } fmt.Println("函数类路径:", f.ClassPath()) fmt.Println("关联资源:", f.Resources()) // 4. 更新函数(例如更换 JAR 资源版本) f.SetClassPath("com.example.udf.UpperCase") f.SetResources([]string{"my-udf-lib-v2.jar"}) err = functions.Update("", "", *f) if err != nil { panic(err) } fmt.Println("函数更新成功") // 5. 删除函数 err = functions.Delete("my_upper_udf") if err != nil { panic(err) } fmt.Println("函数删除成功") } ``` :::info Go SDK 暂不支持遍历函数列表,如需遍历请使用 Java 或 Python SDK。 ::: ## 代码说明 ### 创建函数 创建函数时需要指定三个关键属性: - `name`:函数名称,在项目中唯一 - `classPath`:UDF 实现类的全限定类名 - `resources`:函数依赖的资源列表(通常是 JAR 包资源名) ```java Function function = new Function(); function.setName("my_upper_udf"); function.setClassPath("com.example.udf.UpperCase"); function.setResources(Arrays.asList("my-udf-lib.jar")); functions.create(function); ``` ```python odps.create_function( 'my_upper_udf', class_type='com.example.udf.UpperCase', resources=['my-udf-lib.jar'] ) ``` ```go fb := odps.NewFunctionBuilder() function := fb.Name("my_upper_udf"). ClassPath("com.example.udf.UpperCase"). Resources([]string{"my-udf-lib.jar"}). Build() functions.Create("", "", function) ``` 支持在指定项目或 schema 中创建: ```java // 在指定项目中创建 functions.create("target_project", function); // 在指定项目和 schema 中创建 functions.create("target_project", "my_schema", function); ``` ```python # 在指定项目中创建 odps.create_function('my_upper_udf', class_type='com.example.udf.UpperCase', resources=['my-udf-lib.jar'], project='target_project') # 在指定项目和 schema 中创建 odps.create_function('my_upper_udf', class_type='com.example.udf.UpperCase', resources=['my-udf-lib.jar'], project='target_project', schema='my_schema') ``` ```go // 在指定项目中创建 functions.Create("target_project", "", function) // 在指定项目和 schema 中创建 functions.Create("target_project", "my_schema", function) ``` ### 获取函数 ```java // 从默认项目获取 Function f = functions.get("function_name"); // 从指定项目获取 Function f = functions.get("project_name", "function_name"); // 从指定项目和 schema 获取 Function f = functions.get("project_name", "schema_name", "function_name"); ``` ```python # 从默认项目获取 f = odps.get_function('function_name') # 从指定项目获取 f = odps.get_function('function_name', project='project_name') # 从指定项目和 schema 获取 f = odps.get_function('function_name', project='project_name', schema='schema_name') ``` ```go // 从默认项目获取 f, err := functions.Get("function_name") if err != nil { panic(err) } // 加载函数详情 err = f.Load() ``` :::info Go SDK 的 `Functions.Get` 仅支持获取当前项目中的函数。如需操作其他项目,请创建对应项目的 `Functions` 实例。 ::: 获取到的 Function 对象是延迟加载的,访问详细属性前需要调用 `reload()`。 ### 遍历函数 ```java // 迭代器方式 Iterator it = functions.iterator(); while (it.hasNext()) { Function f = it.next(); System.out.println(f.getName()); } // for-each 方式 for (Function f : functions.iterable()) { System.out.println(f.getName()); } ``` ```python # 遍历所有函数 for f in odps.list_functions(): print(f.name) # 按前缀过滤 for f in odps.list_functions(prefix='my_'): print(f.name) ``` :::info Go SDK 暂不支持遍历函数列表,如需遍历请使用 Java 或 Python SDK。 ::: ### 删除函数 ```java // 删除默认项目中的函数 functions.delete("function_name"); // 删除指定项目中的函数 functions.delete("project_name", "function_name"); // 删除指定项目和 schema 中的函数 functions.delete("project_name", "schema_name", "function_name"); ``` ```python # 删除默认项目中的函数 odps.delete_function('function_name') # 删除指定项目中的函数 odps.delete_function('function_name', project='project_name') # 删除指定项目和 schema 中的函数 odps.delete_function('function_name', project='project_name', schema='schema_name') ``` ```go // 删除默认项目中的函数 err := functions.Delete("function_name") ``` :::info Go SDK 的 `Functions.Delete` 仅支持删除当前项目中的函数。如需操作其他项目,请创建对应项目的 `Functions` 实例。 ::: ## 函数类型 MaxCompute 支持三种自定义函数类型: | 类型 | 基类 | 说明 | 输入/输出 | |------|------|------|-----------| | UDF | `com.aliyun.odps.udf.UDF` | 标量函数 | 一行输入,一个值输出 | | UDTF | `com.aliyun.odps.udf.UDTF` | 表值函数 | 一行输入,多行输出 | | UDAF | `com.aliyun.odps.udf.Aggregator` | 聚合函数 | 多行输入,一个值输出 | 三种类型的函数在注册时使用相同的 API,区别仅在于 `classPath` 指向的实现类不同。 ## 配置选项 ### Function 属性 | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | name | String | Y | 函数名称 | | classPath | String | Y | 实现类的全限定名 | | resources | List\ | Y | 依赖的资源名称列表 | | comment | String | N | 函数描述信息 | ## 注意事项 - 创建函数前,必须先将对应的 JAR/PY 等资源上传到同一项目中(参见 [资源管理](./resources.md)) - 函数名称在同一项目同一 schema 下唯一,重复创建会抛出 `OdpsException` - 更新函数时需要重新指定所有属性(包括 classPath 和 resources),不支持部分更新 - 删除正在被 SQL 作业引用的函数可能导致作业失败 - 函数的 `resources` 列表中可包含多个资源,适用于 UDF 依赖多个 JAR 包的场景 ## 相关文档 - [资源管理](./resources.md) - 上传和管理函数依赖的资源文件 - [UDF 函数开发与使用](./functions.md) - UDF 开发完整流程 - [Functions API 参考](../../reference/Functions.md) - Functions 类完整 API --- # guides/manage-resources/resources.md # 资源管理 MaxCompute 中的资源是 UDF 函数执行时依赖的文件,包括 JAR 包、Python 脚本、数据文件等。通过 Java SDK 的 `Resources` 接口,可以对资源进行上传、下载、更新和删除操作。 ## 前置条件 - 已完成 [认证配置](../../getting-started/authentication.md) - 已添加 `odps-sdk-core` 依赖 ## 完整示例 以下示例演示如何上传 JAR 文件作为资源,并对其进行查询和删除操作: ```java import com.aliyun.odps.FileResource; import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.Resource; import com.aliyun.odps.Resources; import com.aliyun.odps.TableResource; import com.aliyun.odps.account.AliyunAccount; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class ResourceManagementExample { public static void main(String[] args) throws OdpsException, IOException { // 初始化 Odps 客户端 Odps odps = new Odps(new AliyunAccount("accessId", "accessKey")); odps.setDefaultProject("my_project"); odps.setEndpoint("http://service.odps.aliyun.com/api"); Resources resources = odps.resources(); // 1. 上传 JAR 文件资源 FileResource jarResource = new FileResource(); jarResource.setName("my-udf-lib.jar"); jarResource.setComment("UDF library for string operations"); try (InputStream is = new FileInputStream("/path/to/my-udf-lib.jar")) { resources.create(jarResource, is); System.out.println("JAR 资源上传成功: my-udf-lib.jar"); } // 2. 检查资源是否存在 boolean exists = resources.exists("my-udf-lib.jar"); System.out.println("资源是否存在: " + exists); // 3. 获取资源元信息 Resource loaded = resources.get("my-udf-lib.jar"); loaded.reload(); System.out.println("资源类型: " + loaded.getType()); System.out.println("资源大小: " + loaded.getSize()); System.out.println("最后修改: " + loaded.getLastModifiedTime()); // 4. 下载资源内容 try (InputStream is = resources.getResourceAsStream("my-udf-lib.jar")) { byte[] buffer = new byte[1024]; int bytesRead; long totalBytes = 0; while ((bytesRead = is.read(buffer)) != -1) { totalBytes += bytesRead; } System.out.println("下载资源大小: " + totalBytes + " bytes"); } // 5. 遍历所有资源 System.out.println("项目中的所有资源:"); for (Resource r : resources.iterable()) { System.out.println(" - " + r.getName() + " (" + r.getType() + ")"); } // 6. 更新资源 FileResource updatedResource = new FileResource(); updatedResource.setName("my-udf-lib.jar"); try (InputStream is = new FileInputStream("/path/to/my-udf-lib-v2.jar")) { resources.update(updatedResource, is); System.out.println("资源更新成功"); } // 7. 删除资源 resources.delete("my-udf-lib.jar"); System.out.println("资源删除成功"); } } ``` ```python from odps import ODPS # 初始化 ODPS 客户端 odps = ODPS('access_id', 'access_key', 'my_project', 'http://service.odps.aliyun.com/api') # 1. 上传 JAR 文件资源 odps.create_resource('my-udf-lib.jar', 'jar', fileobj=open('/path/to/my-udf-lib.jar', 'rb')) print('JAR 资源上传成功: my-udf-lib.jar') # 2. 检查资源是否存在 exists = odps.exist_resource('my-udf-lib.jar') print('资源是否存在:', exists) # 3. 获取资源元信息 res = odps.get_resource('my-udf-lib.jar') res.reload() print('资源类型:', res.type) print('资源大小:', res.size) print('最后修改:', res.last_modified_time) # 4. 下载资源内容 with odps.open_resource('my-udf-lib.jar', mode='rb', type='jar') as fp: content = fp.read() print('下载资源大小:', len(content), 'bytes') # 5. 遍历所有资源 print('项目中的所有资源:') for r in odps.list_resources(): print(f' - {r.name} ({r.type})') # 6. 更新资源 res = odps.get_resource('my-udf-lib.jar') res.update(file_obj=open('/path/to/my-udf-lib-v2.jar', 'rb')) print('资源更新成功') # 7. 删除资源 odps.delete_resource('my-udf-lib.jar') print('资源删除成功') ``` ```go package main import ( "fmt" "os" "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" ) func main() { // 初始化 ODPS 客户端 acc := account.NewAliyunAccount("accessId", "accessKey") odpsIns := odps.NewOdps(acc, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("my_project") resources := odps.NewResources(odpsIns) // 1. 上传 JAR 文件资源 jarRes := odps.NewJarResource("my-udf-lib.jar") jarRes.SetComment("UDF library for string operations") f, _ := os.Open("/path/to/my-udf-lib.jar") defer f.Close() jarRes.SetReader(f) err := resources.CreateFileResource("", "", jarRes, false) if err != nil { panic(err) } fmt.Println("JAR 资源上传成功: my-udf-lib.jar") // 2. 检查资源是否存在 res := resources.Get("my-udf-lib.jar") exists, err := res.Exist() if err != nil { panic(err) } fmt.Println("资源是否存在:", exists) // 3. 获取资源元信息 res = resources.Get("my-udf-lib.jar") err = res.Load() if err != nil { panic(err) } fmt.Println("资源类型:", res.ResourceType()) fmt.Println("资源描述:", res.Comment()) // 4. 遍历所有资源 fmt.Println("项目中的所有资源:") resources.List(func(r *odps.Resource, err error) { if err != nil { return } fmt.Printf(" - %s (%s)\n", r.Name(), r.ResourceType()) }) // 5. 更新资源 updatedRes := odps.NewJarResource("my-udf-lib.jar") f2, _ := os.Open("/path/to/my-udf-lib-v2.jar") defer f2.Close() updatedRes.SetReader(f2) err = resources.UpdateFileResource("", "", updatedRes) if err != nil { panic(err) } fmt.Println("资源更新成功") // 6. 删除资源 err = resources.Delete("my-udf-lib.jar") if err != nil { panic(err) } fmt.Println("资源删除成功") } ``` :::info Go SDK 暂不支持下载资源内容(`getResourceAsStream`),如需下载资源请使用 Java 或 Python SDK。 ::: ## 代码说明 ### 上传文件资源 文件类型资源(JAR/FILE/ARCHIVE/PY)通过 `InputStream` 上传: ```java FileResource resource = new FileResource(); resource.setName("example.jar"); try (InputStream is = new FileInputStream("local/path/example.jar")) { resources.create(resource, is); } ``` ```python odps.create_resource('example.jar', 'jar', fileobj=open('local/path/example.jar', 'rb')) ``` ```go jarRes := odps.NewJarResource("example.jar") f, _ := os.Open("local/path/example.jar") defer f.Close() jarRes.SetReader(f) err := resources.CreateFileResource("", "", jarRes, false) ``` MaxCompute 会根据文件后缀名自动识别资源类型。也可以显式设置类型: ```java resource.setType(Resource.Type.JAR); // JAR 资源 resource.setType(Resource.Type.FILE); // 普通文件资源 resource.setType(Resource.Type.ARCHIVE);// 压缩包资源 resource.setType(Resource.Type.PY); // Python 脚本资源 ``` ```python # 通过 type 参数指定资源类型 odps.create_resource('example.jar', 'jar', fileobj=fp) # JAR 资源 odps.create_resource('example.txt', 'file', fileobj=fp) # 普通文件资源 odps.create_resource('example.zip', 'archive', fileobj=fp) # 压缩包资源 odps.create_resource('example.py', 'py', fileobj=fp) # Python 脚本资源 ``` ```go // 通过不同的构造函数指定资源类型 jarRes := odps.NewJarResource("example.jar") // JAR 资源 fileRes := odps.NewFileResource("example.txt") // 普通文件资源 archiveRes := odps.NewArchiveResource("example.zip") // 压缩包资源 pyRes := odps.NewPyResource("example.py") // Python 脚本资源 ``` 支持在指定项目或 schema 中创建: ```java // 在指定项目中创建 resources.create("target_project", resource, inputStream); // 在指定项目和 schema 中创建 resources.create("target_project", "my_schema", resource, inputStream); ``` ```python # 在指定项目中创建 odps.create_resource('example.jar', 'jar', fileobj=fp, project='target_project') # 在指定项目和 schema 中创建 odps.create_resource('example.jar', 'jar', fileobj=fp, project='target_project', schema='my_schema') ``` ```go // 在指定项目中创建 resources.CreateFileResource("target_project", "", jarRes, false) // 在指定项目和 schema 中创建 resources.CreateFileResource("target_project", "my_schema", jarRes, false) ``` ### 创建表资源 表资源引用项目中的已有表,不需要 `InputStream`: ```java TableResource tableResource = new TableResource(); tableResource.setName("my_table_resource"); tableResource.setProject("source_project"); tableResource.setTable("source_table"); // 可选:指定分区 tableResource.setPartition("pt='20240101'"); resources.create(tableResource); ``` ```python odps.create_resource( 'my_table_resource', 'table', project_name='source_project', table_name='source_table', partition="pt='20240101'" # 可选:指定分区 ) ``` ```go tableRes := odps.NewTableResource("source_project", "source_table", "pt='20240101'") tableRes.SetName("my_table_resource") err := resources.CreateTableResource("", "", tableRes, false) ``` ### 下载资源 通过 `getResourceAsStream` 方法获取资源的输入流: ```java try (InputStream is = resources.getResourceAsStream("resource_name")) { // 处理输入流 } // 指定项目和 schema try (InputStream is = resources.getResourceAsStream("project", "schema", "resource_name")) { // 处理输入流 } ``` ```python with odps.open_resource('resource_name', mode='rb') as fp: content = fp.read() # 指定项目和 schema with odps.open_resource('resource_name', mode='rb', project='project', schema='schema') as fp: content = fp.read() ``` :::info Go SDK 暂不支持下载资源内容,如需下载资源请使用 Java 或 Python SDK。 ::: ### 遍历资源 ```java // 迭代器方式 Iterator it = resources.iterator(); while (it.hasNext()) { Resource r = it.next(); System.out.println(r.getName() + " : " + r.getType()); } // for-each 方式 for (Resource r : resources.iterable()) { System.out.println(r.getName()); } ``` ```python # 遍历所有资源 for r in odps.list_resources(): print(r.name, ':', r.type) # 按前缀过滤 for r in odps.list_resources(prefix='my-udf'): print(r.name) ``` ```go // 回调方式遍历 resources.List(func(r *odps.Resource, err error) { if err != nil { return } fmt.Println(r.Name(), ":", r.ResourceType()) }) // 按前缀过滤 resources.List(func(r *odps.Resource, err error) { if err != nil { return } fmt.Println(r.Name()) }, odps.ResourceFilter.NamePrefix("my-udf")) ``` ### 删除资源 ```java // 删除默认项目中的资源 resources.delete("resource_name"); // 删除指定项目中的资源 resources.delete("project_name", "resource_name"); // 删除指定项目和 schema 中的资源 resources.delete("project_name", "schema_name", "resource_name"); ``` ```python # 删除默认项目中的资源 odps.delete_resource('resource_name') # 删除指定项目中的资源 odps.delete_resource('resource_name', project='project_name') # 删除指定项目和 schema 中的资源 odps.delete_resource('resource_name', project='project_name', schema='schema_name') ``` ```go // 删除默认项目中的资源 err := resources.Delete("resource_name") ``` :::info Go SDK 的 `Resources.Delete` 仅支持删除当前项目中的资源。如需操作其他项目,请创建对应项目的 `Resources` 实例。 ::: ## 资源类型 | 类型 | 后缀名 | 说明 | 用途 | |------|---------|------|------| | JAR | `.jar` | Java 归档文件 | UDF/UDTF/UDAF 的实现代码 | | FILE | 任意 | 普通文件 | UDF 运行时读取的配置或数据文件 | | PY | `.py` | Python 脚本 | Python UDF 的实现代码 | | ARCHIVE | `.zip`/`.tar.gz`/`.tgz` | 压缩包 | 包含多个文件的归档,运行时自动解压 | | TABLE | - | 表资源 | 引用项目中的表数据,供 UDF 运行时读取 | ## 配置选项 ### FileResource 属性 | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | name | String | Y | 资源名称(包含后缀) | | type | Resource.Type | N | 资源类型,默认根据后缀名推断 | | comment | String | N | 资源描述信息 | ### TableResource 属性 | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | name | String | Y | 资源名称 | | project | String | Y | 源表所在的项目名 | | table | String | Y | 源表名称 | | partition | String | N | 分区规格(如 `pt='20240101'`) | ## 注意事项 - 单个资源文件大小限制为 500 MB,超出需要使用 ARCHIVE 类型拆分上传 - 资源名称在同一项目同一 schema 下唯一,重复创建会抛出异常 - 删除已被函数引用的资源会导致对应函数不可用 - 上传资源时,`InputStream` 会被完整读取,确保网络连接稳定 - 更新资源时需要重新上传完整文件,不支持增量更新 - 表资源引用的表必须在同一项目或已通过 Package 授权的项目中 ## 相关文档 - [函数管理](./functions.md) - 基于资源创建和管理自定义函数 - [UDF 函数开发与使用](./functions.md) - UDF 开发完整流程 - [Resources API 参考](../../reference/Resources.md) - Resources 类完整 API --- # guides/manage-tables/alter-table.md # 修改表 本文介绍如何使用 `Table` 对象对已有表进行修改操作,包括重命名、修改生命周期、增删列、更改列类型等。 ## 前置条件 所有修改操作都需要先获取 `Table` 实例: ```java Odps odps = new Odps(...); Table table = odps.tables().get("project_name", "table_name"); ``` ```python from odps import ODPS odps = ODPS(...) table = odps.get_table('table_name', project='project_name') ``` ```go odpsIns := odps.NewOdps(account, endpoint) odpsIns.SetDefaultProjectName("project_name") table := odpsIns.Tables().Get("table_name") ``` ## 重命名表 ```java public void rename(String newName) throws Exception ``` ```python table.rename(new_name) ``` ```go func (t *Table) Rename(newName string) error ``` 将表重命名为新名称,新名称必须符合 MaxCompute 命名规则。 ```java table.rename("new_table_name"); ``` ```python table.rename('new_table_name') ``` ```go err := table.Rename("new_table_name") ``` ## 修改生命周期 ```java public void setLifeCycle(int days) throws OdpsException ``` ```python table.set_lifecycle(days) ``` ```go func (t *Table) SetLifeCycle(days int) error ``` 修改表的生命周期,单位为天,必须为正整数。 ```java // 设置生命周期为 90 天 table.setLifeCycle(90); ``` ```python # 设置生命周期为 90 天 table.set_lifecycle(90) ``` ```go // 设置生命周期为 90 天 err := table.SetLifeCycle(90) ``` ## 更改表所有者 ```java public void changeOwner(String newOwner) throws OdpsException ``` ```python table.set_owner(new_owner) ``` ```go func (t *Table) ChangeOwner(newOwner string) error ``` 更改表的所有者。只有项目所有者或具备超级管理角色的用户可以执行此操作。 ```java table.changeOwner("new_owner_id"); ``` ```python table.set_owner('new_owner_id') ``` ```go err := table.ChangeOwner("new_owner_id") ``` ## 修改表注释 ```java public void changeComment(String newComment) throws OdpsException ``` ```python table.set_comment(new_comment) ``` ```go func (t *Table) ChangeComment(newComment string) error ``` 修改表的注释内容,可以传入空字符串来清空注释。 ```java table.changeComment("这是一张用户行为日志表"); ``` ```python table.set_comment('这是一张用户行为日志表') ``` ```go err := table.ChangeComment("这是一张用户行为日志表") ``` ## 更新时间戳 ```java public void touch() throws OdpsException ``` ```python table.touch() ``` ```go func (t *Table) Touch() error ``` 将表的最后修改时间更新为当前时间。可用于延长表在生命周期管理下的存活时间。 ```java table.touch(); ``` ```python table.touch() ``` ```go err := table.Touch() ``` ## 添加列 ```java public void addColumns(List columns, boolean ifNotExists) throws Exception ``` ```python table.add_columns(columns, if_not_exists=False) ``` ```go func (t *Table) AddColumns(columns []tableschema.Column, ifNotExists bool) error ``` 向表中添加新列。 **参数说明** - `columns`:要添加的列列表 - `ifNotExists`:为 `true` 时,列已存在不会抛出异常 ```java List newColumns = List.of( Column.newBuilder("email", TypeInfoFactory.STRING).withComment("邮箱地址").build(), Column.newBuilder("age", TypeInfoFactory.INT).build() ); table.addColumns(newColumns, true); ``` ```python from odps.types import Column table.add_columns([ Column('email', 'string', comment='邮箱地址'), Column('age', 'int'), ], if_not_exists=True) ``` ```go columns := []tableschema.Column{ {Name: "email", Type: datatype.StringType, Comment: "邮箱地址"}, {Name: "age", Type: datatype.IntType}, } err := table.AddColumns(columns, true) ``` ## 删除列 ```java public void dropColumns(List columnNames) throws Exception ``` ```python table.delete_columns(columns) ``` ```go func (t *Table) DropColumns(columnNames []string) error ``` 从表中删除指定列。 ```java List columnsToDrop = List.of("temp_col", "deprecated_col"); table.dropColumns(columnsToDrop); ``` ```python table.delete_columns(['temp_col', 'deprecated_col']) ``` ```go err := table.DropColumns([]string{"temp_col", "deprecated_col"}) ``` ## 更改列类型 ```java public void alterColumnType(String columnName, TypeInfo columnType) throws Exception ``` ```python # Python SDK 暂不提供直接的更改列类型方法,可通过 execute_sql 执行 DDL odps.execute_sql('ALTER TABLE table_name CHANGE COLUMN col_name col_name new_type;') ``` ```go func (t *Table) AlterColumnType(columnName string, columnType datatype.DataType) error ``` 更改表中已有列的数据类型。注意只支持兼容的类型转换。 ```java // 将 age 列从 INT 改为 BIGINT table.alterColumnType("age", TypeInfoFactory.BIGINT); // 将 score 列改为 DECIMAL table.alterColumnType("score", TypeInfoFactory.DECIMAL); ``` ```python # Python SDK 暂不提供直接的更改列类型方法,可通过 execute_sql 执行 DDL odps.execute_sql('ALTER TABLE my_table CHANGE COLUMN age age BIGINT;') odps.execute_sql('ALTER TABLE my_table CHANGE COLUMN score score DECIMAL;') ``` ```go // 将 age 列从 INT 改为 BIGINT err := table.AlterColumnType("age", datatype.BigIntType) // 将 score 列改为 DECIMAL err = table.AlterColumnType("score", datatype.NewDecimalType(38, 18)) ``` ## 更改列名 ```java public void changeColumnName(String oldColumnName, String newColumnName) throws Exception ``` ```python table.rename_column(old_column_name, new_column_name) ``` ```go func (t *Table) ChangeColumnName(oldColumnName string, newColumnName string) error ``` 修改表中某一列的名称。 ```java table.changeColumnName("old_column_name", "new_column_name"); ``` ```python table.rename_column('old_column_name', 'new_column_name') ``` ```go err := table.ChangeColumnName("old_column_name", "new_column_name") ``` ## 更改聚簇信息 ```java public void changeClusterInfo(ClusterInfo clusterInfo) throws OdpsException ``` ```python table.set_cluster_info(new_cluster_info) ``` ```go func (t *Table) ChangeClusterInfo(clusterInfo tableschema.ClusterInfo) error ``` 修改表的聚簇(Cluster)信息,可以变更聚簇列、排序方式或桶数量。 ```java Table.ClusterInfo newClusterInfo = new Table.ClusterInfo( Table.ClusterInfo.ClusterType.HASH, List.of("user_id"), List.of(new Table.SortColumn("user_id", Table.SortColumn.Order.ASC)), 32 ); table.changeClusterInfo(newClusterInfo); ``` ```python from odps.models.cluster_info import ClusterInfo, ClusterType, ClusterSortCol, ClusterSortOrder cluster_info = ClusterInfo( cluster_type=ClusterType.HASH, cluster_cols=['user_id'], sort_cols=[ClusterSortCol(name='user_id', order=ClusterSortOrder.ASC)], bucket_num=32, ) table.set_cluster_info(cluster_info) ``` ```go clusterInfo := tableschema.ClusterInfo{ ClusterType: tableschema.CLUSTER_TYPE.Hash, ClusterCols: []string{"user_id"}, SortCols: []tableschema.SortColumn{{Name: "user_id", Order: tableschema.SORT_ORDER.ASC}}, BucketNum: 32, } err := table.ChangeClusterInfo(clusterInfo) ``` ## 完整示例 以下示例展示了对表进行一系列修改操作的典型流程: ```java public void alterTableExample(Odps odps) throws Exception { Table table = odps.tables().get("my_project", "user_events"); // 1. 修改表注释 table.changeComment("用户事件表 - 记录用户行为日志"); // 2. 设置生命周期为 180 天 table.setLifeCycle(180); // 3. 添加新列 List newColumns = List.of( Column.newBuilder("device_type", TypeInfoFactory.STRING).withComment("设备类型").build(), Column.newBuilder("app_version", TypeInfoFactory.STRING).withComment("应用版本").build() ); table.addColumns(newColumns, true); // 4. 重命名列 table.changeColumnName("device_type", "device_category"); // 5. 更新时间戳 table.touch(); } ``` ```python from odps.types import Column table = odps.get_table('user_events', project='my_project') # 1. 修改表注释 table.set_comment('用户事件表 - 记录用户行为日志') # 2. 设置生命周期为 180 天 table.set_lifecycle(180) # 3. 添加新列 table.add_columns([ Column('device_type', 'string', comment='设备类型'), Column('app_version', 'string', comment='应用版本'), ], if_not_exists=True) # 4. 重命名列 table.rename_column('device_type', 'device_category') # 5. 更新时间戳 table.touch() ``` ```go func alterTableExample(odpsIns *odps.Odps) error { table := odpsIns.Tables().Get("user_events") // 1. 修改表注释 err := table.ChangeComment("用户事件表 - 记录用户行为日志") if err != nil { return err } // 2. 设置生命周期为 180 天 err = table.SetLifeCycle(180) if err != nil { return err } // 3. 添加新列 columns := []tableschema.Column{ {Name: "device_type", Type: datatype.StringType, Comment: "设备类型"}, {Name: "app_version", Type: datatype.StringType, Comment: "应用版本"}, } err = table.AddColumns(columns, true) if err != nil { return err } // 4. 重命名列 err = table.ChangeColumnName("device_type", "device_category") if err != nil { return err } // 5. 更新时间戳 return table.Touch() } ``` --- # guides/manage-tables/create-table.md # 创建表 `Tables.TableCreator` 是 MaxCompute Java SDK 提供的建表工具,采用 Builder 模式简化表创建过程。本文介绍如何使用 `TableCreator` 创建各种类型的表。 ## TableCreator Builder 模式 ### 获取 TableCreator 对象 ```java Odps odps = new Odps(...); // 指定项目名 Tables.TableCreator creator = odps.tables().newTableCreator("project_name", "table_name", schema); // 使用默认项目 Tables.TableCreator creator = odps.tables().newTableCreator("table_name", schema); ``` ```python from odps import ODPS odps = ODPS('', '', project='default_project', endpoint='') # 使用 schema 对象创建表 table = odps.create_table('table_name', table_schema=schema) # 使用简易字符串语法创建表 table = odps.create_table('table_name', 'col1 string, col2 bigint') ``` ```go import ( "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/tableschema" ) // 使用默认项目 tables := odps.NewTables(odpsIns, odpsIns.DefaultProjectName(), "") // 指定项目名 tables := odps.NewTables(odpsIns, "project_name", "") // 调用 Create 创建表 err := tables.Create(schema, true, nil, nil) ``` ### 完整示例 ```java TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("id", TypeInfoFactory.BIGINT).notNull().build()) .withColumn(Column.newBuilder("name", TypeInfoFactory.STRING).withComment("用户名").build()) .withColumn(Column.newBuilder("amount", TypeInfoFactory.DECIMAL).build()) .withPartitionColumn(Column.newBuilder("dt", TypeInfoFactory.STRING).build()) .build(); odps.tables().newTableCreator("my_project", "orders", schema) .withSchemaName("my_schema") // 三层模型 .withComment("订单表") // 表注释 .withLifeCycle(365L) // 生命周期(天) .ifNotExists() // 表已存在时不报错 .create(); // 执行创建 ``` ```python from odps import ODPS from odps.models import TableSchema, Column, Partition schema = TableSchema( columns=[ Column('id', 'bigint', nullable=False), Column('name', 'string', comment='用户名'), Column('amount', 'decimal'), ], partitions=[ Partition('dt', 'string'), ] ) odps.create_table( 'my_schema.orders', # 三层模型使用 schema.table 格式 table_schema=schema, comment='订单表', lifecycle=365, if_not_exists=True, project='my_project', ) ``` ```go import ( "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/datatype" "github.com/aliyun/aliyun-odps-go-sdk/odps/tableschema" ) schema := tableschema.NewSchemaBuilder(). Name("orders"). Comment("订单表"). Column(tableschema.Column{Name: "id", Type: datatype.BigIntType, NotNull: true}). Column(tableschema.Column{Name: "name", Type: datatype.StringType, Comment: "用户名"}). Column(tableschema.Column{Name: "amount", Type: datatype.NewDecimalType(38, 18)}). PartitionColumn(tableschema.Column{Name: "dt", Type: datatype.StringType}). Lifecycle(365). Build() // 三层模型通过 schemaName 参数指定 tables := odps.NewTables(odpsIns, "my_project", "my_schema") err := tables.Create(schema, true, nil, nil) // createIfNotExists=true ``` ## 列定义 使用 `Column.newBuilder` 构建列对象(since 0.47.0): ```java // 基本列定义 Column idCol = Column.newBuilder("id", TypeInfoFactory.BIGINT).build(); Column nameCol = Column.newBuilder("name", TypeInfoFactory.STRING).build(); // 设置列属性 Column col = Column.newBuilder("price", TypeInfoFactory.DECIMAL) .notNull() // 非空约束 .withComment("商品价格") // 列注释 .withDefaultValue("0.00") // 默认值 .build(); ``` ```python from odps.models import Column # 基本列定义 id_col = Column('id', 'bigint') name_col = Column('name', 'string') # 设置列属性 col = Column( 'price', 'decimal', nullable=False, # 非空约束 comment='商品价格', # 列注释 ) ``` ```go import ( "github.com/aliyun/aliyun-odps-go-sdk/odps/datatype" "github.com/aliyun/aliyun-odps-go-sdk/odps/tableschema" ) // 基本列定义 idCol := tableschema.Column{Name: "id", Type: datatype.BigIntType} nameCol := tableschema.Column{Name: "name", Type: datatype.StringType} // 设置列属性 col := tableschema.Column{ Name: "price", Type: datatype.NewDecimalType(38, 18), NotNull: true, // 非空约束 Comment: "商品价格", // 列注释 } ``` `TypeInfoFactory` 支持 MaxCompute 全部数据类型,包括 `BIGINT`、`STRING`、`DOUBLE`、`DECIMAL`、`DATETIME`、`BOOLEAN`、`ARRAY`、`MAP`、`STRUCT` 等。 ## 构建表结构 `TableSchema` 包含数据列和分区列: ```java TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("c1", TypeInfoFactory.STRING).build()) .withColumn(Column.newBuilder("c2", TypeInfoFactory.BIGINT).build()) .withColumns(moreColumns) // 批量添加数据列 .withPartitionColumn(Column.newBuilder("pt", TypeInfoFactory.STRING).build()) .build(); ``` ```python from odps.models import TableSchema, Column, Partition schema = TableSchema( columns=[ Column('c1', 'string'), Column('c2', 'bigint'), ], partitions=[ Partition('pt', 'string'), ] ) # 也可以用 from_lists 快捷创建 schema = TableSchema.from_lists( ['c1', 'c2'], ['string', 'bigint'], # 数据列 ['pt'], ['string'] # 分区列 ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("table_name"). Column(tableschema.Column{Name: "c1", Type: datatype.StringType}). Column(tableschema.Column{Name: "c2", Type: datatype.BigIntType}). PartitionColumn(tableschema.Column{Name: "pt", Type: datatype.StringType}). Build() ``` 也提供快捷方法创建简单表结构: :::info 此功能仅 Java SDK 支持。::: ```java TableSchema schema = TableSchema.builder() .withBigintColumn("id") .withStringColumn("name") .withDoubleColumn("score") .withDatetimeColumn("created_at") .withBooleanColumn("is_active") .build(); ``` ## 分区列 分区列通过 `withPartitionColumn` 指定,通常使用 `STRING` 类型: ```java TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("data", TypeInfoFactory.STRING).build()) .withPartitionColumn(Column.newBuilder("dt", TypeInfoFactory.STRING).build()) .withPartitionColumn(Column.newBuilder("region", TypeInfoFactory.STRING).build()) .build(); odps.tables().newTableCreator("project", "partitioned_table", schema) .ifNotExists() .create(); ``` ```python from odps.models import TableSchema, Column, Partition schema = TableSchema( columns=[ Column('data', 'string'), ], partitions=[ Partition('dt', 'string'), Partition('region', 'string'), ] ) odps.create_table('partitioned_table', schema, if_not_exists=True, project='project') ``` ```go schema := tableschema.NewSchemaBuilder(). Name("partitioned_table"). Column(tableschema.Column{Name: "data", Type: datatype.StringType}). PartitionColumn(tableschema.Column{Name: "dt", Type: datatype.StringType}). PartitionColumn(tableschema.Column{Name: "region", Type: datatype.StringType}). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, true, nil, nil) ``` ## 主键 (since 0.48.5) Delta 表必须配置主键信息,主键列必须设置 `notNull()`: ```java TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("pk1", TypeInfoFactory.BIGINT).notNull().build()) .withColumn(Column.newBuilder("pk2", TypeInfoFactory.STRING).notNull().build()) .withColumn(Column.newBuilder("value", TypeInfoFactory.STRING).build()) .build(); odps.tables().newTableCreator("project", "delta_pk_table", schema) .deltaTable() .withPrimaryKeys(List.of("pk1", "pk2")) .withDeltaTableBucketNum(16) .create(); ``` ```python from odps.models import TableSchema, Column schema = TableSchema.from_lists( ['pk1', 'pk2', 'value'], ['bigint', 'string', 'string'] ) odps.create_table( 'delta_pk_table', schema, primary_key=['pk1', 'pk2'], transactional=True, table_properties={'write.bucket.num': '16'}, project='project', ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("delta_pk_table"). Column(tableschema.Column{Name: "pk1", Type: datatype.BigIntType, NotNull: true}). Column(tableschema.Column{Name: "pk2", Type: datatype.StringType, NotNull: true}). Column(tableschema.Column{Name: "value", Type: datatype.StringType}). PrimaryKeys([]string{"pk1", "pk2"}). TblProperties(map[string]string{ "transactional": "true", "write.bucket.num": "16", }). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, false, nil, nil) ``` ## 表格式 ### Append 表(默认) 默认创建的即为 Append 表。如需 Append 2.0 格式,可通过 `withTblProperties` 指定: ```java Map props = new HashMap<>(); props.put("table.format.version", "2"); odps.tables().newTableCreator("project", "append_v2_table", schema) .withTblProperties(props) .create(); ``` ```python odps.create_table( 'append_v2_table', schema, table_properties={'table.format.version': '2'}, project='project', ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("append_v2_table"). Columns(columns...). TblProperties(map[string]string{"table.format.version": "2"}). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, false, nil, nil) ``` ### Transaction 表 ```java odps.tables().newTableCreator("project", "txn_table", schema) .transactionTable() .create(); ``` ```python odps.create_table( 'txn_table', schema, transactional=True, project='project', ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("txn_table"). Columns(columns...). TblProperties(map[string]string{"transactional": "true"}). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, false, nil, nil) ``` ### Delta 表 Delta 表是一种支持高效 upsert 操作的事务表类型,必须配置主键和分桶数: ```java TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("pk", TypeInfoFactory.BIGINT).notNull().build()) .withColumn(Column.newBuilder("c1", TypeInfoFactory.STRING).build()) .withColumn(Column.newBuilder("c2", TypeInfoFactory.STRING).build()) .build(); odps.tables().newTableCreator("project", "delta_table", schema) .deltaTable() .withPrimaryKeys(List.of("pk")) .withDeltaTableBucketNum(16) .ifNotExists() .create(); ``` ```python from odps.models import TableSchema schema = TableSchema.from_lists( ['pk', 'c1', 'c2'], ['bigint', 'string', 'string'] ) odps.create_table( 'delta_table', schema, primary_key=['pk'], transactional=True, table_properties={'write.bucket.num': '16'}, if_not_exists=True, project='project', ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("delta_table"). Column(tableschema.Column{Name: "pk", Type: datatype.BigIntType, NotNull: true}). Column(tableschema.Column{Name: "c1", Type: datatype.StringType}). Column(tableschema.Column{Name: "c2", Type: datatype.StringType}). PrimaryKeys([]string{"pk"}). TblProperties(map[string]string{ "transactional": "true", "write.bucket.num": "16", }). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, true, nil, nil) ``` ## 聚簇表 (Clustering) ### Hash Clustering ```java Table.ClusterInfo clusterInfo = new Table.ClusterInfo( Table.ClusterInfo.ClusterType.HASH, List.of("c1", "c2"), // clustered by 列 List.of( // 桶内排序列 new Table.SortColumn("c1", Table.SortColumn.Order.ASC), new Table.SortColumn("c2", Table.SortColumn.Order.DESC) ), 64 // 桶数量 ); odps.tables().newTableCreator("project", "hash_cluster_table", schema) .withClusterInfo(clusterInfo) .create(); ``` ```python odps.execute_sql( 'CREATE TABLE project.hash_cluster_table (c1 STRING, c2 STRING, c3 STRING) ' 'CLUSTERED BY (c1, c2) ' 'SORTED BY (c1 ASC, c2 DESC) ' 'INTO 64 BUCKETS' ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("hash_cluster_table"). Column(tableschema.Column{Name: "c1", Type: datatype.StringType}). Column(tableschema.Column{Name: "c2", Type: datatype.StringType}). Column(tableschema.Column{Name: "c3", Type: datatype.StringType}). ClusterType(tableschema.CLUSTER_TYPE.Hash). ClusterColumns([]string{"c1", "c2"}). ClusterSortColumns([]tableschema.SortColumn{ {Name: "c1", Order: "asc"}, {Name: "c2", Order: "desc"}, }). ClusterBucketNum(64). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, false, nil, nil) ``` ### Range Clustering ```java Table.ClusterInfo clusterInfo = new Table.ClusterInfo( Table.ClusterInfo.ClusterType.RANGE, List.of("c1", "c2"), List.of( new Table.SortColumn("c1", Table.SortColumn.Order.ASC) ), 0 // Range Cluster 桶数量可设为0(自动) ); odps.tables().newTableCreator("project", "range_cluster_table", schema) .withClusterInfo(clusterInfo) .create(); ``` ```python odps.execute_sql( 'CREATE TABLE project.range_cluster_table (c1 STRING, c2 STRING, c3 STRING) ' 'RANGE CLUSTERED BY (c1, c2) ' 'SORTED BY (c1 ASC)' # Range Cluster 桶数量可省略(自动) ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("range_cluster_table"). Column(tableschema.Column{Name: "c1", Type: datatype.StringType}). Column(tableschema.Column{Name: "c2", Type: datatype.StringType}). Column(tableschema.Column{Name: "c3", Type: datatype.StringType}). ClusterType(tableschema.CLUSTER_TYPE.Range). ClusterColumns([]string{"c1", "c2"}). ClusterSortColumns([]tableschema.SortColumn{ {Name: "c1", Order: "asc"}, }). ClusterBucketNum(0). // Range Cluster 桶数量可设为0(自动) Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, false, nil, nil) ``` ## Auto-Partition (since 0.51.0) Auto-partition 表可以根据生成表达式自动创建分区,无需手动管理分区: ```java Column autoPartCol = Column.newBuilder("p1", TypeInfoFactory.STRING) .withGenerateExpression(new TruncTime("event_time", TruncTime.DatePart.DAY)) .build(); TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("event_time", TypeInfoFactory.DATETIME).build()) .withColumn(Column.newBuilder("data", TypeInfoFactory.STRING).build()) .withPartitionColumn(autoPartCol) .build(); odps.tables().newTableCreator("project", "auto_part_table", schema) .ifNotExists() .create(); ``` ```python from odps.models import TableSchema, Column, Partition schema = TableSchema( columns=[ Column('event_time', 'datetime'), Column('data', 'string'), ], partitions=[ Partition('p1', 'string', generate_expression="trunc_time(`event_time`, 'DAY')"), ] ) odps.create_table('auto_part_table', schema, if_not_exists=True, project='project') ``` ```go autoPartCol := tableschema.Column{ Name: "p1", Type: datatype.StringType, GenerateExpression: tableschema.NewTruncTime("event_time", tableschema.DAY), } schema := tableschema.NewSchemaBuilder(). Name("auto_part_table"). Column(tableschema.Column{Name: "event_time", Type: datatype.DateTimeType}). Column(tableschema.Column{Name: "data", Type: datatype.StringType}). PartitionColumn(autoPartCol). Build() tables := odps.NewTables(odpsIns, "project", "") err := tables.Create(schema, true, nil, nil) ``` `TruncTime.DatePart` 支持的粒度:`YEAR`、`MONTH`、`DAY`、`HOUR`。 ## 外部表 外部表用于连接外部数据源(如 OSS): ```java TableSchema schema = TableSchema.builder() .withColumn(Column.newBuilder("c1", TypeInfoFactory.STRING).build()) .withColumn(Column.newBuilder("c2", TypeInfoFactory.STRING).build()) .build(); odps.tables().newTableCreator("project", "external_table", schema) .externalTable() .withStorageHandler("com.aliyun.odps.udf.example.text.TextStorageHandler") .withLocation("oss://bucket/path/to/data/") .withResources(List.of("odps-udf-example.jar")) .withSerdeProperties(Map.of( "odps.text.option.delimiter", "|", "odps.text.option.encoding", "UTF-8" )) .create(); ``` ```python from odps.models import TableSchema schema = TableSchema.from_lists(['c1', 'c2'], ['string', 'string']) odps.create_table( 'external_table', schema, storage_handler='com.aliyun.odps.udf.example.text.TextStorageHandler', location='oss://bucket/path/to/data/', resources='odps-udf-example.jar', serde_properties={ 'odps.text.option.delimiter': '|', 'odps.text.option.encoding': 'UTF-8', }, project='project', ) ``` ```go schema := tableschema.NewSchemaBuilder(). Name("external_table"). Column(tableschema.Column{Name: "c1", Type: datatype.StringType}). Column(tableschema.Column{Name: "c2", Type: datatype.StringType}). StorageHandler("com.aliyun.odps.udf.example.text.TextStorageHandler"). Location("oss://bucket/path/to/data/"). Build() serdeProperties := map[string]string{ "odps.text.option.delimiter": "|", "odps.text.option.encoding": "UTF-8", } jars := []string{"odps-udf-example.jar"} tables := odps.NewTables(odpsIns, "project", "") err := tables.CreateExternal(schema, false, serdeProperties, jars, nil, nil) ``` ## SQL 预览 在执行创建前,可以通过 `getSQL()` 方法预览将要执行的 DDL 语句: :::info 此功能仅 Java SDK 支持。::: ```java Tables.TableCreator creator = odps.tables().newTableCreator("project", "my_table", schema) .transactionTable() .withLifeCycle(90L); // 预览 SQL String sql = creator.getSQL(); System.out.println(sql); // 确认后执行 creator.create(); ``` --- # guides/manage-tables/index.md # 管理表 本章节介绍如何使用 MaxCompute Java SDK 对表进行全生命周期管理,包括创建、修改、分区管理和标签操作。 ## 表类型 MaxCompute 中的表分为以下四种类型: ```java public enum TableType { MANAGED_TABLE, // 常规 MaxCompute 内部表 VIRTUAL_VIEW, // 虚拟视图 EXTERNAL_TABLE, // 外部表 MATERIALIZED_VIEW // 物化视图 } ``` ```python # Python SDK 中表类型通过 table.type 属性获取 # 可能的值: Table.Type.MANAGED_TABLE, VIRTUAL_VIEW, EXTERNAL_TABLE, MATERIALIZED_VIEW ``` ```go // Go SDK 中表类型定义 const ( ManagedTable TableType = "MANAGED_TABLE" VirtualView TableType = "VIRTUAL_VIEW" ExternalTable TableType = "EXTERNAL_TABLE" MaterializedView TableType = "MATERIALIZED_VIEW" ) ``` 可以通过以下方法判断表类型: ```java Table.TableType tableType = table.getType(); // 或使用便捷方法 table.isVirtualView(); table.isMaterializedView(); table.isExternalTable(); ``` ```python table = odps.get_table('my_table') table_type = table.type # 判断表类型 table.is_virtual_view table.is_materialized_view ``` ```go table := odpsIns.Table("my_table") table.Load() tableType := table.Type() ``` ## 获取表实例 要操作一张表,首先需要获取 `Table` 实例对象。获取表实例是一个 **lazy** 操作,即此时不会发起网络请求,只有在调用 `Table` 类的其他方法时,才会真正获取表的元数据信息。 ```java // 指定项目名获取表实例 Table table = odps.tables().get("project_name", "table_name"); // 使用默认项目 Table table = odps.tables().get("table_name"); // 三层模型:指定 schema Table table = odps.tables().get("project_name", "schema_name", "table_name"); ``` ```python # 指定项目名获取表实例 table = odps.get_table('table_name', project='project_name') # 使用默认项目 table = odps.get_table('table_name') # 三层模型:指定 schema table = odps.get_table('table_name', project='project_name', schema='schema_name') ``` ```go // 使用默认项目获取表实例 table := odpsIns.Table("table_name") // 通过 Tables 集合获取 table = odpsIns.Tables().Get("table_name") ``` ## 读取表元数据 `Table` 实现了 `lazyload` 机制,首次调用以下方法时会自动发起网络请求加载元数据。也可以手动调用 `reload()` 强制刷新: ```java // 手动加载/刷新元数据 table.reload(); ``` ```python # 手动加载/刷新元数据 table.reload() ``` ```go // 手动加载元数据 err := table.Load() ``` 除非再次调用 `reload()`,否则后续访问均使用缓存数据。 ### 常用元数据方法 | 方法 | 说明 | |------|------| | `table.getName()` | 获取表名 | | `table.getProject()` | 获取所在项目名 | | `table.getSchemaName()` | 获取所在 Schema 名(需开启 Schema 功能) | | `table.getSchema()` | 获取表结构(TableSchema) | | `table.getJsonSchema()` | 获取 JSON 格式的表结构 | | `table.getComment()` | 获取表注释 | | `table.getOwner()` | 获取表所属用户 | | `table.getType()` | 获取表类型 | | `table.getSize()` | 获取表存储大小(bytes,估计值) | | `table.getRecordNum()` | 获取记录数(无准确数据时返回 -1) | | `table.getLife()` | 获取生命周期(天) | | `table.getCreatedTime()` | 获取创建时间 | | `table.getLastMetaModifiedTime()` | 获取最后修改时间 | ### 示例 ```java Odps odps = new Odps(...); Table table = odps.tables().get("my_project", "user_events"); // 获取表结构 TableSchema schema = table.getSchema(); List columns = schema.getColumns(); // 数据列 List partCols = schema.getPartitionColumns(); // 分区列 // 获取表大小 long sizeInBytes = table.getSize(); // 获取表类型 Table.TableType type = table.getType(); System.out.println("Table type: " + type); ``` ```python table = odps.get_table('user_events', project='my_project') # 获取表结构 schema = table.table_schema columns = schema.columns # 数据列 part_cols = schema.partitions # 分区列 # 获取表大小 size_in_bytes = table.size # 获取表类型 table_type = table.type print(f"Table type: {table_type}") ``` ```go table := odpsIns.Table("user_events") table.Load() // 获取表结构 schema := table.Schema() columns := schema.Columns // 数据列 partCols := schema.PartitionColumns // 分区列 // 获取表大小 sizeInBytes := table.Size() // 获取表类型 tableType := table.Type() fmt.Printf("Table type: %s\n", tableType) ``` ## 子章节 - [创建表](./create-table.md) - 使用 TableCreator 创建各类表 - [修改表](./alter-table.md) - 重命名、修改生命周期、增删列等操作 - [分区操作](./partitions.md) - 分区的增删查操作 - [标签操作](./tags.md) - 表级别和列级别的标签管理 --- # guides/manage-tables/partitions.md # 分区操作 本文介绍如何使用 MaxCompute SDK 对分区表进行分区管理,包括分区的查询、创建、删除以及分区规格的构造方法。 ## 前置条件 所有分区操作都基于 `Table` 实例: ```java Odps odps = new Odps(...); Table table = odps.tables().get("project_name", "partitioned_table"); ``` ```python from odps import ODPS odps = ODPS(...) table = odps.get_table('partitioned_table', project='project_name') ``` ```go odpsClient := odps.NewOdps(...) table := odpsClient.Table("partitioned_table") err := table.Load() ``` ## PartitionSpec 构造 `PartitionSpec` 用于描述一个具体的分区值,是分区操作的核心参数。 ### 通过字符串解析构造 ```java // 单级分区 PartitionSpec spec = new PartitionSpec("dt='20240101'"); // 多级分区 PartitionSpec spec = new PartitionSpec("dt='20240101',region='cn'"); ``` ```python # 分区规格以字符串形式表示 # 单级分区 spec = 'dt=20240101' # 多级分区 spec = 'dt=20240101,region=cn' ``` ```go // 分区规格以 "key=value/key=value" 格式表示 // 单级分区 spec := "dt=20240101" // 多级分区 spec := "dt=20240101/region=cn" ``` ### 通过 key-value 逐个添加 ```java PartitionSpec spec = new PartitionSpec(); spec.set("dt", "20240101"); spec.set("region", "cn"); ``` ```python # Python SDK 使用字符串形式指定分区 spec = 'dt=20240101,region=cn' ``` ```go // Go SDK 使用 "/" 分隔的字符串格式 spec := "dt=20240101/region=cn" ``` ### trim 参数 (since 0.50.4) 构造 `PartitionSpec` 时,可以指定 `trim` 参数来自动去除分区值两端的空白字符: ```java // trim=true 会去除分区值前后的空格 PartitionSpec spec = new PartitionSpec("dt=' 20240101 '", true); // 等价于 dt='20240101' ``` ```python # Python SDK 自动处理分区值的空白字符 spec = 'dt=20240101' ``` ```go // Go SDK 使用标准字符串处理 import "strings" spec := strings.TrimSpace("dt=20240101") ``` ## 获取单个分区 ```java Partition partition = table.getPartition(new PartitionSpec("dt='20240101'")); ``` ```python partition = table.get_partition('dt=20240101') ``` ```go partition, err := table.GetPartition("dt=20240101") ``` 获取到分区对象后,可以查询该分区的详细信息: ```java long size = partition.getSize(); // 分区存储大小(bytes) long recordNum = partition.getRecordNum(); // 分区记录数 Date createdTime = partition.getCreatedTime(); // 分区创建时间 ``` ```python partition.reload() size = partition.size # 分区存储大小(bytes) record_num = partition.record_num # 分区记录数 created_time = partition.creation_time # 分区创建时间 ``` ```go err := partition.Load() size := partition.Size() // 分区存储大小(bytes) recordNum := partition.RecordNum() // 分区记录数 createdTime := partition.CreatedTime() // 分区创建时间 ``` ## 获取所有分区 ### getPartitions - 获取完整分区信息 返回所有分区的详细信息列表,包括每个分区的大小、记录数等元数据: ```java List partitions = table.getPartitions(); for (Partition p : partitions) { System.out.println("Partition: " + p.getPartitionSpec()); System.out.println(" Size: " + p.getSize()); System.out.println(" Records: " + p.getRecordNum()); System.out.println(" Created: " + p.getCreatedTime()); } ``` ```python for p in table.partitions: print(f'Partition: {p.name}') p.reload() print(f' Size: {p.size}') print(f' Records: {p.record_num}') print(f' Created: {p.creation_time}') ``` ```go partitions, err := table.GetPartitions() if err != nil { log.Fatalf("%+v", err) } for _, p := range partitions { fmt.Printf("Partition: %s\n", p.Value()) fmt.Printf(" Size: %d\n", p.Size()) fmt.Printf(" Records: %d\n", p.RecordNum()) fmt.Printf(" Created: %s\n", p.CreatedTime()) } ``` ### getPartitionSpecs - 仅获取分区值(高性能) 与 `getPartitions()` 不同,此方法仅返回分区值,不包含分区的详细元数据信息,因此效率更高。当只需要知道有哪些分区存在时,推荐使用此方法。 ```java List specs = table.getPartitionSpecs(); for (PartitionSpec spec : specs) { System.out.println("Partition spec: " + spec.toString()); } ``` ```python for p in table.iterate_partitions(): print(f'Partition spec: {p.name}') ``` ```go values, err := table.GetPartitionValues() if err != nil { log.Fatalf("%+v", err) } for _, v := range values { fmt.Printf("Partition spec: %s\n", v) } ``` > **性能提示**:当表分区数量较多时,`getPartitionSpecs()` 比 `getPartitions()` 性能显著更优,因为它不需要加载每个分区的元数据。 ## 判断分区是否存在 ```java PartitionSpec spec = new PartitionSpec("dt='20240101'"); boolean exists = table.hasPartition(spec); if (exists) { System.out.println("分区已存在"); } ``` ```python exists = table.exist_partition('dt=20240101') if exists: print('分区已存在') ``` ```go // Go SDK 通过 GetPartition 返回 error 来判断分区是否存在 _, err := table.GetPartition("dt=20240101") if err == nil { fmt.Println("分区已存在") } ``` ## 创建分区 ```java PartitionSpec spec = new PartitionSpec("dt='20240115',region='cn'"); table.createPartition(spec); ``` ```python table.create_partition('dt=20240115,region=cn') ``` ```go err := table.AddPartition(false, "dt=20240115/region=cn") ``` 带 `ifNotExists` 参数,分区已存在时不抛出异常: ```java table.createPartition(spec, true); ``` ```python table.create_partition('dt=20240115,region=cn', if_not_exists=True) ``` ```go err := table.AddPartition(true, "dt=20240115/region=cn") ``` ## 删除分区 ```java PartitionSpec spec = new PartitionSpec("dt='20240101'"); table.deletePartition(spec); ``` ```python table.delete_partition('dt=20240101') ``` ```go err := table.DeletePartition(false, "dt=20240101") ``` 带 `ifExists` 参数,分区不存在时不抛出异常: ```java table.deletePartition(spec, true); ``` ```python table.delete_partition('dt=20240101', if_exists=True) ``` ```go err := table.DeletePartition(true, "dt=20240101") ``` ## 完整示例 ### 批量创建分区 ```java public void batchCreatePartitions(Odps odps) throws OdpsException { Table table = odps.tables().get("my_project", "daily_events"); // 批量创建最近 7 天的分区 LocalDate today = LocalDate.now(); for (int i = 0; i < 7; i++) { String dt = today.minusDays(i).format(DateTimeFormatter.BASIC_ISO_DATE); PartitionSpec spec = new PartitionSpec("dt='" + dt + "'"); if (!table.hasPartition(spec)) { table.createPartition(spec); System.out.println("Created partition: " + spec); } } } ``` ```python from datetime import datetime, timedelta def batch_create_partitions(odps): table = odps.get_table('daily_events', project='my_project') # 批量创建最近 7 天的分区 today = datetime.now() for i in range(7): dt = (today - timedelta(days=i)).strftime('%Y%m%d') spec = f'dt={dt}' if not table.exist_partition(spec): table.create_partition(spec) print(f'Created partition: {spec}') ``` ```go func batchCreatePartitions(odpsClient *odps.Odps) error { table := odpsClient.Table("daily_events") // 批量创建最近 7 天的分区 today := time.Now() specs := make([]string, 0, 7) for i := 0; i < 7; i++ { dt := today.AddDate(0, 0, -i).Format("20060102") specs = append(specs, fmt.Sprintf("dt=%s", dt)) } return table.AddPartitions(true, specs) } ``` ### 清理过期分区 ```java public void cleanExpiredPartitions(Odps odps, int retentionDays) throws OdpsException { Table table = odps.tables().get("my_project", "daily_events"); LocalDate threshold = LocalDate.now().minusDays(retentionDays); List specs = table.getPartitionSpecs(); for (PartitionSpec spec : specs) { String dtValue = spec.get("dt"); LocalDate partDate = LocalDate.parse(dtValue, DateTimeFormatter.BASIC_ISO_DATE); if (partDate.isBefore(threshold)) { table.deletePartition(spec, true); System.out.println("Deleted expired partition: " + spec); } } } ``` ```python from datetime import datetime, timedelta def clean_expired_partitions(odps, retention_days): table = odps.get_table('daily_events', project='my_project') threshold = datetime.now() - timedelta(days=retention_days) for p in table.iterate_partitions(): # 分区名格式: dt=20240101 dt_value = p.partition_spec['dt'] part_date = datetime.strptime(dt_value, '%Y%m%d') if part_date < threshold: table.delete_partition(p.partition_spec, if_exists=True) print(f'Deleted expired partition: {p.name}') ``` ```go func cleanExpiredPartitions(odpsClient *odps.Odps, retentionDays int) error { table := odpsClient.Table("daily_events") threshold := time.Now().AddDate(0, 0, -retentionDays) values, err := table.GetPartitionValues() if err != nil { return err } var expired []string for _, v := range values { // 分区值格式: dt=20240101 parts := strings.SplitN(v, "=", 2) partDate, err := time.Parse("20060102", parts[1]) if err != nil { continue } if partDate.Before(threshold) { expired = append(expired, v) } } if len(expired) > 0 { return table.DeletePartitions(true, expired) } return nil } ``` ### 遍历分区统计信息 ```java public void printPartitionStats(Odps odps) throws OdpsException { Table table = odps.tables().get("my_project", "user_logs"); List partitions = table.getPartitions(); long totalSize = 0; long totalRecords = 0; for (Partition p : partitions) { totalSize += p.getSize(); totalRecords += p.getRecordNum(); } System.out.println("Total partitions: " + partitions.size()); System.out.println("Total size: " + totalSize + " bytes"); System.out.println("Total records: " + totalRecords); } ``` ```python def print_partition_stats(odps): table = odps.get_table('user_logs', project='my_project') total_size = 0 total_records = 0 count = 0 for p in table.partitions: p.reload() total_size += p.size total_records += p.record_num count += 1 print(f'Total partitions: {count}') print(f'Total size: {total_size} bytes') print(f'Total records: {total_records}') ``` ```go func printPartitionStats(odpsClient *odps.Odps) error { table := odpsClient.Table("user_logs") partitions, err := table.GetPartitions() if err != nil { return err } totalSize := 0 totalRecords := 0 for _, p := range partitions { totalSize += p.Size() totalRecords += p.RecordNum() } fmt.Printf("Total partitions: %d\n", len(partitions)) fmt.Printf("Total size: %d bytes\n", totalSize) fmt.Printf("Total records: %d\n", totalRecords) return nil } ``` --- # guides/manage-tables/tags.md # 标签操作 MaxCompute 支持对表和列打标操作。通过标签,可以基于不同的业务场景对数据做标识(Tagging),使业务能够基于标签合理使用数据,例如数据访问控制和数据血缘追踪。 :::info 标签(Tag)操作目前仅 Java SDK 支持。Python SDK 和 Go SDK 暂未提供标签管理 API。 ::: ## Tag 与 SimpleTag MaxCompute 提供两种标签机制: | 类型 | 说明 | 特点 | |------|------|------| | **Tag** | 标准标签,由独立的 Tag 对象表示 | 需要预先创建 Tag 资源,支持复杂的权限控制和策略关联 | | **SimpleTag** | 简单标签,以 category/key/value 三元组表示 | 无需预先创建,直接以键值对形式附加到表或列上,适合轻量级分类 | ## 前置条件 所有标签操作基于 `Table` 实例: ```java Odps odps = new Odps(...); Table table = odps.tables().get("project_name", "table_name"); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ## 标准标签 (Tag) ### 获取表级别标签 ```java List tags = table.getTags(); for (Tag tag : tags) { System.out.println("Tag: " + tag.getName()); } ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 获取列级别标签 ```java // 获取指定列的标签 List columnTags = table.getTags("column_name"); for (Tag tag : columnTags) { System.out.println("Column tag: " + tag.getName()); } ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 添加表级别标签 表和标签必须属于同一个 project: ```java // 获取 Tag 对象 Tag tag = ...; // 通过 Tags API 获取 // 为表添加标签 table.addTag(tag); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 添加列级别标签 为指定列添加标签: ```java Tag tag = ...; // 通过 Tags API 获取 // 为多个列添加同一个标签 table.addTag(tag, List.of("column1", "column2")); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 删除标签 ```java Tag tag = ...; // 要删除的标签对象 table.removeTag(tag); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ## 简单标签 (SimpleTag) SimpleTag 通过 category(类别)、key(键)、value(值)三元组来标识,无需预先创建 Tag 资源。 ### 获取表级别简单标签 返回值为嵌套 Map 结构:`Map>` ```java Map> simpleTags = table.getSimpleTags(); for (Map.Entry> categoryEntry : simpleTags.entrySet()) { String category = categoryEntry.getKey(); for (Map.Entry kvEntry : categoryEntry.getValue().entrySet()) { System.out.println(category + "/" + kvEntry.getKey() + " = " + kvEntry.getValue()); } } ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 获取列级别简单标签 ```java Map> columnSimpleTags = table.getSimpleTags("column_name"); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 添加表级别简单标签 ```java // 参数:category, key, value table.addSimpleTag("data_classification", "sensitivity", "high"); table.addSimpleTag("business", "domain", "finance"); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 添加列级别简单标签 ```java // 为指定列添加简单标签 table.addSimpleTag("pii", "type", "phone_number", List.of("phone_col")); table.addSimpleTag("pii", "type", "email", List.of("email_col", "backup_email_col")); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 删除表级别简单标签 ```java // 参数:category, key, value table.removeSimpleTag("data_classification", "sensitivity", "high"); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 删除列级别简单标签 ```java table.removeSimpleTag("pii", "type", "phone_number", List.of("phone_col")); ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ## 完整示例 ### 为表添加数据分类标签 ```java public void classifyTable(Odps odps) throws OdpsException { Table table = odps.tables().get("my_project", "user_profile"); // 添加业务域标签 table.addSimpleTag("business", "domain", "user_center"); table.addSimpleTag("business", "priority", "P0"); // 添加数据安全等级标签 table.addSimpleTag("security", "level", "confidential"); // 查看已添加的标签 Map> tags = table.getSimpleTags(); System.out.println("Table simple tags: " + tags); } ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 为敏感列打标 ```java public void tagSensitiveColumns(Odps odps) throws OdpsException { Table table = odps.tables().get("my_project", "customer_info"); // 标记 PII(个人身份信息)列 table.addSimpleTag("pii", "type", "id_card", List.of("id_number")); table.addSimpleTag("pii", "type", "phone", List.of("mobile", "home_phone")); table.addSimpleTag("pii", "type", "address", List.of("home_address")); // 查看特定列的标签 Map> phoneTags = table.getSimpleTags("mobile"); System.out.println("mobile column tags: " + phoneTags); } ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` ### 使用标准 Tag 进行访问控制 ```java public void applyAccessControlTag(Odps odps, Tag sensitiveTag) throws OdpsException { Table table = odps.tables().get("my_project", "financial_data"); // 为表添加标准标签(用于策略关联) table.addTag(sensitiveTag); // 为特定敏感列添加标签 table.addTag(sensitiveTag, List.of("account_balance", "transaction_amount")); // 验证标签已添加 List tableTags = table.getTags(); System.out.println("Table tags count: " + tableTags.size()); List colTags = table.getTags("account_balance"); System.out.println("Column tags count: " + colTags.size()); } ``` ```python # Python SDK 暂不支持标签操作 ``` ```go // Go SDK 暂不支持标签操作 ``` --- # guides/read-data/blob.md # Blob 数据下载 :::note Blob 数据下载目前支持 Java SDK 和 Python SDK,Go SDK 暂不支持。 ::: MaxCompute 的 `BLOB` 类型用于存储非结构化的二进制大对象数据(如图片、音频、文档等)。Blob 在表中以引用(Reference)的形式存储,实际的二进制数据需要通过 `BlobManager`(Java)或 `StorageApiClient.read_blobs()`(Python)单独下载。 ## 前置条件 - 添加 `odps-sdk-storage-api` 模块依赖 - 已初始化 `MaxStorageClient` - 目标表包含 `BLOB` 类型列 - SDK 版本 >= 0.54.0 ## 完整示例 ```java import com.aliyun.odps.storage.api.MaxStorageClient; import com.aliyun.odps.storage.api.TableIdentifier; import com.aliyun.odps.storage.api.blob.Blob; import com.aliyun.odps.storage.api.blob.BlobManager; import com.aliyun.odps.storage.api.blob.BlobDataIterator; import com.aliyun.odps.storage.api.read.TableReadSession; import com.aliyun.odps.storage.api.read.InputSplit; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.RecordReader; import org.apache.arrow.vector.ipc.ArrowReader; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class BlobDownloadExample { public static void main(String[] args) throws Exception { // 1. 创建客户端 MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); // 2. 读取包含 Blob 列的表 TableIdentifier tableId = TableIdentifier.of("my_project", "image_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "image_data")) .build(); // 3. 获取 BlobManager BlobManager blobManager = client.openBlobManager(); // 4. 读取表数据并收集 Blob 引用 for (InputSplit split : session.getSplits()) { ArrowReader arrowReader = session.createReaderBuilder(split).build(); RecordReader recordReader = arrowReader.getAsRecordReader(); List blobs = new ArrayList<>(); List ids = new ArrayList<>(); recordReader.forEach(record -> { Blob blob = (Blob) record.get("image_data"); if (blob != null) { blobs.add(blob); ids.add((Long) record.get("id")); } }); // 5. 批量下载 Blob 数据 try (BlobDataIterator iterator = blobManager.batchDownload(blobs)) { int index = 0; while (iterator.hasNext()) { try (InputStream data = iterator.next()) { byte[] content = data.readAllBytes(); // 保存到本地文件 Path outputPath = Paths.get("/tmp/images/" + ids.get(index) + ".jpg"); Files.write(outputPath, content); System.out.println("已下载: " + outputPath + " (" + content.length + " bytes)"); } index++; } } arrowReader.close(); recordReader.close(); } } } ``` ```python from odps import ODPS from odps.apis.storage_api_v2 import StorageApiArrowClient import os # 1. 初始化 ODPS 客户端 odps = ODPS("", "", "", "") # 2. 获取表对象并创建客户端 table = odps.get_table("image_table") client = StorageApiArrowClient(odps, table) # 3. 创建读取会话,指定包含 Blob 列的字段 read_resp = client.create_read_session( required_data_columns=["id", "image_data"] ) session_id = read_resp.session_id # 4. 读取表数据并收集 Blob 引用 blob_refs = [] ids = [] for split_index in range(read_resp.splits_count): reader = client.read_rows_arrow(session_id, split_index=split_index) while True: batch = reader.read() if batch is None: break id_list = batch.column("id").to_pylist() blob_list = batch.column("image_data").to_pylist() for i, ref in enumerate(blob_list): if ref is not None: blob_refs.append( ref.decode("utf-8") if isinstance(ref, bytes) else ref ) ids.append(id_list[i]) # 5. 批量下载 Blob 数据 os.makedirs("/tmp/images", exist_ok=True) for idx, (data, mime_type) in enumerate(client.read_blobs(blob_references=blob_refs)): output_path = f"/tmp/images/{ids[idx]}.jpg" with open(output_path, "wb") as f: f.write(data) print(f"已下载: {output_path} ({len(data)} bytes)") ``` ## 代码说明 1. **创建客户端**:与 Storage API 读取相同,通过 `MaxStorageClient.builder()` 构建。 2. **读取表数据**:使用 `TableReadSession` 读取包含 Blob 列的表,获取每行的 Blob 引用。 3. **获取 BlobManager**:通过 `client.openBlobManager()` 获取 Blob 管理器实例。 4. **收集 Blob 引用**:通过行接口(`RecordReader`)读取表数据,从 Record 中获取 `Blob` 对象。 5. **批量下载**:调用 `blobManager.batchDownload(blobs)` 批量下载,返回 `BlobDataIterator` 迭代器。 6. **处理数据**:从迭代器中逐个获取 `InputStream`,读取实际的二进制内容。 ## 配置选项 ### BlobManager 方法 | 方法 | 参数 | 返回值 | 说明 | |------|------|--------|------| | `download(Blob)` | Blob 引用对象 | InputStream | 下载单个 Blob,调用方必须关闭流 | | `batchDownload(List)` | Blob 引用列表 | BlobDataIterator | 批量下载,返回顺序与输入一致 | ### Blob 构造方式 | 方法 | 说明 | |------|------| | `Blob.fromReference(String)` | 从引用字符串构造 Blob 对象 | | Record 中直接获取 | 行接口自动返回 `Blob` 对象,无需手动构造 | ## 单条下载 适用于 Blob 数量较少或需要按需下载的场景: ```java BlobManager blobManager = client.openBlobManager(); // 从 Record 中获取 Blob 引用 Blob blobRef = (Blob) record.get("image_data"); // 单条下载 try (InputStream data = blobManager.download(blobRef)) { byte[] content = data.readAllBytes(); System.out.println("Blob 大小: " + content.length + " bytes"); } ``` ```python # 从 Arrow 数据中获取 Blob 引用 blob_ref = ref.decode("utf-8") if isinstance(ref, bytes) else ref # 单条下载(传入单元素列表) for data, mime_type in client.read_blobs(blob_references=[blob_ref]): print(f"Blob 大小: {len(data)} bytes") ``` ## 批量下载 适用于大量 Blob 的高效下载,单次请求传输多个 Blob: ```java BlobManager blobManager = client.openBlobManager(); // 收集多个 Blob 引用 List blobs = new ArrayList<>(); // ... 从表数据中收集 ... // 批量下载 try (BlobDataIterator iterator = blobManager.batchDownload(blobs)) { while (iterator.hasNext()) { try (InputStream data = iterator.next()) { byte[] content = data.readAllBytes(); // 处理 content... } } } catch (BlobDownloadException e) { // 获取失败的 Blob 引用 Blob failedBlob = e.getFailedBlobRef(); System.err.println("下载失败: " + failedBlob.getReference()); } ``` ```python # 收集多个 Blob 引用 blob_refs = [] # ... 从表数据中收集 ... # 批量下载 try: for data, mime_type in client.read_blobs(blob_references=blob_refs): # 处理 data... pass except Exception as e: print(f"下载失败: {e}") ``` ## Arrow 列接口读取 Blob 如果使用 Arrow 列接口而非行接口,需要手动从 `VarBinaryVector` 中解析 Blob 引用: ```java import org.apache.arrow.vector.VarBinaryVector; import java.nio.charset.StandardCharsets; for (InputSplit split : session.getSplits()) { try (ArrowReader reader = session.createReaderBuilder(split).build()) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); VarBinaryVector blobVector = (VarBinaryVector) root.getVector("image_data"); List blobRefs = new ArrayList<>(); for (int row = 0; row < root.getRowCount(); row++) { if (!blobVector.isNull(row)) { // 读取时 VarBinaryVector 中是引用字符串的 UTF-8 字节 String reference = new String(blobVector.get(row), StandardCharsets.UTF_8); blobRefs.add(Blob.fromReference(reference)); } } if (!blobRefs.isEmpty()) { try (BlobDataIterator iterator = blobManager.batchDownload(blobRefs)) { while (iterator.hasNext()) { try (InputStream data = iterator.next()) { byte[] content = data.readAllBytes(); // 处理 content... } } } } } } } ``` ```python for split_index in range(read_resp.splits_count): reader = client.read_rows_arrow(session_id, split_index=split_index) while True: batch = reader.read() if batch is None: break blob_vector = batch.column("image_data") blob_refs = [] for row in range(len(batch)): ref = blob_vector[row].as_py() if ref is not None: # Arrow 读取时 VarBinary 中是引用字符串的 UTF-8 字节 blob_refs.append( ref.decode("utf-8") if isinstance(ref, bytes) else ref ) if blob_refs: for data, mime_type in client.read_blobs(blob_references=blob_refs): # 处理 data... pass ``` ## 注意事项 - **优先使用批量下载**:`batchDownload()` 单次请求传输多个 Blob,效率远高于循环调用 `download()`。 - **必须关闭 InputStream**:每个 `InputStream` 用完后必须立即关闭,否则会阻塞迭代器读取下一个 Blob。 - **BlobDataIterator 必须关闭**:务必使用 try-with-resources 确保底层网络连接正确释放。 - **迭代顺序**:`batchDownload()` 返回的数据顺序与输入的 Blob 引用列表顺序一致。 - **行接口 vs Arrow 接口**:推荐使用行接口(`RecordReader`)读取 Blob 列,SDK 自动处理引用格式转换;使用 Arrow 列接口需要手动从字节解码引用字符串。 - **异常处理**:`BlobDownloadException` 提供 `getFailedBlobRef()` 方法,可获取下载失败的具体 Blob 引用,便于重试。 ## 相关文档 - [Storage API 读取](./storage-api-read.md) - Storage API 读取表数据 - [读取数据概览](./index.md) - 三种方式对比与选型 --- # guides/read-data/index.md # 读取数据 MaxCompute Java SDK 提供三种数据读取方式:数据预览(Table.read)、Tunnel 下载和 Storage API 读取,分别适用于不同的数据规模和性能需求。 ## 方式对比 | 特性 | Table.read(预览) | Tunnel Download | Storage API | |------|-------------------|-----------------|-------------| | **模块** | odps-sdk-core | odps-sdk-tunnel | odps-sdk-storage-api | | **数据格式** | Record | Record / Arrow | Arrow | | **最大数据量** | 1万行 / 10MB | 无限制 | 无限制 | | **并行读取** | 不支持 | 支持(按行号分块) | 支持(按 Split 分片) | | **列裁剪** | 支持 | 支持 | 支持 | | **谓词下推** | 不支持 | 不支持 | 支持 | | **分区过滤** | 支持 | 支持 | 支持 | | **Transactional 表** | 支持 | 不支持 | 支持 | | **Blob 类型** | 不支持 | 不支持 | 支持 | | **增量读取** | 不支持 | 不支持 | 支持 | | **适用版本** | >= 0.47.0 | >= 0.48.0 | >= 0.52.0 | ## 选型决策 ``` 需要读取表数据 │ ├─ 数据量 < 1万行,仅用于预览/调试? │ → Table.read(数据预览) │ ├─ 批量导出数据,对性能要求一般? │ → Tunnel Download │ └─ 以下任一场景: ├─ 需要高性能并行读取(Arrow 列式格式) ├─ 需要谓词下推减少数据传输 ├─ 需要读取 Transactional 表 ├─ 需要读取 Blob 类型数据 └─ 需要增量读取 → Storage API ``` ## 快速开始 ### 数据预览(最简单) ```java RecordReader reader = table.read(100); Record record; while ((record = reader.read()) != null) { System.out.println(record.get(0)); } ``` ```python table = odps.get_table('my_table') records = table.head(100) for record in records: print(record[0]) ``` ### Tunnel 下载(批量导出) ```java TableTunnel tunnel = new TableTunnel(odps); DownloadSession session = tunnel.buildDownloadSession() .setProjectName("my_project") .setTableName("my_table") .build(); try (TunnelRecordReader reader = session.openRecordReader(0, session.getRecordCount())) { while (reader.hasNext()) { Record record = reader.next(); // 处理数据... } } ``` ```python # 使用 Tunnel 下载数据 with odps.get_table('my_table').open_reader() as reader: for record in reader: # 处理数据... pass ``` ### Storage API(高性能) ```java MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "name")) .build(); for (InputSplit split : session.getSplits()) { try (ArrowReader reader = session.createReaderBuilder(split).build()) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); // 处理 Arrow 批次数据... } } } ``` ```python from odps.apis.storage_api_v2 import StorageApiArrowClient # 使用 Storage API 高性能读取 table = odps.get_table('my_table') client = StorageApiArrowClient(odps, table) resp = client.create_read_session(required_data_columns=["id", "name"]) reader = client.read_rows_arrow(resp.session_id, split_index=0) batch = reader.read() print(batch.to_pandas()) ``` ## 相关文档 - [数据预览(Table.read)](./preview.md) - 快速预览少量数据 - [Tunnel 下载](./tunnel-download.md) - 批量导出表数据 - [Storage API 读取](./storage-api-read.md) - 高性能并行读取 - [Blob 数据下载](./blob.md) - 非结构化二进制数据读取 --- # guides/read-data/preview.md # 数据预览(Table.read) `Table.read()` 是最简单的数据读取方式,适用于快速预览表中少量数据,无需额外配置 Tunnel 或 Storage API 客户端。 ## 前置条件 - 已初始化 `Odps` 客户端对象 - 对目标表的读取列具有 `select` 权限 - SDK 版本 >= 0.47.0-public ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.PartitionSpec; import com.aliyun.odps.Table; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.RecordReader; import java.util.Arrays; import java.util.List; public class TableReadExample { public static void main(String[] args) throws Exception { // 假设 odps 客户端已初始化 Odps odps = getOdpsClient(); // 获取表对象 Table table = odps.tables().get("my_project", "user_info"); // 示例1:基本读取,最多返回100行 RecordReader reader = table.read(100); Record record; while ((record = reader.read()) != null) { System.out.println("id=" + record.get("id") + ", name=" + record.get("name")); } // 示例2:指定分区和列读取 PartitionSpec partition = new PartitionSpec("dt=20250101"); List columns = Arrays.asList("id", "name", "age"); RecordReader partitionReader = table.read(partition, columns, 500); while ((record = partitionReader.read()) != null) { System.out.println("id=" + record.get("id") + ", name=" + record.get("name") + ", age=" + record.get("age")); } // 示例3:非分区表读取全部列 RecordReader fullReader = table.read(null, null, 1000); while ((record = fullReader.read()) != null) { // 处理每行数据... } } } ``` ```python from odps import ODPS # 假设 odps 客户端已初始化 odps = ODPS('access_id', 'access_key', 'my_project', endpoint='http://service.odps.aliyun.com/api') # 获取表对象 table = odps.get_table('user_info') # 示例1:基本读取,最多返回100行 records = table.head(100) for record in records: print(f"id={record['id']}, name={record['name']}") # 示例2:指定分区和列读取 records = table.head(500, partition='dt=20250101', columns=['id', 'name', 'age']) for record in records: print(f"id={record['id']}, name={record['name']}, age={record['age']}") # 示例3:非分区表读取全部列 records = table.head(1000) for record in records: # 处理每行数据... pass ``` ## 代码说明 1. **获取表对象**:通过 `odps.tables().get(projectName, tableName)` 获取 `Table` 实例。 2. **调用 read 方法**:`table.read(limit)` 是最简形式,仅指定最大行数;`table.read(partition, columns, limit)` 支持指定分区和列。 3. **迭代 RecordReader**:循环调用 `reader.read()`,返回 `null` 表示读取完毕。 4. **获取字段值**:通过 `record.get("columnName")` 或 `record.get(index)` 获取字段值,返回值类型参见下方类型映射表。 ## 配置选项 | 参数 | 类型 | 必需 | 说明 | |------|------|------|------| | `limit` | int | 是 | 最多读取的行数,上限为 1 万行 | | `partition` | PartitionSpec | 否 | 分区规格,非分区表传 `null` | | `columns` | List\ | 否 | 指定读取的列名列表,传 `null` 读取全部列 | | `timezone` | String | 否 | datetime 类型的时区设置(新接口使用 Java8 无时区类型,此参数无效) | | `useLegacyMode` | boolean | 否 | 是否使用兼容旧接口模式,默认 `false`(不推荐开启) | | `tunnelEndpoint` | String | 否 | 自定义 Tunnel Endpoint,通常由 SDK 自动获取 | ## 类型映射 RecordReader 返回的 Record 中,OdpsType 与 Java 类型的映射关系如下: | OdpsType | JavaType | |----------|----------| | TINYINT | java.lang.Byte | | SMALLINT | java.lang.Short | | INT | java.lang.Integer | | BIGINT | java.lang.Long | | BINARY | com.aliyun.odps.data.Binary | | FLOAT | java.lang.Float | | DOUBLE | java.lang.Double | | DECIMAL(precision,scale) | java.math.BigDecimal | | VARCHAR(n) | com.aliyun.odps.data.Varchar | | CHAR(n) | com.aliyun.odps.data.Char | | STRING | java.lang.String | | DATE | java.time.LocalDate | | DATETIME | java.time.ZonedDateTime | | TIMESTAMP | java.time.Instant | | TIMESTAMP_NTZ | java.time.LocalDateTime | | BOOLEAN | java.lang.Boolean | | ARRAY | java.util.ArrayList | | MAP | java.util.HashMap | | STRUCT | com.aliyun.odps.data.SimpleStruct | | JSON | com.aliyun.odps.data.JsonValue | :::info 时间类型变更(0.47.0) 从 0.47.0-public 版本起,时间类型使用 Java 8 推荐的无时区类型: | OdpsType | 新版本(>= 0.47.0) | 旧版本 | |----------|---------------------|--------| | DATE | java.time.LocalDate | java.sql.Date | | DATETIME | java.time.ZonedDateTime | java.util.Date | | TIMESTAMP | java.time.Instant | java.sql.Timestamp | 如需兼容旧行为,可设置 `useLegacyMode = true`,但会有额外性能开销。 ::: ## 注意事项 - **行数限制**:最多返回 1 万行记录,超出部分会被截断,不会报错。 - **数据量限制**:单次预览数据量上限约 10MB。 - **不保证有序**:read 接口的返回数据不保证顺序。 - **仅适用于预览**:对于大量数据读取,应使用 [Tunnel 下载](./tunnel-download.md) 或 [Storage API](./storage-api-read.md)。 - **底层实现**:自 0.47.0 起,read 接口底层使用 TableTunnel 的 preview 方法实现。 - **Tunnel Endpoint**:通常由 SDK 自动获取,无需手动指定。 ## 相关文档 - [Tunnel 下载](./tunnel-download.md) - 适用于大批量数据导出 - [Storage API 读取](./storage-api-read.md) - 适用于高性能并行读取 - [读取数据概览](./index.md) - 三种方式对比与选型 --- # guides/read-data/storage-api-read.md # Storage API 读取 Storage API 是 MaxCompute 提供的高性能数据读取接口,基于 Apache Arrow 列式内存格式,支持列裁剪、谓词下推、分区过滤和并行分片读取,适用于大规模数据处理场景。 :::note Go SDK 暂不支持 Storage API。 ::: ## 前置条件 - 添加 `odps-sdk-storage-api` 模块依赖 - 拥有目标表的读取权限 - SDK 版本 >= 0.52.0 ## 完整示例 ```java import com.aliyun.odps.storage.api.MaxStorageClient; import com.aliyun.odps.storage.api.TableIdentifier; import com.aliyun.odps.storage.api.read.TableReadSession; import com.aliyun.odps.storage.api.read.InputSplit; import com.aliyun.odps.storage.api.read.SplitOptions; import org.apache.arrow.vector.VectorSchemaRoot; import org.apache.arrow.vector.BigIntVector; import org.apache.arrow.vector.VarCharVector; import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.ipc.ArrowReader; import java.util.Arrays; import java.util.List; public class StorageApiReadExample { public static void main(String[] args) throws Exception { // 1. 创建 MaxStorageClient MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); // 2. 指定表标识 TableIdentifier tableId = TableIdentifier.of("my_project", "user_behavior"); // 3. 创建 TableReadSession,配置列裁剪和过滤条件 TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("user_id", "action", "timestamp")) .withFilter("action = 'purchase' AND timestamp > '2025-01-01'") .withSplitOptions(SplitOptions.newBuilder() .withSplitSize(256 * 1024 * 1024L) // 每个 Split 约 256MB .build()) .build(); // 4. 获取 Split 列表 List splits = session.getSplits(); System.out.println("分片数量: " + splits.size()); // 5. 逐个 Split 读取数据 long totalRows = 0; for (InputSplit split : splits) { try (ArrowReader reader = session.createReaderBuilder(split).build()) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); int rowCount = root.getRowCount(); totalRows += rowCount; // 按列获取 Vector 进行处理 BigIntVector userIdVector = (BigIntVector) root.getVector("user_id"); VarCharVector actionVector = (VarCharVector) root.getVector("action"); for (int i = 0; i < rowCount; i++) { long userId = userIdVector.get(i); String action = new String(actionVector.get(i)); // 处理业务逻辑... } } } } System.out.println("总读取行数: " + totalRows); } } ``` ```python from odps import ODPS from odps.apis.storage_api_v2 import StorageApiArrowClient o = ODPS(access_id, secret_access_key, project='my_project', endpoint=endpoint) table = o.get_table("my_table") client = StorageApiArrowClient(o, table) # 创建读取会话 resp = client.create_read_session( required_data_columns=["id", "name"], required_partitions=["dt=20231001"], ) # 按分片并行读取 for split_index in range(resp.splits_count): reader = client.read_rows_arrow(resp.session_id, split_index=split_index) while True: batch = reader.read() if batch is None: break df = batch.to_pandas() print(df) ``` ## 代码说明 1. **创建客户端**:通过 `MaxStorageClient.builder()` 配置 Endpoint 和认证信息,构建客户端实例。 2. **指定表标识**:使用 `TableIdentifier.of(project, table)` 标识目标表。 3. **构建 ReadSession**:通过 Builder 配置读取参数(列裁剪、过滤条件、分片策略等),调用 `build()` 创建 Session。 4. **获取 Split 列表**:`session.getSplits()` 返回所有可并行读取的数据分片。 5. **读取 Arrow 数据**:对每个 Split 创建 `ArrowReader`,循环调用 `loadNextBatch()` 读取数据批次,通过 `VectorSchemaRoot` 访问列式数据。 ## 配置选项 ### TableReadSessionBuilder 参数 | 方法 | 说明 | 默认值 | |------|------|--------| | `withColumns(List)` | 列裁剪,指定需要读取的数据列 | 全部列 | | `withPartitionColumns(List)` | 指定需要返回的分区列 | - | | `withPartitions(List)` | 分区过滤,仅读取指定分区 | 全部分区 | | `withFilter(String)` | 谓词下推,服务端过滤数据 | - | | `enableFilterFallback(boolean)` | 不支持的过滤条件是否回退为全表扫描 | false | | `withSplitOptions(SplitOptions)` | 分片策略配置 | 256MB/Split | | `withBucketIds(List)` | 按 Bucket ID 过滤(分桶表) | - | | `withSessionReadyTimeout(long)` | Session 就绪超时(秒) | 3600 | | `withSessionId(String)` | 复用已有 Session | - | | `withIncrementalReadOptions(...)` | 增量读取配置 | - | | `withIncrementalReadEnabled(boolean)` | 启用增量读取 | false | ### SplitOptions 分片策略 | 方法 | 说明 | 默认值 | |------|------|--------| | `withSplitSize(long bytes)` | 按字节大小分片 | 256MB | | `withSplitRowCount(long)` | 按行数分片 | - | | `withCrossPartition(boolean)` | 是否允许跨分区的 Split | true | ### TableReaderBuilder 参数 | 方法 | 说明 | |------|------| | `withMaxBatchRows(long)` | 每批最大行数 | | `withMaxBatchRawSize(long)` | 每批最大原始数据大小(字节) | | `withSkipRowNum(long)` | 跳过的行数 | | `withDataColumns(List)` | Split 级别的列选择 | ## 并行读取 利用线程池对多个 Split 并行处理,充分发挥 Storage API 的吞吐优势: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; public class ParallelStorageApiRead { public static void main(String[] args) throws Exception { MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); TableIdentifier tableId = TableIdentifier.of("my_project", "large_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "name", "score")) .withSplitOptions(SplitOptions.newBuilder() .withSplitSize(256 * 1024 * 1024L) .build()) .build(); List splits = session.getSplits(); System.out.println("分片数量: " + splits.size()); ExecutorService executor = Executors.newFixedThreadPool(splits.size()); AtomicLong totalRows = new AtomicLong(0); List> futures = new ArrayList<>(); for (InputSplit split : splits) { futures.add(executor.submit(() -> { try (ArrowReader reader = session.createReaderBuilder(split).build()) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); totalRows.addAndGet(root.getRowCount()); // 处理数据... } } catch (Exception e) { throw new RuntimeException(e); } })); } for (Future f : futures) { f.get(); } executor.shutdown(); System.out.println("总读取行数: " + totalRows.get()); } } ``` ```python import concurrent.futures table = o.get_table("large_table") client = StorageApiArrowClient(o, table) resp = client.create_read_session( required_data_columns=["id", "name", "score"], ) print(f"分片数量: {resp.splits_count}") total_rows = 0 def read_split(split_index): rows = 0 reader = client.read_rows_arrow(resp.session_id, split_index=split_index) while True: batch = reader.read() if batch is None: break rows += batch.num_rows # 处理数据... return rows with concurrent.futures.ThreadPoolExecutor(max_workers=resp.splits_count) as pool: results = pool.map(read_split, range(resp.splits_count)) total_rows = sum(results) print(f"总读取行数: {total_rows}") ``` ## 读取分区表 ```java TableReadSession session = client.createTableReadSessionBuilder(tableId) .withPartitions(Arrays.asList( new PartitionSpec("dt='20250101'"), new PartitionSpec("dt='20250102'") )) .withPartitionColumns(Arrays.asList("dt")) // 同时返回分区列值 .withColumns(Arrays.asList("id", "name")) .build(); ``` ```python resp = client.create_read_session( required_data_columns=["id", "name"], required_partitions=["dt=20250101", "dt=20250102"], ) ``` ## 增量读取 读取指定时间范围内的变化数据: ```java IncrementalReadOptions options = IncrementalReadOptions.newBuilder() .withMode("timestamp") .withStartTimeStamp("2025-01-01 00:00:00") .withEndTimeStamp("2025-01-02 00:00:00") .build(); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withIncrementalReadOptions(options) .withIncrementalReadEnabled(true) .build(); ``` ```python from odps.apis.storage_api_v2 import IncrementalReadOptions # Storage API 增量读取 incr_opts = IncrementalReadOptions() incr_opts.mode = "timestamp" incr_opts.start_time_stamp = "2025-01-01 00:00:00" incr_opts.end_time_stamp = "2025-01-02 00:00:00" resp = client.create_read_session( required_data_columns=["id", "name"], incremental_read=True, incremental_read_options=incr_opts, ) ``` ## 注意事项 - **Arrow 内存管理**:`VectorSchemaRoot` 中的数据在下一次 `loadNextBatch()` 调用后会被覆盖,如需保留数据请在当前批次内完成处理或拷贝。 - **资源释放**:务必使用 try-with-resources 关闭 `ArrowReader`,避免内存泄漏。 - **谓词下推限制**:并非所有表达式都支持下推,不支持时默认抛出异常;如需降级为全表扫描,请设置 `enableFilterFallback(true)`。 - **Session 复用**:对于重复读取相同数据的场景,可通过 `session.getId()` 保存 Session ID,后续用 `withSessionId()` 复用,避免重复创建。 - **Split 并行度**:Split 数量由服务端根据数据量和 SplitOptions 决定,建议线程池大小不超过 Split 数量。 - **支持 Transactional 表**:Storage API 支持读取 Transactional 表,这是相比 Tunnel 的重要优势。 ## 相关文档 - [Tunnel 下载](./tunnel-download.md) - 适用于简单批量导出 - [Blob 数据下载](./blob.md) - 读取非结构化二进制数据 - [读取数据概览](./index.md) - 三种方式对比与选型 --- # guides/read-data/tunnel-download.md # Tunnel 下载 TableTunnel 是 MaxCompute 的标准数据通道,适用于大批量数据的导出场景。支持压缩传输、多线程并行下载,无行数限制。 ## 前置条件 - 已初始化 `Odps` 客户端对象 - 添加 `odps-sdk-tunnel` 模块依赖 - 对目标表具有读取权限 - SDK 版本 >= 0.48.0 ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.PartitionSpec; import com.aliyun.odps.data.Record; import com.aliyun.odps.tunnel.TableTunnel; import com.aliyun.odps.tunnel.TableTunnel.DownloadSession; import com.aliyun.odps.tunnel.io.CompressOption; import com.aliyun.odps.tunnel.io.TunnelRecordReader; public class TunnelDownloadExample { public static void main(String[] args) throws Exception { // 假设 odps 客户端已初始化 Odps odps = getOdpsClient(); // 1. 创建 TableTunnel 对象 TableTunnel tunnel = new TableTunnel(odps); // 2. 创建 DownloadSession DownloadSession session = tunnel.buildDownloadSession() .setProjectName("my_project") .setTableName("user_behavior") .setPartitionSpec(new PartitionSpec("dt=20250101")) .build(); // 3. 获取总行数 long totalRecords = session.getRecordCount(); System.out.println("总记录数: " + totalRecords); // 4. 打开 Reader 并读取数据 try (TunnelRecordReader reader = session.openRecordReader(0, totalRecords)) { while (reader.hasNext()) { Record record = reader.next(); System.out.println("user_id=" + record.get("user_id") + ", action=" + record.get("action")); } } } } ``` ```python # 读取整张表 with table.open_reader() as reader: for record in reader: print(record) # 读取分区表 with table.open_reader(partition='dt=20231001') as reader: for record in reader: print(record) # Arrow 格式读取(高性能) with table.open_reader(arrow=True) as reader: for batch in reader: df = batch.to_pandas() # 指定列读取 with table.open_reader(columns=['id', 'name']) as reader: for record in reader: print(record) ``` ```go project := odpsIns.DefaultProject() tunnelEndpoint, _ := project.GetTunnelEndpoint() tunnelIns := tunnel.NewTunnel(odpsIns, tunnelEndpoint) // 创建下载会话 session, _ := tunnelIns.CreateDownloadSession( project.Name(), "my_table", tunnel.SessionCfg.WithPartitionKey("dt=20231001"), ) // 读取数据 recordCount := session.RecordCount() reader, _ := session.OpenRecordReader(0, recordCount, nil) reader.Iterator(func(record data.Record, err error) { if err != nil { return } fmt.Println(record) }) reader.Close() // Arrow 格式读取 arrowReader, _ := session.OpenRecordArrowReader(0, recordCount, nil) arrowReader.Iterator(func(rec array.Record, err error) { // 处理 Arrow RecordBatch }) ``` ## 代码说明 1. **创建 TableTunnel**:基于已初始化的 `Odps` 对象创建 `TableTunnel` 实例,作为 Tunnel 操作的入口。 2. **构建 DownloadSession**:通过 Builder 模式设置项目名、表名、分区等参数,调用 `build()` 创建会话。会话创建时服务端会准备数据快照。 3. **获取记录总数**:`session.getRecordCount()` 返回可下载的记录总行数,用于规划分块下载。 4. **打开 Reader**:`openRecordReader(start, count)` 指定起始行号和读取行数,返回 `TunnelRecordReader`。 5. **迭代读取**:通过 `hasNext()` / `next()` 逐条读取 `Record` 对象。 ## 配置选项 ### DownloadSession 构建参数 | 方法 | 类型 | 必需 | 默认值 | 说明 | |------|------|------|--------|------| | `setProjectName(String)` | String | 是 | - | 目标项目名称 | | `setTableName(String)` | String | 是 | - | 目标表名称 | | `setPartitionSpec(PartitionSpec)` | PartitionSpec | 否 | null | 分区规格,分区表必须指定 | | `setShardId(Long)` | Long | 否 | null | 指定分片 ID | | `setAsyncMode(boolean)` | boolean | 否 | false | 异步初始化模式(大表推荐) | | `setWaitAsyncBuild(boolean)` | boolean | 否 | false | 阻塞等待异步会话就绪 | ### openRecordReader 参数 | 参数 | 类型 | 约束 | 说明 | |------|------|------|------| | `start` | long | >= 0 | 读取起始行号 | | `count` | long | >= 1 | 读取记录数量 | | `option` | CompressOption | - | 压缩配置(默认启用 zlib 压缩) | | `columns` | List\ | 非空 | 指定下载的列集合(列裁剪) | | `disableModifiedCheck` | boolean | - | 禁用数据版本校验 | ### CompressOption 压缩选项 | 压缩算法 | 说明 | |----------|------| | `CompressOption.CompressAlgorithm.ODPS_RAW` | 不压缩 | | `CompressOption.CompressAlgorithm.ODPS_ZLIB` | zlib 压缩(默认) | | `CompressOption.CompressAlgorithm.ODPS_SNAPPY` | Snappy 压缩(更快,压缩比略低) | ## 多线程并行下载 对于大数据量场景,可将数据按行号范围拆分为多个块,使用线程池并行下载: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.ArrayList; import java.util.List; public class ParallelDownloadExample { public static void main(String[] args) throws Exception { Odps odps = getOdpsClient(); TableTunnel tunnel = new TableTunnel(odps); DownloadSession session = tunnel.buildDownloadSession() .setProjectName("my_project") .setTableName("large_table") .build(); long totalRecords = session.getRecordCount(); int parallelism = 8; // 根据 CPU 核心数和网络带宽设置 ExecutorService pool = Executors.newFixedThreadPool(parallelism); List> futures = new ArrayList<>(); for (int i = 0; i < parallelism; i++) { long start = i * (totalRecords / parallelism); long count = (i == parallelism - 1) ? totalRecords - start : totalRecords / parallelism; futures.add(pool.submit(() -> { try (TunnelRecordReader reader = session.openRecordReader(start, count, new CompressOption(CompressOption.CompressAlgorithm.ODPS_ZLIB, 1, 0))) { while (reader.hasNext()) { Record record = reader.next(); processRecord(record); } } catch (Exception e) { throw new RuntimeException("下载失败, start=" + start, e); } })); } // 等待所有下载任务完成 for (Future future : futures) { future.get(); } pool.shutdown(); System.out.println("下载完成,共 " + totalRecords + " 条记录"); } } ``` ```python import concurrent.futures table = o.get_table('large_table') # 方式1: 使用 Arrow 格式高效读取(自动批次处理,推荐) with table.open_reader(arrow=True) as reader: for batch in reader: df = batch.to_pandas() process_dataframe(df) # 方式2: 按分区并行读取 def read_partition(pt_spec): with table.open_reader(partition=pt_spec, arrow=True) as reader: for batch in reader: process_dataframe(batch.to_pandas()) partitions = [str(p.partition_spec) for p in table.partitions] with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool: pool.map(read_partition, partitions) print("下载完成") ``` ```go session, _ := tunnelIns.CreateDownloadSession( project.Name(), "large_table", ) totalRecords := session.RecordCount() parallelism := 8 chunkSize := totalRecords / int64(parallelism) var wg sync.WaitGroup for i := 0; i < parallelism; i++ { wg.Add(1) go func(idx int) { defer wg.Done() start := int64(idx) * chunkSize count := chunkSize if idx == parallelism-1 { count = totalRecords - start } reader, _ := session.OpenRecordReader(start, count, nil) reader.Iterator(func(record data.Record, err error) { if err != nil { return } processRecord(record) }) reader.Close() }(i) } wg.Wait() fmt.Printf("下载完成,共 %d 条记录\n", totalRecords) ``` ## 异步创建模式(大表) 对于数据量特别大的表,Session 创建可能需要较长时间。可以使用异步模式避免阻塞: ```java TableTunnel.DownloadSessionBuilder builder = tunnel.buildDownloadSession() .setProjectName("big_data_project") .setTableName("huge_table") .setAsyncMode(true); DownloadSession session = builder.build(); // 轮询等待会话就绪,每 5 秒检查一次,最长等待 300 秒 boolean ready = builder.wait(session, 5, 300); if (ready) { System.out.println("会话就绪,记录数: " + session.getRecordCount()); // 开始下载... } else { System.err.println("会话创建超时"); } ``` ```python # PyODPS 自动处理大表的会话创建,无需手动配置异步模式 table = o.get_table('huge_table') with table.open_reader() as reader: print(f"会话就绪,记录数: {reader.count}") for record in reader: process_record(record) ``` ```go // Go SDK 在 CreateDownloadSession 时自动处理会话初始化 // 无需额外的异步模式配置 session, _ := tunnelIns.CreateDownloadSession( project.Name(), "huge_table", ) recordCount := session.RecordCount() fmt.Printf("会话就绪,记录数: %d\n", recordCount) ``` ## 注意事项 - **不支持 Transactional 表**:Tunnel Download 目前不支持 Transactional 表的下载,请使用 [Storage API](./storage-api-read.md)。 - **超时机制**:默认 300 秒无数据传输时,服务端会断开连接。对于处理逻辑较重的场景,建议缩小每次的 `count` 值。 - **数据一致性**:DownloadSession 创建时会生成数据快照,读取期间数据不会变化。 - **分区表必须指定分区**:对于分区表,`setPartitionSpec()` 是必需的,否则会抛出异常。 - **资源释放**:务必使用 try-with-resources 语法关闭 `TunnelRecordReader`,避免连接泄漏。 - **并行度设置**:建议根据 CPU 核心数和网络带宽合理设置,一般 4-16 个线程较为合适。 ## 相关文档 - [数据预览(Table.read)](./preview.md) - 适用于少量数据快速预览 - [Storage API 读取](./storage-api-read.md) - 适用于高性能场景和 Transactional 表 - [读取数据概览](./index.md) - 三种方式对比与选型 --- # guides/security/acl-query.md # ACL 查询 在 MaxCompute 中,可以通过 `SecurityManager` 执行 ACL(Access Control List)权限命令来管理访问控制。ACL 命令支持授权、撤销、查看权限、角色管理等操作,与在 MaxCompute Console 中执行权限 SQL 的效果一致。 ## 前置条件 - 已完成 [认证配置](../../getting-started/authentication.md) - 已添加 `odps-sdk-core` 依赖 - 当前账号需要有项目的 Admin 角色或相应的管理权限 ## 完整示例 以下示例演示如何通过 SDK 执行常见的 ACL 权限命令: ```java import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.security.SecurityManager; import com.aliyun.odps.utils.StringUtils; public class AclQueryExample { public static void main(String[] args) throws OdpsException { // 初始化 Odps 客户端 Odps odps = new Odps(new AliyunAccount("accessId", "accessKey")); odps.setDefaultProject("my_project"); odps.setEndpoint("http://service.odps.aliyun.com/api"); // 获取 SecurityManager SecurityManager sm = odps.projects().get().getSecurityManager(); // 1. 授予用户对表的 Select 权限 executeCommand(sm, "GRANT SELECT ON TABLE sales_data TO USER alice;"); // 2. 查看用户的权限 executeCommand(sm, "SHOW GRANTS FOR USER alice;"); // 3. 创建角色并绑定权限 executeCommand(sm, "CREATE ROLE analyst;"); executeCommand(sm, "GRANT SELECT ON TABLE sales_data TO ROLE analyst;"); executeCommand(sm, "GRANT analyst TO alice;"); // 4. 查看角色的权限 executeCommand(sm, "DESCRIBE ROLE analyst;"); // 5. 撤销权限 executeCommand(sm, "REVOKE SELECT ON TABLE sales_data FROM USER alice;"); // 6. 查看当前用户的权限 executeCommand(sm, "SHOW GRANTS;"); } /** * 执行 ACL 命令并输出结果 */ private static void executeCommand(SecurityManager sm, String command) throws OdpsException { System.out.println("执行: " + command); // 提交命令 SecurityManager.AuthorizationQueryInstance instance = sm.run(command, false, null, null); // 等待执行完成 while (!instance.isTerminated()) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new OdpsException("命令执行被中断"); } } // 检查执行状态 if (instance.getStatus() == SecurityManager.AuthorizationQueryStatus.FAILED) { throw new OdpsException("命令执行失败: " + instance.getResult()); } // 输出结果 String result = instance.getResult(); if (StringUtils.isNullOrEmpty(result)) { System.out.println("OK\n"); } else { System.out.println(result + "\n"); } } } ``` ```python from odps import ODPS # 初始化 ODPS 客户端 odps = ODPS('access_id', 'access_key', 'my_project', endpoint='http://service.odps.aliyun.com/api') # 1. 授予用户对表的 Select 权限 odps.run_security_query("GRANT SELECT ON TABLE sales_data TO USER alice;") # 2. 查看用户的权限 result = odps.run_security_query("SHOW GRANTS FOR USER alice;") print(result) # 3. 创建角色并绑定权限 odps.run_security_query("CREATE ROLE analyst;") odps.run_security_query("GRANT SELECT ON TABLE sales_data TO ROLE analyst;") odps.run_security_query("GRANT analyst TO alice;") # 4. 查看角色的权限 result = odps.run_security_query("DESCRIBE ROLE analyst;") print(result) # 5. 撤销权限 odps.run_security_query("REVOKE SELECT ON TABLE sales_data FROM USER alice;") # 6. 查看当前用户的权限 result = odps.run_security_query("SHOW GRANTS;") print(result) ``` ```go package main import ( "fmt" "log" "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" "github.com/aliyun/aliyun-odps-go-sdk/odps/security" ) func main() { acc := account.NewAliyunAccount("accessId", "accessKey") odpsIns := odps.NewOdps(acc, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("my_project") // 获取 SecurityManager sm := odpsIns.Projects().GetDefaultProject().SecurityManager() // 1. 授予用户对表的 Select 权限 executeCommand(sm, "GRANT SELECT ON TABLE sales_data TO USER alice;") // 2. 查看用户的权限 executeCommand(sm, "SHOW GRANTS FOR USER alice;") // 3. 创建角色并绑定权限 executeCommand(sm, "CREATE ROLE analyst;") executeCommand(sm, "GRANT SELECT ON TABLE sales_data TO ROLE analyst;") executeCommand(sm, "GRANT analyst TO alice;") // 4. 查看角色的权限 executeCommand(sm, "DESCRIBE ROLE analyst;") // 5. 撤销权限 executeCommand(sm, "REVOKE SELECT ON TABLE sales_data FROM USER alice;") // 6. 查看当前用户的权限 executeCommand(sm, "SHOW GRANTS;") } func executeCommand(sm security.Manager, command string) { result, err := sm.RunQuery(command, false, "") if err != nil { log.Fatalf("命令执行失败: %+v", err) } if result != "" { fmt.Println(result) } else { fmt.Println("OK") } } ``` ## 代码说明 ### 获取 SecurityManager ```java SecurityManager sm = odps.projects().get().getSecurityManager(); ``` ```python # Python SDK 中无需显式获取 SecurityManager,直接通过 odps 实例调用 odps.run_security_query("SHOW GRANTS;") ``` ```go sm := odpsIns.Projects().GetDefaultProject().SecurityManager() ``` ### 执行 ACL 命令 通过 `run` 方法提交 ACL 命令,返回一个异步查询实例: ```java SecurityManager.AuthorizationQueryInstance instance = sm.run(command, false, null, null); ``` ```python result = odps.run_security_query(command) ``` ```go instance, err := sm.Run(command, false, "") ``` 参数说明: - `command`:要执行的 ACL SQL 命令 - `useJson`:是否以 JSON 格式返回结果(`false` 表示普通文本格式) - `projectName`:指定在哪个项目上下文中执行(`null` 使用默认项目) - `supervisorToken`:supervisor token(通常为 `null`) ### 等待命令完成 ACL 命令是异步执行的,需要轮询等待完成: ```java while (!instance.isTerminated()) { Thread.sleep(1000); } ``` ```python # Python SDK 的 run_security_query 会自动等待执行完成并返回结果 result = odps.run_security_query(command) ``` ```go // 使用 WaitForSuccess 等待执行完成并获取结果 result, err := instance.WaitForSuccess() ``` ### 获取执行结果 ```java // 检查是否失败 if (instance.getStatus() == SecurityManager.AuthorizationQueryStatus.FAILED) { System.err.println("失败: " + instance.getResult()); } // 获取结果文本 String result = instance.getResult(); ``` ```python # run_security_query 直接返回结果,失败时抛出异常 try: result = odps.run_security_query(command) print(result) except Exception as e: print(f"失败: {e}") ``` ```go // RunQuery 直接返回结果,失败时返回 error result, err := sm.RunQuery(command, false, "") if err != nil { log.Fatalf("失败: %+v", err) } fmt.Println(result) ``` ## 常见 ACL 命令 ### 授权(GRANT) ```sql -- 授予用户对表的权限 GRANT SELECT ON TABLE table_name TO USER user_name; -- 授予角色对项目的权限 GRANT CreateInstance ON PROJECT project_name TO ROLE role_name; -- 授予用户对 Package 的权限 GRANT Read ON PACKAGE project_name.package_name TO USER user_name; ``` ### 撤销(REVOKE) ```sql -- 撤销用户对表的权限 REVOKE SELECT ON TABLE table_name FROM USER user_name; -- 撤销角色对项目的权限 REVOKE CreateInstance ON PROJECT project_name FROM ROLE role_name; ``` ### 查看权限(SHOW GRANTS) ```sql -- 查看指定用户的权限 SHOW GRANTS FOR USER user_name; -- 查看指定角色的权限 SHOW GRANTS FOR ROLE role_name; -- 查看当前用户的权限 SHOW GRANTS; ``` ### 角色管理 ```sql -- 创建角色 CREATE ROLE role_name; -- 将角色授予用户 GRANT role_name TO user_name; -- 查看角色详情 DESCRIBE ROLE role_name; -- 删除角色 DROP ROLE role_name; -- 列出所有角色 LIST ROLES; ``` ### 用户管理 ```sql -- 添加用户 ADD USER user_name; -- 移除用户 REMOVE USER user_name; -- 列出所有用户 LIST USERS; ``` ### 清除过期授权 ```sql CLEAR EXPIRED GRANTS; ``` ## 配置选项 ### run 方法参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | command | String | - | ACL SQL 命令(必填) | | useJson | boolean | false | 是否以 JSON 格式返回结果 | | projectName | String | null | 目标项目名(null 使用默认项目) | | supervisorToken | String | null | Supervisor Token | ### AuthorizationQueryStatus 状态 | 状态 | 说明 | |------|------| | `SUCCESS` | 命令执行成功 | | `FAILED` | 命令执行失败 | ## 注意事项 - ACL 命令是异步执行的,必须等待 `isTerminated()` 返回 `true` 后再获取结果 - 执行授权/撤销操作需要当前账号拥有 Admin 角色或 Super_Administrator 角色 - ACL 命令的语法与 MaxCompute Console 中的权限 SQL 完全一致 - 命令执行失败时,通过 `getResult()` 获取错误信息进行排查 - 建议设置合理的轮询间隔(如 1-3 秒),避免频繁请求 - 批量执行多条 ACL 命令时,建议逐条执行并检查结果,而非拼接为一条 ## 相关文档 - [权限校验](./check-permission.md) - 检查用户是否拥有特定权限 - [ACL 权限命令参考](./acl-query.md) - 完整 ACL 命令列表 - [MaxCompute 权限管理](https://help.aliyun.com/zh/maxcompute/user-guide/maxcompute-permissions) - 官方权限文档 --- # guides/security/check-permission.md # 权限校验 在 MaxCompute 中,可以通过 `SecurityManager` 的 `checkPermission` 方法检查当前用户是否拥有对特定对象的操作权限。这在需要在业务逻辑中做权限预检时非常有用,可以避免在执行操作时才发现权限不足。 ## 前置条件 - 已完成 [认证配置](../../getting-started/authentication.md) - 已添加 `odps-sdk-core` 依赖 - 当前账号需要有目标项目的访问权限 ## 完整示例 以下示例演示如何检查用户对表的 Select 权限: ```java import com.aliyun.odps.Odps; import com.aliyun.odps.OdpsException; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.security.CheckPermissionResult; import com.aliyun.odps.security.CheckPermissionResultInfo; import com.aliyun.odps.security.SecurityManager; import com.aliyun.odps.type.ActionType; import com.aliyun.odps.type.ObjectType; import com.aliyun.odps.security.PermissionDesc; public class CheckPermissionExample { public static void main(String[] args) throws OdpsException { // 初始化 Odps 客户端 Odps odps = new Odps(new AliyunAccount("accessId", "accessKey")); odps.setDefaultProject("my_project"); odps.setEndpoint("http://service.odps.aliyun.com/api"); // 获取 SecurityManager SecurityManager sm = odps.projects().get().getSecurityManager(); // 1. 检查对表的 Select 权限 PermissionDesc desc = new PermissionDesc( "my_project", ObjectType.Table, "sales_data", ActionType.Select ); CheckPermissionResultInfo resultInfo = sm.checkPermission(desc); CheckPermissionResult result = resultInfo.getResult(); if (result == CheckPermissionResult.Allow) { System.out.println("允许访问: 用户对 sales_data 表拥有 Select 权限"); } else { System.out.println("拒绝访问: " + resultInfo.getMessage()); } // 2. 检查对函数的 Execute 权限 PermissionDesc funcDesc = new PermissionDesc( "my_project", ObjectType.Function, "my_udf", ActionType.Execute ); CheckPermissionResultInfo funcResult = sm.checkPermission(funcDesc); System.out.println("函数执行权限: " + funcResult.getResult().name()); // 3. 检查指定 schema 下对象的权限 PermissionDesc schemaDesc = new PermissionDesc( "my_project", "my_schema", ObjectType.Table, "user_profile", ActionType.Describe ); CheckPermissionResultInfo schemaResult = sm.checkPermission(schemaDesc); System.out.println("Schema 下表的 Describe 权限: " + schemaResult.getResult().name()); } } ``` ```python from odps import ODPS # 初始化 ODPS 客户端 odps = ODPS('access_id', 'access_key', 'my_project', endpoint='http://service.odps.aliyun.com/api') # 1. 检查对表的 Select 权限(通过执行权限查询命令) result = odps.run_security_query( "SHOW GRANTS FOR USER ALIYUN$current_user ON TABLE sales_data;" ) print(result) # 2. 检查对函数的权限 result = odps.run_security_query( "SHOW GRANTS FOR USER ALIYUN$current_user ON FUNCTION my_udf;" ) print("函数执行权限:", result) # 3. 查看当前用户的全部权限 result = odps.run_security_query("SHOW GRANTS;") print(result) ``` :::note Python SDK 没有直接对应的 `checkPermission` 方法,可通过 `run_security_query` 执行 `SHOW GRANTS` 命令来查询权限。 ::: ```go package main import ( "fmt" "log" "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" "github.com/aliyun/aliyun-odps-go-sdk/odps/security" ) func main() { acc := account.NewAliyunAccount("accessId", "accessKey") odpsIns := odps.NewOdps(acc, "http://service.odps.aliyun.com/api") odpsIns.SetDefaultProjectName("my_project") // 获取 SecurityManager sm := odpsIns.Projects().GetDefaultProject().SecurityManager() // 1. 检查对表的 Select 权限 perm := security.Permission{ ProjectName: "my_project", ObjectType: security.ObjectTypeTable, ObjectName: "sales_data", ActionType: security.ActionTypeSelect, } result, err := sm.CheckPermissionV1(perm) if err != nil { log.Fatalf("权限检查失败: %+v", err) } fmt.Printf("权限检查结果: %s, 信息: %s\n", result.Result, result.Message) } ``` ## 代码说明 ### 获取 SecurityManager `SecurityManager` 通过项目对象获取: ```java SecurityManager sm = odps.projects().get().getSecurityManager(); ``` ```python # Python SDK 中无需显式获取 SecurityManager,直接通过 odps 实例调用 odps.run_security_query("SHOW GRANTS;") ``` ```go sm := odpsIns.Projects().GetDefaultProject().SecurityManager() ``` ### 构建权限描述 `PermissionDesc` 封装了权限检查所需的全部参数: ```java // 基本构造方法 PermissionDesc desc = new PermissionDesc(projectName, objectType, objectName, actionType); // 指定 schema 的构造方法 PermissionDesc desc = new PermissionDesc(projectName, schemaName, objectType, objectName, actionType); ``` ```python # Python SDK 通过 SHOW GRANTS 命令查询权限 result = odps.run_security_query("SHOW GRANTS FOR USER user_name ON TABLE table_name;") ``` ```go // 构建权限描述 perm := security.Permission{ ProjectName: projectName, ObjectType: security.ObjectTypeTable, ObjectName: objectName, ActionType: security.ActionTypeSelect, } ``` ### 执行权限检查 调用 `checkPermission` 返回 `CheckPermissionResultInfo`,包含检查结果和附加信息: ```java CheckPermissionResultInfo resultInfo = sm.checkPermission(desc); CheckPermissionResult result = resultInfo.getResult(); // Allow 或 Deny String message = resultInfo.getMessage(); // 附加说明信息 ``` ```python # 查询结果直接返回为字符串 result = odps.run_security_query("SHOW GRANTS FOR USER user_name;") print(result) ``` ```go result, err := sm.CheckPermissionV1(perm) if err != nil { log.Fatalf("权限检查失败: %+v", err) } fmt.Printf("结果: %s, 信息: %s\n", result.Result, result.Message) ``` ## 对象类型与操作类型 ### ObjectType(对象类型) | 枚举值 | 说明 | |--------|------| | `Project` | 项目 | | `Table` | 表 | | `Function` | 函数 | | `Resource` | 资源 | | `Instance` | 实例 | ### ActionType(操作类型) | 枚举值 | 适用对象 | 说明 | |--------|----------|------| | `Read` | Project | 读取项目信息 | | `Write` | Project | 写入项目信息 | | `List` | Project | 列出项目中的对象 | | `CreateTable` | Project | 在项目中创建表 | | `CreateInstance` | Project | 在项目中创建实例 | | `CreateFunction` | Project | 在项目中创建函数 | | `CreateResource` | Project | 在项目中创建资源 | | `Select` | Table | 查询表数据 | | `Alter` | Table | 修改表结构 | | `Update` | Table | 更新表数据 | | `Drop` | Table | 删除表 | | `Describe` | Table/Function/Resource | 查看对象元信息 | | `Execute` | Function/Instance | 执行函数或实例 | | `Delete` | Resource | 删除资源 | | `Download` | Table | 下载表数据 | | `All` | 所有 | 全部权限 | :::info ActionType 和 ObjectType 存在对应关系,例如不存在 "Read Table" 权限或 "Create Project" 权限。具体的权限列表请参考 [MaxCompute 权限文档](https://help.aliyun.com/zh/maxcompute/user-guide/maxcompute-permissions)。 ::: ## 注意事项 - `checkPermission` 检查的是当前认证账号的权限 - 传入的参数(项目名、对象类型、对象名、操作类型)不能为 null,否则会抛出异常 - 权限检查结果可能受项目安全策略、角色绑定、Package 授权等多种因素影响 - 此方法仅做权限预检,不会实际执行操作 - 如果目标对象不存在,检查仍可能返回 Allow(权限模型不依赖对象是否存在) ## 相关文档 - [ACL 查询](./acl-query.md) - 执行 ACL 权限命令管理访问控制 - [权限检查参考](./check-permission.md) - checkPermission 详细类说明 --- # guides/write-data/index.md # 写入数据 MaxCompute Java SDK 提供了多种数据写入方式,适用于不同的业务场景。本文帮助您快速了解各方式的特点并选择最合适的方案。 ## 写入方式对比 | 方式 | 适用场景 | 数据可见性 | 事务性 | 性能 | |------|---------|-----------|--------|------| | [Tunnel Upload](./tunnel-upload.md) | 批量导入 | 提交后可见 | 原子提交 | 中 | | [Tunnel Stream](./tunnel-stream.md) | 实时写入 | flush 后可见 | 非事务 | 高 | | [Tunnel Upsert](./tunnel-upsert.md) | 更新插入(Delta Table) | flush 后可见 | 行级 | 高 | | [Storage API](./storage-api-write.md) | 高性能写入 | 配置决定 | 可选 | 最高 | ## 选型指南 ### 按场景选择 - **离线批量导入**:选择 [Tunnel Upload](./tunnel-upload.md)。支持原子提交,可确保数据要么全部可见,要么全部不可见,适合 ETL 作业和定期数据同步。 - **实时/准实时写入**:选择 [Tunnel Stream](./tunnel-stream.md)。数据写入后 flush 即可见,无需等待 commit,适合日志采集、实时监控等场景。 - **数据更新与删除**:选择 [Tunnel Upsert](./tunnel-upsert.md)。针对 Delta Table(主键表)设计,支持按主键进行 insert/update/delete 操作,适合数据库变更同步(CDC)场景。 - **超高吞吐写入**:选择 [Storage API](./storage-api-write.md)。基于 Arrow 列式格式,支持批量和流式两种模式,提供最高的写入性能,适合大规模数据导入和高频写入场景。 ### 按特性选择 | 需求 | 推荐方式 | |------|---------| | 需要原子性保证(全部成功或全部失败) | Tunnel Upload / Storage API (Batch) | | 写入后立即可查 | Tunnel Stream / Tunnel Upsert / Storage API (Streaming) | | 需要更新或删除已有数据 | Tunnel Upsert | | 追求最大写入吞吐 | Storage API | | 多线程并行写入 | 以上均支持 | | 分区表动态分区写入 | Tunnel Stream(0.55.0+) | ## 相关文档 - [Tunnel Upload 批量写入](./tunnel-upload.md) - [Tunnel Stream 流式写入](./tunnel-stream.md) - [Tunnel Upsert 更新插入](./tunnel-upsert.md) - [Storage API 高性能写入](./storage-api-write.md) --- # guides/write-data/storage-api-write.md # Storage API 高性能写入 Storage API 基于 Apache Arrow 列式格式提供最高性能的数据写入能力,支持 Batch(批量)和 Streaming(流式)两种模式,适合大规模数据导入和高频实时写入场景。 :::note Go SDK 暂不支持 Storage API。 ::: ## 前置条件 - 已添加 `odps-sdk-storage-api` 依赖 - 已初始化 `MaxStorageClient` 实例 - 目标表已存在 - 项目中已引入 Apache Arrow 相关依赖 ```xml com.aliyun.odps odps-sdk-storage-api 0.52.0 ``` ## 完整示例 ### Batch 模式(批量写入) Batch 模式需要显式调用 `commit()` 后数据才对外可见,支持事务回滚,适合需要原子性保证的场景。 ```java import com.aliyun.odps.storage.api.MaxStorageClient; import com.aliyun.odps.storage.api.TableIdentifier; import com.aliyun.odps.storage.api.write.ArrowWriter; import com.aliyun.odps.storage.api.write.TableArrowWriter; import com.aliyun.odps.storage.api.write.TableWriteSession; import org.apache.arrow.vector.BigIntVector; import org.apache.arrow.vector.VarCharVector; import org.apache.arrow.vector.VectorSchemaRoot; public class StorageApiBatchWriteExample { public static void main(String[] args) throws Exception { // 1. 创建 MaxStorageClient MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); // 2. 指定目标表 TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); // 3. 创建 TableWriteSession(Batch 模式,默认) try (TableWriteSession session = client.createTableWriteSessionBuilder(tableId) .build()) { // 4. 创建 ArrowWriter try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { TableArrowWriter arrowWriter = (TableArrowWriter) writer; // 5. 创建 VectorSchemaRoot 并填充数据 try (VectorSchemaRoot root = arrowWriter.createVectorSchemaRoot()) { root.allocateNew(); VarCharVector nameVector = (VarCharVector) root.getVector("name"); BigIntVector ageVector = (BigIntVector) root.getVector("age"); for (int i = 0; i < 10000; i++) { nameVector.setSafe(i, ("user_" + i).getBytes()); ageVector.setSafe(i, 20 + i % 50); } root.setRowCount(10000); // 6. 写入数据批次 writer.writeBatch(root); } // 7. flush 数据到服务端 writer.flush(); } // 8. 提交事务,数据变为可见 session.commit(); System.out.println("Batch write committed successfully."); } } } ``` ```python import pyarrow as pa from odps.apis.storage_api_v2 import StorageApiArrowClient table = o.get_table("my_table") client = StorageApiArrowClient(o, table) # 创建写入会话 resp = client.create_write_session() session_id = resp.session_id # 写入数据 batch = pa.record_batch( [pa.array([1, 2, 3]), pa.array(["a", "b", "c"])], schema=pa.schema([pa.field("id", pa.int64()), pa.field("name", pa.string())]) ) writer = client.write_rows_arrow(session_id, stream_id=0, record_count=3) writer.write(batch) commit_msg, success = writer.finish() # 提交 client.commit_write_session(session_id) ``` ### Streaming 模式(流式写入) Streaming 模式下数据 flush 后立即可见,无需调用 `commit()`,适合实时写入场景。 ```java import com.aliyun.odps.storage.api.MaxStorageClient; import com.aliyun.odps.storage.api.TableIdentifier; import com.aliyun.odps.storage.api.write.ArrowWriter; import com.aliyun.odps.storage.api.write.TableArrowWriter; import com.aliyun.odps.storage.api.write.TableWriteSession; import com.aliyun.odps.storage.api.write.WriteMode; import org.apache.arrow.vector.BigIntVector; import org.apache.arrow.vector.VarCharVector; import org.apache.arrow.vector.VectorSchemaRoot; public class StorageApiStreamingWriteExample { public static void main(String[] args) throws Exception { // 1. 创建 MaxStorageClient MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); // 2. 指定目标表 TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); // 3. 创建 TableWriteSession(Streaming 模式) try (TableWriteSession session = client.createTableWriteSessionBuilder(tableId) .withWriteMode(WriteMode.STREAMING) .build()) { // 4. 创建 ArrowWriter try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { TableArrowWriter arrowWriter = (TableArrowWriter) writer; // 5. 循环写入,每批 flush 后立即可见 for (int batch = 0; batch < 10; batch++) { try (VectorSchemaRoot root = arrowWriter.createVectorSchemaRoot()) { root.allocateNew(); VarCharVector nameVector = (VarCharVector) root.getVector("name"); BigIntVector ageVector = (BigIntVector) root.getVector("age"); for (int i = 0; i < 1000; i++) { nameVector.setSafe(i, ("batch_" + batch + "_user_" + i).getBytes()); ageVector.setSafe(i, 20 + i % 50); } root.setRowCount(1000); writer.writeBatch(root); } // flush 后数据立即可见 writer.flush(); System.out.println("Batch " + batch + " flushed and visible."); } } // Streaming 模式无需 commit } } } ``` ```python import pyarrow as pa from odps.apis.storage_api_v2 import StorageApiArrowClient table = o.get_table("my_table") client = StorageApiArrowClient(o, table) # 创建写入会话 resp = client.create_write_session() session_id = resp.session_id # 写入数据 batch = pa.record_batch( [pa.array([1, 2, 3]), pa.array(["a", "b", "c"])], schema=pa.schema([pa.field("id", pa.int64()), pa.field("name", pa.string())]) ) writer = client.write_rows_arrow(session_id, stream_id=0, record_count=3) writer.write(batch) commit_msg, success = writer.finish() # 提交 client.commit_write_session(session_id) ``` ## 代码说明 ### 两种写入模式 | 特性 | Batch 模式 | Streaming 模式 | |------|-----------|---------------| | 数据可见时机 | `commit()` 后 | `flush()` 后 | | 事务支持 | 支持(可 abort 回滚) | 不支持(flush 后不可回滚) | | 是否需要 commit | 必须 | 无需(空操作) | | 适用场景 | ETL、批量导入 | 实时写入、流处理 | ### Arrow VectorSchemaRoot 的创建和填充 `VectorSchemaRoot` 是 Arrow 格式的核心数据容器,代表一批列式数据: ```java TableArrowWriter arrowWriter = (TableArrowWriter) writer; // 创建与表 Schema 匹配的 VectorSchemaRoot try (VectorSchemaRoot root = arrowWriter.createVectorSchemaRoot()) { // 分配内存 root.allocateNew(); // 获取列向量(按列名) VarCharVector nameCol = (VarCharVector) root.getVector("name"); BigIntVector ageCol = (BigIntVector) root.getVector("age"); // 逐行填充数据 int rowCount = 5000; for (int i = 0; i < rowCount; i++) { nameCol.setSafe(i, ("value_" + i).getBytes()); ageCol.setSafe(i, (long) i); } // 设置实际行数 root.setRowCount(rowCount); // 写入批次 writer.writeBatch(root); } ``` ```python import pyarrow as pa # 使用 PyArrow 构建 RecordBatch batch = pa.record_batch( [ pa.array([f"value_{i}" for i in range(5000)]), pa.array(list(range(5000)), type=pa.int64()), ], schema=pa.schema([ pa.field("name", pa.string()), pa.field("age", pa.int64()), ]) ) # 写入批次 writer = client.write_rows_arrow(session_id, stream_id=0, record_count=5000) writer.write(batch) commit_msg, success = writer.finish() ``` 常用向量类型对应关系: | MaxCompute 类型 | Arrow Vector 类型 | |----------------|-------------------| | STRING/VARCHAR | `VarCharVector` | | BIGINT | `BigIntVector` | | INT | `IntVector` | | DOUBLE | `Float8Vector` | | FLOAT | `Float4Vector` | | BOOLEAN | `BitVector` | | DATETIME | `TimeStampMilliVector` | | DECIMAL | `DecimalVector` | ### 多 Writer 并行写入 同一 Session 下可创建多个独立的 Writer,通过不同的 `streamId` 标识: ```java try (TableWriteSession session = client.createTableWriteSessionBuilder(tableId).build()) { int numWriters = 4; ExecutorService executor = Executors.newFixedThreadPool(numWriters); List> futures = new ArrayList<>(); for (int i = 0; i < numWriters; i++) { final int writerIndex = i; futures.add(executor.submit(() -> { try (ArrowWriter writer = session.createWriterBuilder( "stream-" + writerIndex, 1).build()) { TableArrowWriter arrowWriter = (TableArrowWriter) writer; try (VectorSchemaRoot root = arrowWriter.createVectorSchemaRoot()) { root.allocateNew(); // 填充该 Writer 负责的数据分片... root.setRowCount(batchSize); writer.writeBatch(root); } writer.flush(); } })); } for (Future f : futures) { f.get(); } executor.shutdown(); // 所有 Writer 完成后统一提交 session.commit(); } ``` ```python import concurrent.futures import pyarrow as pa table = o.get_table("my_table") client = StorageApiArrowClient(o, table) resp = client.create_write_session() session_id = resp.session_id def write_stream(stream_id, data_chunk): batch = pa.record_batch( data_chunk, schema=pa.schema([pa.field("name", pa.string()), pa.field("age", pa.int64())]) ) writer = client.write_rows_arrow(session_id, stream_id=stream_id, record_count=len(data_chunk[0])) writer.write(batch) writer.finish() # 多线程并行写入 num_writers = 4 with concurrent.futures.ThreadPoolExecutor(max_workers=num_writers) as pool: for i in range(num_writers): chunk = [pa.array([f"stream_{i}_row_{j}" for j in range(1000)]), pa.array(list(range(1000)), type=pa.int64())] pool.submit(write_stream, i, chunk) # 所有 Writer 完成后统一提交 client.commit_write_session(session_id) ``` ### 幂等重试 `streamId` + `streamVersion` 的组合用于标识一次写入操作的唯一性。对同一批数据使用相同的 `streamId` 和 `streamVersion` 重新写入,服务端会保证只保留一份结果: ```java // 初次写入 try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { writer.writeBatch(root); writer.flush(); } // 如果写入失败,使用相同的 streamId 和 streamVersion 重试 try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { writer.writeBatch(root); // 服务端保证幂等 writer.flush(); } ``` ```python # 初次写入 writer = client.write_rows_arrow(session_id, stream_id=0, record_count=n) writer.write(batch) commit_msg, success = writer.finish() # 如果写入失败,使用相同的 stream_id 重试 if not success: writer = client.write_rows_arrow(session_id, stream_id=0, record_count=n) writer.write(batch) # 服务端保证幂等 writer.finish() ``` ## 配置选项 ### Session 构建参数 | 方法 | 说明 | |------|------| | `withPartition(PartitionSpec)` | 写入指定分区 | | `withOverwrite(boolean)` | 是否覆盖已有数据(默认 false) | | `withWriteMode(WriteMode)` | 写入模式:BATCH(默认)/ STREAMING | | `withSessionId(String)` | 复用已有 Session | ### Writer 构建参数 | 方法 | 默认值 | 说明 | |------|--------|------| | `withBufferSize(long)` | 64MB | 写入缓冲区大小(字节) | | `withAutoFlushEnabled(boolean)` | true | 缓冲区满时是否自动 flush | | `withExecutorService(ExecutorService)` | - | 异步 flush 线程池 | ### 异步 Flush 通过设置线程池,可以实现写入和网络发送的并行化,提升吞吐: ```java ExecutorService flushExecutor = Executors.newSingleThreadExecutor(); ArrowWriter writer = session.createWriterBuilder("stream-1", 1) .withExecutorService(flushExecutor) .build(); ``` ```python # PyODPS Storage API 内部自动处理异步 flush # 写入大量数据时自动优化网络发送 writer = client.write_rows_arrow(session_id, stream_id=0, record_count=n) writer.write(batch) writer.finish() ``` ## 注意事项 1. **VectorSchemaRoot 生命周期**:调用方负责管理 `VectorSchemaRoot` 的关闭,必须使用 try-with-resources 或手动 `close()` 防止内存泄漏。 2. **writeBatch 后可复用**:`writeBatch()` 方法内部会立即序列化数据,调用返回后 `VectorSchemaRoot` 可安全修改或重用。 3. **Batch 模式必须 commit**:Batch 模式下如果 Session 关闭前未调用 `commit()`,会自动执行 `abort()` 丢弃所有数据。 4. **Streaming 模式不可回滚**:Streaming 模式下 flush 后的数据无法撤回。 5. **streamId 唯一性**:同一 Session 下不同 Writer 应使用不同的 `streamId`。 6. **分区列不需写入**:写入分区表时,分区列不包含在写入数据中,通过 `withPartition()` 指定。 ## 相关文档 - [Storage API 写入 API 参考](../../reference/TableWriteSession) - [Storage API Client 参考](../../reference/MaxStorageClient) - [Tunnel Upload 批量写入](./tunnel-upload.md) - [Tunnel Stream 流式写入](./tunnel-stream.md) - [Tunnel Upsert 更新插入](./tunnel-upsert.md) --- # guides/write-data/tunnel-stream.md # Tunnel Stream 流式写入 Tunnel Stream 是面向实时写入场景的数据通道,数据 flush 后立即可见,无需像 Tunnel Upload 那样手动提交 Session,适合日志采集、实时监控等需要低延迟可见的场景。 ## 前置条件 - 已初始化 `Odps` 客户端实例 - 已创建 `TableTunnel` 实例(参考 [TableTunnel 文档](../../reference/TableTunnel)) - 目标表已存在 ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.tunnel.TableTunnel; import com.aliyun.odps.tunnel.io.CompressOption; import com.aliyun.odps.tunnel.streams.StreamRecordPack; public class TunnelStreamExample { public static void main(String[] args) throws Exception { // 1. 初始化 ODPS 客户端 AliyunAccount account = new AliyunAccount("", ""); Odps odps = new Odps(account); odps.setDefaultProject(""); odps.setEndpoint(""); // 2. 获取 TableTunnel 实例 TableTunnel tunnel = odps.tableTunnel(); // 3. 创建 StreamUploadSession String projectName = ""; String tableName = ""; TableTunnel.StreamUploadSession session = tunnel .buildStreamUploadSession(projectName, tableName) .build(); // 4. 创建 StreamRecordPack StreamRecordPack pack = session.newRecordPack(); // 5. 写入数据 for (int i = 0; i < 1000; i++) { Record record = session.newRecord(); record.setString("name", "user_" + i); record.setBigint("age", (long) (20 + i % 50)); pack.append(record); } // 6. flush 数据(flush 后数据立即可见) String traceId = pack.flush(); System.out.println("Flush completed, traceId: " + traceId); } } ``` ```python # 流式写入 - flush 后数据即可见 with table.open_writer(partition='dt=20231001', create_partition=True) as writer: for batch in data_source: writer.write(batch) # close 时自动 flush ``` ```go session, _ := tunnelIns.CreateStreamUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithPartitionKey("dt=20231001"), tunnel.SessionCfg.WithCreatePartition(), ) pack := session.OpenRecordPackWriter() record := []data.Data{data.BigInt(1), data.String("hello")} pack.Append(record) // flush 后数据即可见 traceId, recordCount, bytesSend, err := pack.Flush() fmt.Printf("traceId=%s, records=%d, bytes=%d\n", traceId, recordCount, bytesSend) ``` ## 代码说明 ### 与批量写入的关键区别 | 特性 | Tunnel Upload | Tunnel Stream | |------|--------------|---------------| | 数据可见时机 | commit 后 | flush 后 | | 是否需要手动 commit | 是 | 否 | | Block 管理 | 需手动指定 blockId | 无需关注 | | 事务性 | 原子提交 | 非事务(逐批可见) | | Session 有效期 | 24 小时 | 长期有效 | ### 写入流程 1. **创建 Session**:通过 `buildStreamUploadSession().build()` 创建流式上传会话 2. **创建 RecordPack**:通过 `session.newRecordPack()` 创建数据包 3. **追加记录**:向 RecordPack 中逐条追加数据 4. **flush 提交**:调用 `pack.flush()` 将数据发送到服务端并立即可见 5. **重复使用**:同一个 RecordPack 可以反复 append 和 flush ### Schema 版本追踪(0.50.0+) 从 SDK 0.50.0 起,当表结构发生变更时,如果 Session 中的 Schema 版本与服务端不一致,flush 操作会抛出 `SchemaMismatchException`。此时需要重新创建 Session 以获取最新的 Schema。 ```java try { pack.flush(); } catch (SchemaMismatchException e) { // 表结构已变更,需要重建 Session session = tunnel.buildStreamUploadSession(projectName, tableName).build(); pack = session.newRecordPack(); // 重新写入数据... } ``` ```python # PyODPS 在写入时自动检测 Schema 变更 # 如需重新获取最新 Schema,重新打开 writer 即可 try: with table.open_writer() as writer: writer.write(records) except Exception as e: # 表结构已变更,重新获取表对象 table.reload() with table.open_writer() as writer: writer.write(records) ``` ```go traceId, _, _, err := pack.Flush() if err != nil { // 表结构已变更,需要重建 Session session, _ = tunnelIns.CreateStreamUploadSession( project.Name(), "my_table", ) pack = session.OpenRecordPackWriter() // 重新写入数据... } ``` 可通过 `allowSchemaMismatch(boolean)` 配置是否允许字段类型不匹配: ```java TableTunnel.StreamUploadSession session = tunnel .buildStreamUploadSession(projectName, tableName) .allowSchemaMismatch(false) // 严格模式:Schema 不匹配时报错 .build(); ``` ```python # PyODPS 默认严格校验 Schema,写入时字段类型不匹配会抛出异常 with table.open_writer() as writer: writer.write(records) # 字段类型需与表 Schema 一致 ``` ```go // Go SDK 默认严格校验 Schema session, _ := tunnelIns.CreateStreamUploadSession( project.Name(), "my_table", ) ``` ### 动态分区写入(0.55.0+) 从 SDK 0.55.0 起,支持通过 `DynamicPartitionRecordPack` 在一个 Session 中向多个分区写入数据,无需为每个分区单独创建 Session: ```java // 创建 StreamUploadSession(不指定分区) TableTunnel.StreamUploadSession session = tunnel .buildStreamUploadSession(projectName, tableName) .setCreatePartition(true) // 自动创建不存在的分区 .build(); // 使用动态分区 RecordPack StreamRecordPack pack = session.newRecordPack(); // 写入不同分区的数据 Record record1 = session.newRecord(); record1.setString("name", "alice"); record1.setBigint("age", 25L); record1.setString("dt", "20250101"); // 分区列作为普通列写入 pack.append(record1); Record record2 = session.newRecord(); record2.setString("name", "bob"); record2.setBigint("age", 30L); record2.setString("dt", "20250102"); // 另一个分区 pack.append(record2); pack.flush(); // 数据自动路由到对应分区 ``` ```python # PyODPS 支持动态分区写入,分区列作为普通列写入 with table.open_writer(create_partition=True) as writer: writer.write([ ["alice", 25, "20250101"], # 分区列 dt 作为最后一列 ["bob", 30, "20250102"], # 自动路由到对应分区 ]) ``` ```go // 创建 StreamUploadSession(不指定分区,启用自动创建) session, _ := tunnelIns.CreateStreamUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithCreatePartition(), ) pack := session.OpenRecordPackWriter() // 写入不同分区的数据(分区列作为普通列写入) pack.Append([]data.Data{data.String("alice"), data.BigInt(25), data.String("20250101")}) pack.Append([]data.Data{data.String("bob"), data.BigInt(30), data.String("20250102")}) // flush 后数据自动路由到对应分区 traceId, _, _, _ := pack.Flush() ``` ## 配置选项 ### Session 构建参数 | 方法 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `setSlotNum(long)` | long | 0 | 并行槽位数量(0 表示自动分配) | | `setCreatePartition(boolean)` | boolean | false | 自动创建不存在的分区 | | `setSchemaVersion(String)` | String | 最新版本 | 指定 Schema 版本号 | | `allowSchemaMismatch(boolean)` | boolean | true | 是否允许字段类型不匹配 | | `setPartitionSpec(String)` | String | - | 指定目标分区 | | `setSchemaName(String)` | String | - | 三层模型中的 Schema 名称 | ### 压缩选项 ```java // 使用 Snappy 压缩 StreamRecordPack pack = session.newRecordPack( new CompressOption(CompressOption.CompressAlgorithm.ODPS_SNAPPY, 0, 0)); ``` ```python # PyODPS 默认启用压缩,也可指定压缩算法 with table.open_writer(compress_algo='zlib') as writer: writer.write(records) ``` ```go // Go SDK 在创建 Session 时配置压缩 session, _ := tunnelIns.CreateStreamUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithSnappyFramedCompressor(), ) ``` | 算法 | 说明 | |------|------| | `ODPS_RAW` | 不压缩 | | `ODPS_ZLIB` | ZLIB 压缩(0.51.2 起为默认值) | | `ODPS_SNAPPY` | Snappy 压缩 | | `ODPS_LZ4_FRAME` | LZ4 Frame 压缩 | :::note 在 0.51.2 版本之前,默认压缩算法为 `ODPS_RAW`(不压缩)。 ::: ### Flush 控制 ```java // 带超时控制的 flush FlushOption option = new FlushOption().timeout(3000); // 3 秒超时 FlushResult result = pack.flush(option); System.out.println("recordCount: " + result.getRecordCount()); System.out.println("flushSize: " + result.getFlushSize()); ``` ```python # PyODPS 在 writer.close() 时自动 flush # 超时由底层连接配置控制 with table.open_writer() as writer: writer.write(records) # close 时自动 flush,返回写入结果 ``` ```go // flush 返回 traceId、记录数和字节数 traceId, recordCount, bytesSend, err := pack.Flush() fmt.Printf("recordCount: %d, flushSize: %d\n", recordCount, bytesSend) ``` ## 注意事项 1. **非事务性**:Tunnel Stream 不提供事务保证,每次 flush 的数据独立可见。如果写入过程中发生失败,已 flush 的数据不会回滚。 2. **数据量控制**:建议当 RecordPack 中数据量达到 64MB 时触发 flush,避免单次 flush 数据量过小导致文件碎片。 3. **内存管理**:长时间未 flush 的 RecordPack 会持续占用内存,需注意内存使用情况。 4. **避免跨线程操作**:单个 StreamUploadSession 支持创建多个 RecordPack,但需避免跨线程并发操作同一个 RecordPack。 5. **Schema 变更**:表结构变更后需重新创建 Session 获取最新 Schema。 6. **flush 频率**:建议合理控制 flush 频率,过于频繁的 flush 可能影响服务端性能。 ## 相关文档 - [StreamUploadSession API 参考](../../reference/StreamUploadSession) - [TableTunnel API 参考](../../reference/TableTunnel) - [Tunnel Upload 批量写入](./tunnel-upload.md) - [Tunnel Upsert 更新插入](./tunnel-upsert.md) --- # guides/write-data/tunnel-upload.md # Tunnel Upload 批量写入 Tunnel Upload 是 MaxCompute 最经典的批量数据写入方式,通过 UploadSession 管理写入生命周期,支持原子提交确保数据一致性。 ## 前置条件 - 已初始化 `Odps` 客户端实例 - 已创建 `TableTunnel` 实例(参考 [TableTunnel 文档](../../reference/TableTunnel)) - 目标表已存在;若为分区表,分区需已创建或指定已有分区 ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.PartitionSpec; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.data.RecordWriter; import com.aliyun.odps.tunnel.TableTunnel; public class TunnelUploadExample { public static void main(String[] args) throws Exception { // 1. 初始化 ODPS 客户端 AliyunAccount account = new AliyunAccount("", ""); Odps odps = new Odps(account); odps.setDefaultProject(""); odps.setEndpoint(""); // 2. 获取 TableTunnel 实例 TableTunnel tunnel = odps.tableTunnel(); // 3. 创建 UploadSession String projectName = ""; String tableName = ""; TableTunnel.UploadSession session = tunnel.createUploadSession(projectName, tableName); System.out.println("Session ID: " + session.getId()); // 4. 打开 RecordWriter(指定 blockId) long blockId = 0; RecordWriter writer = session.openRecordWriter(blockId, true); // 启用压缩 // 5. 写入数据 for (int i = 0; i < 1000; i++) { Record record = session.newRecord(); record.setString("name", "user_" + i); record.setBigint("age", (long) (20 + i % 50)); writer.write(record); } // 6. 关闭 Writer writer.close(); // 7. 提交(带 Block 校验) session.commit(new Long[]{blockId}); System.out.println("Upload committed successfully."); } } ``` ```python # 记录写入 records = [[1, 'Alice'], [2, 'Bob']] with table.open_writer(partition='dt=20231001', create_partition=True) as writer: writer.write(records) # 使用 write_table(支持 DataFrame) import pandas as pd df = pd.DataFrame({'id': [1, 2], 'name': ['Alice', 'Bob']}) o.write_table('my_table', df, partition='dt=20231001', create_partition=True) ``` ```go session, _ := tunnelIns.CreateUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithPartitionKey("dt=20231001"), tunnel.SessionCfg.WithDefaultDeflateCompressor(), ) // 打开 writer writer, _ := session.OpenRecordWriter(0) // 写入数据 record := []data.Data{data.BigInt(1), data.String("Alice")} writer.Write(record) record2 := []data.Data{data.BigInt(2), data.String("Bob")} writer.Write(record2) // 关闭并提交 writer.Close() session.Commit([]int{0}) ``` ## 代码说明 ### 写入流程 整个 Tunnel Upload 写入流程分为以下步骤: 1. **创建 Session**:通过 `TableTunnel.createUploadSession()` 创建上传会话,获取会话 ID 2. **打开 Writer**:通过 `session.openRecordWriter(blockId)` 获取写入器,需指定 Block ID 3. **写入记录**:构建 Record 对象并逐条写入 4. **关闭 Writer**:调用 `writer.close()` 完成当前 Block 的数据传输 5. **提交 Session**:调用 `session.commit()` 使数据对外可见 ### Block 管理 Block 是 Tunnel Upload 的核心概念: - **Block ID 范围**:0 ~ 19999,同一 Session 内唯一标识一个数据块 - **唯一写入者**:同一时刻每个 Block ID 只能有一个活跃的 Writer - **覆盖语义**:对同一个 Block ID 重新写入会覆盖之前的数据(未 commit 前) - **大小建议**:每个 Block 建议不小于 64MB,上限为 100GB ### 多 Block 并行上传 利用不同的 Block ID,可以实现多线程并行写入: ```java TableTunnel.UploadSession session = tunnel.createUploadSession(projectName, tableName); int threadCount = 4; ExecutorService executor = Executors.newFixedThreadPool(threadCount); List> futures = new ArrayList<>(); for (int t = 0; t < threadCount; t++) { final long blockId = t; futures.add(executor.submit(() -> { try { RecordWriter writer = session.openRecordWriter(blockId, true); for (int i = 0; i < 10000; i++) { Record record = session.newRecord(); record.setString("name", "thread_" + blockId + "_row_" + i); record.setBigint("value", (long) i); writer.write(record); } writer.close(); } catch (Exception e) { throw new RuntimeException(e); } })); } // 等待所有线程完成 for (Future f : futures) { f.get(); } executor.shutdown(); // 提交所有 Block Long[] blocks = new Long[]{0L, 1L, 2L, 3L}; session.commit(blocks); ``` ```python import concurrent.futures # PyODPS 自动管理 Block,支持多线程写入 def write_chunk(thread_id, records): with table.open_writer(partition='dt=20250101', create_partition=True) as writer: writer.write(records) # 准备分片数据 thread_count = 4 with concurrent.futures.ThreadPoolExecutor(max_workers=thread_count) as pool: for t in range(thread_count): records = [ [f"thread_{t}_row_{i}", i] for i in range(10000) ] pool.submit(write_chunk, t, records) ``` ```go session, _ := tunnelIns.CreateUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithDefaultDeflateCompressor(), ) threadCount := 4 var wg sync.WaitGroup for t := 0; t < threadCount; t++ { wg.Add(1) go func(blockId int) { defer wg.Done() writer, _ := session.OpenRecordWriter(blockId) for i := 0; i < 10000; i++ { record := []data.Data{ data.String(fmt.Sprintf("thread_%d_row_%d", blockId, i)), data.BigInt(int64(i)), } writer.Write(record) } writer.Close() }(t) } wg.Wait() // 提交所有 Block session.Commit([]int{0, 1, 2, 3}) ``` ### 使用 TunnelBufferedWriter 如果不想手动管理 Block ID,可以使用 `TunnelBufferedWriter`,它内部自动管理缓冲区和 Block 分配: ```java TableTunnel.UploadSession session = tunnel.createUploadSession(projectName, tableName); RecordWriter writer = session.openBufferedWriter(true); // 启用压缩 for (int i = 0; i < 100000; i++) { Record record = session.newRecord(); record.setString("name", "user_" + i); record.setBigint("age", (long) (20 + i % 50)); writer.write(record); } writer.close(); session.commit(); ``` ```python # PyODPS 默认自动管理缓冲区和 Block 分配 records = [[f"user_{i}", 20 + i % 50] for i in range(100000)] with table.open_writer() as writer: writer.write(records) ``` ```go session, _ := tunnelIns.CreateUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithDefaultDeflateCompressor(), ) writer, _ := session.OpenRecordWriter(0) for i := 0; i < 100000; i++ { record := []data.Data{ data.String(fmt.Sprintf("user_%d", i)), data.BigInt(int64(20 + i%50)), } writer.Write(record) } writer.Close() session.Commit([]int{0}) ``` ## 配置选项 ### 压缩算法 | 算法 | 说明 | |------|------| | `CompressOption.CompressAlgorithm.ODPS_RAW` | 不压缩 | | `CompressOption.CompressAlgorithm.ODPS_ZLIB` | ZLIB 压缩(支持 level 和 strategy) | | `CompressOption.CompressAlgorithm.ODPS_SNAPPY` | Snappy 压缩 | | `CompressOption.CompressAlgorithm.ODPS_LZ4_FRAME` | LZ4 Frame 压缩 | ```java CompressOption option = new CompressOption( CompressOption.CompressAlgorithm.ODPS_LZ4_FRAME, 0, 0); RecordWriter writer = session.openRecordWriter(blockId, option); ``` ```python # PyODPS 默认启用压缩,也可指定压缩算法 with table.open_writer(compress_algo='zlib') as writer: writer.write(records) ``` ```go session, _ := tunnelIns.CreateUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithDefaultDeflateCompressor(), ) ``` ### 分区表写入 ```java // 写入指定分区 PartitionSpec partitionSpec = new PartitionSpec("dt='20250101'"); TableTunnel.UploadSession session = tunnel.createUploadSession( projectName, tableName, partitionSpec); ``` ```python with table.open_writer(partition='dt=20250101', create_partition=True) as writer: writer.write(records) ``` ```go session, _ := tunnelIns.CreateUploadSession( project.Name(), "my_table", tunnel.SessionCfg.WithPartitionKey("dt=20250101"), ) ``` ### 覆盖写入 ```java // 覆盖表中已有数据 boolean overwrite = true; TableTunnel.UploadSession session = tunnel.createUploadSession( projectName, tableName, overwrite); ``` ```python # 覆盖表中已有数据 with table.open_writer(overwrite=True) as writer: writer.write(records) ``` ```go // 覆盖表中已有数据 session, _ := tunnelIns.CreateUploadSession( project.Name(), "my_table", tunnel.SessionCfg.Overwrite(), ) ``` ## 注意事项 1. **Session 有效期**:每个 UploadSession 在服务端的生命周期为 24 小时,超时后需重新创建。 2. **Writer 超时**:RecordWriter 如果 120 秒内没有网络活动,服务端将主动断开连接,需重新打开 Writer。 3. **Block 大小**:建议每个 Block 数据量不小于 64MB,避免产生大量小文件。单个 Block 上限 100GB。 4. **提交后不可变**:`commit()` 成功后数据立即对外可见,且不可回滚。 5. **Block ID 唯一性**:同一个 Block ID 同时只能有一个 Writer 在写入,否则会导致数据混乱。 6. **Session 不可复用**:一个 Session 只能 commit 一次,后续写入需创建新的 Session。 ## 相关文档 - [UploadSession API 参考](../../reference/UploadSession) - [TableTunnel API 参考](../../reference/TableTunnel) - [Tunnel Stream 流式写入](./tunnel-stream.md) - [Storage API 高性能写入](./storage-api-write.md) --- # guides/write-data/tunnel-upsert.md # Tunnel Upsert 更新插入 :::note Tunnel Upsert 目前仅 Java SDK 支持。Python 和 Go SDK 暂不提供此功能。 ::: Tunnel Upsert 专为 Delta Table(主键表)设计,支持按主键进行数据的插入、更新和删除操作,适合数据库变更同步(CDC)、实时数据修正等场景。 ## 前置条件 - 已初始化 `Odps` 客户端实例 - 已创建 `TableTunnel` 实例(参考 [TableTunnel 文档](../../reference/TableTunnel)) - **目标表必须是 Delta Table(Transactional 表),且定义了主键(Primary Key)** :::info Delta Table 是 MaxCompute 支持行级更新/删除的表类型。建表时需要指定主键列和 `TBLPROPERTIES('transactional'='true')`。 ::: ## 完整示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.data.Record; import com.aliyun.odps.tunnel.TableTunnel; import com.aliyun.odps.tunnel.streams.UpsertStream; public class TunnelUpsertExample { public static void main(String[] args) throws Exception { // 1. 初始化 ODPS 客户端 AliyunAccount account = new AliyunAccount("", ""); Odps odps = new Odps(account); odps.setDefaultProject(""); odps.setEndpoint(""); // 2. 获取 TableTunnel 实例 TableTunnel tunnel = odps.tableTunnel(); // 3. 创建 UpsertSession String projectName = ""; String tableName = ""; try (TableTunnel.UpsertSession upsertSession = tunnel .buildUpsertSession(projectName, tableName) .build()) { System.out.println("Upsert Session ID: " + upsertSession.getId()); // 4. 创建 UpsertStream UpsertStream stream = upsertSession.buildUpsertStream().build(); // 5. 插入/更新记录 Record record = upsertSession.newRecord(); record.setString("id", "1001"); record.setString("name", "alice"); record.setBigint("score", 95L); stream.upsert(record); // 6. 更新另一条记录 Record record2 = upsertSession.newRecord(); record2.setString("id", "1002"); record2.setString("name", "bob"); record2.setBigint("score", 88L); stream.upsert(record2); // 7. 删除一条记录(只需设置主键列) Record deleteRecord = upsertSession.newRecord(); deleteRecord.setString("id", "1003"); stream.delete(deleteRecord); // 8. flush 数据到服务端 stream.flush(); // 9. 关闭 Stream stream.close(); // 10. 提交 Session(数据变为可见) upsertSession.commit(false); // false = 同步提交 System.out.println("Upsert committed successfully."); } } } ``` ## 代码说明 ### 写入流程 1. **创建 UpsertSession**:通过 `buildUpsertSession().build()` 创建会话 2. **创建 UpsertStream**:通过 `upsertSession.buildUpsertStream().build()` 获取写入流 3. **执行操作**:调用 `stream.upsert(record)` 或 `stream.delete(record)` 执行数据变更 4. **flush 缓冲**:调用 `stream.flush()` 将缓冲区数据发送到服务端 5. **关闭 Stream**:调用 `stream.close()` 关闭写入流 6. **提交 Session**:调用 `upsertSession.commit(false)` 使数据对外可见 :::info 数据只有在 `commit()` 后才对外可见。`flush()` 仅将缓冲区数据发送到服务端暂存,并不代表数据可查。 ::: ### Record 创建注意事项 必须使用 `upsertSession.newRecord()` 方法创建 Record 实例,不能手动创建 `ArrayRecord`。因为 `UpsertRecord` 内部维护了必要的元数据隐藏列。 ```java // 正确 Record record = upsertSession.newRecord(); // 错误 - 不要这样做 // Record record = new ArrayRecord(schema); ``` ### 部分列更新 支持仅更新指定列,未指定的列保持原值不变: ```java Record record = upsertSession.newRecord(); record.setString("id", "1001"); // 主键列必须设置 record.setBigint("score", 99L); // 只更新 score 列 List upsertCols = Arrays.asList("id", "score"); stream.upsert(record, upsertCols); ``` ### Flush Listener 模式 通过自定义 Listener 可以监听 flush 事件并实现自定义重试逻辑: ```java UpsertStream stream = upsertSession.buildUpsertStream() .setListener(new UpsertSessionImpl.DefaultUpsertSteamListener() { @Override public void onFlushSuccess(long flushSize) { System.out.println("Flush success, size: " + flushSize); } @Override public void onFlushFailed(String errorMessage, int retryCount) { System.err.println("Flush failed (retry " + retryCount + "): " + errorMessage); } }) .build(); ``` :::tip 推荐继承 `UpsertSessionImpl.DefaultUpsertSteamListener`,以保留 TableTunnel 默认的重试逻辑。 ::: ### 高并发写入 对于需要高吞吐的场景,可以创建多个 UpsertStream 并行写入。需要注意:相同主键的记录应当路由到同一个 Stream 中,避免并发冲突。 ```java try (TableTunnel.UpsertSession upsertSession = tunnel .buildUpsertSession(projectName, tableName) .setSlotNum(4) .build()) { int threadCount = 4; ExecutorService executor = Executors.newFixedThreadPool(threadCount); for (int t = 0; t < threadCount; t++) { final int threadId = t; executor.submit(() -> { try { UpsertStream stream = upsertSession.buildUpsertStream().build(); // 每个线程写入属于自己的数据(按主键 hash 分配) for (Record record : getRecordsForThread(threadId)) { stream.upsert(record); } stream.flush(); stream.close(); } catch (Exception e) { throw new RuntimeException(e); } }); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.HOURS); upsertSession.commit(false); } ``` ## 配置选项 ### Session 构建参数 | 方法 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `setSlotNum(long)` | long | 自动 | 并行槽位数量 | | `setPartitionSpec(String)` | String | - | 目标分区 | | `setSchemaName(String)` | String | - | 三层模型 Schema 名称 | | `setUpsertId(String)` | String | - | 复用已有 Session 的 ID | | `setCommitTimeout(long)` | long | - | 提交超时时间(毫秒) | | `setLifecycle(long)` | long | 服务端默认 | Session 生命周期(小时,1~24) | | `setNetworkThreadNum(int)` | int | 1 | Netty 网络 IO 线程数 | | `setConcurrentNum(int)` | int | 20 | 最大并发 Channel 数 | | `setConnectTimeout(long)` | long | 180000 | 连接超时时间(毫秒) | | `setReadTimeout(long)` | long | 300000 | 请求响应超时时间(毫秒) | ### Stream 构建参数 | 方法 | 类型 | 说明 | |------|------|------| | `setMaxBufferSize(long)` | long | 数据缓冲区最大容量(字节) | | `setSlotBufferSize(long)` | long | 每个桶的缓冲区大小(字节) | | `setCompressOption(CompressOption)` | CompressOption | 压缩选项 | | `setListener(Listener)` | Listener | flush 事件监听器 | ### 压缩选项 自 0.48.3 起,默认压缩算法为 `ODPS_LZ4_FRAME`: ```java UpsertStream stream = upsertSession.buildUpsertStream() .setCompressOption(new CompressOption( CompressOption.CompressAlgorithm.ODPS_LZ4_FRAME, 0, 0)) .build(); ``` ## 注意事项 1. **仅适用于 Delta Table**:目标表必须是 Transactional 表且定义了主键,否则会报错。 2. **主键冲突**:多个并发写入同一主键可能导致不可预期的行为。建议对数据按主键进行 shuffle,将相同主键的记录分配到同一线程。 3. **commit 频率**:推荐 commit 间隔每分区不低于一分钟,过于频繁的 commit 会产生大量小文件,影响查询性能,还可能触发服务端限流。 4. **单次 commit**:每个 Session 只能 commit 一次,继续写入需要重建 Session。 5. **资源清理**:务必调用 `close()` 方法释放客户端资源。推荐使用 try-with-resources 语法。 6. **数据可见性**:数据只有在 `commit()` 之后才对外可见,`flush()` 仅将数据发送到服务端暂存。 ## 相关文档 - [UpsertSession API 参考](../../reference/UpsertSession) - [TableTunnel API 参考](../../reference/TableTunnel) - [Tunnel Upload 批量写入](./tunnel-upload.md) - [Tunnel Stream 流式写入](./tunnel-stream.md) --- # index.md # MaxCompute SDK MaxCompute(ODPS) SDK 是阿里云 MaxCompute 大数据计算服务的官方客户端库,支持 **Java**、**Python** 和 **Go** 三种语言。它为开发者提供了完整的 API,用于管理 MaxCompute 项目中的表、资源、函数等对象,执行 SQL 查询,以及通过 Tunnel 或 Storage API 高效地读写数据。 ## 安装 ```xml com.aliyun.odps odps-sdk-core 0.57.2-public ``` ```bash pip install pyodps ``` ```bash go get github.com/aliyun/aliyun-odps-go-sdk ``` ## 模块概览(Java) | 模块 | artifactId | 说明 | |------|-----------|------| | Core | `odps-sdk-core` | 核心模块,提供 MaxCompute 资源管理、SQL 执行、Tunnel 数据通道等功能 | | Storage API | `odps-sdk-storage-api` | 基于 Arrow 格式的高性能数据读写接口 | | Table API | `odps-sdk-table-api` | 表级别的数据读写高层封装 | | UDF | `odps-sdk-udf` | 用户自定义函数(UDF/UDTF/UDAF)开发框架 | ## 文档导航 - [**快速开始**](getting-started/) - 环境配置与第一个程序 - [**使用指南**](guides/) - SQL 执行、数据读写、表管理等操作指南 - [**模块文档**](modules/) - 各模块的详细 API 说明 - [**API 参考**](reference/) - 类与方法的完整参考文档(Java) - [**常见问题**](faq) - FAQ 与问题排查 ## 相关链接 - [Java SDK GitHub](https://github.com/aliyun/aliyun-odps-java-sdk) - [Python SDK GitHub](https://github.com/aliyun/aliyun-odps-python-sdk) - [Go SDK GitHub](https://github.com/aliyun/aliyun-odps-go-sdk) --- # modules/core.md # Core 核心模块 `odps-sdk-core` 是 MaxCompute Java SDK 的核心模块,提供与 MaxCompute 服务交互的基础能力。通过该模块,用户可以管理项目空间中的各类资源(表、实例、函数、资源文件等),提交和管理 SQL 任务,以及进行数据通道(Tunnel)操作。 该模块是所有其他 SDK 模块的基础依赖,提供了统一的认证、网络通信和资源抽象层。 ## Maven 依赖 ```xml com.aliyun.odps odps-sdk-core ${odps.sdk.version} ``` ## 核心功能 | 功能 | 说明 | |------|------| | **认证管理** | 支持 AliyunAccount、StsAccount、BearerTokenAccount 等多种认证方式 | | **项目管理** | 创建、查询、配置 MaxCompute 项目空间 | | **表管理** | 表的创建、删除、Schema 查询、分区管理等 | | **实例管理** | SQL/任务实例的提交、状态查询、结果获取 | | **函数管理** | 自定义函数的注册和管理 | | **资源管理** | Jar、File、Table 等资源的上传和管理 | | **SQL 执行** | 通过 SQLTask 提交和执行 SQL 语句 | | **Schema 管理** | MaxCompute Schema(命名空间)管理 | | **Tunnel 集成** | 内置 TableTunnel 数据通道支持 | ## 核心类 | 类 | 说明 | |------|------| | `Odps` | SDK 入口类,持有认证信息和服务配置,提供访问各类资源集合的方法 | | `Table` | 表对象,提供 Schema 查询、分区管理、生命周期管理等操作 | | `Instance` | 实例对象,代表一个提交到 MaxCompute 的任务 | | `Project` | 项目空间对象,管理项目级别的配置和属性 | | `Schema` | Schema(命名空间)对象,用于组织表、函数等资源 | | `Function` | 自定义函数对象 | | `Resource` | 资源对象,包括 Jar、File、Table 等类型 | | `SQLTask` | SQL 任务工具类,提供便捷的 SQL 提交和执行方法 | ## 配置 ### 基础配置 ```java Account account = new AliyunAccount("accessId", "accessKey"); Odps odps = new Odps(account); odps.setEndpoint("http://service.cn-hangzhou.maxcompute.aliyun.com/api"); odps.setDefaultProject("my_project"); odps.setCurrentSchema("my_schema"); ``` ### RestClient 配置 | 配置项 | 说明 | 默认值 | |--------|------|--------| | `connectTimeout` | 连接超时时间 | 10 秒 | | `readTimeout` | 读取超时时间 | 120 秒 | | `retryTimes` | 重试次数 | 3 | ### OdpsOptions 通过 `Odps` 对象的 `getOptions()` 方法获取和设置 SDK 全局选项。 ## 使用示例 ```java import com.aliyun.odps.Odps; import com.aliyun.odps.Table; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; import com.aliyun.odps.task.SQLTask; // 1. 初始化 Odps 对象 Account account = new AliyunAccount("accessId", "accessKey"); Odps odps = new Odps(account); odps.setEndpoint("http://service.cn-hangzhou.maxcompute.aliyun.com/api"); odps.setDefaultProject("my_project"); // 2. 访问表 for (Table table : odps.tables()) { System.out.println(table.getName()); } // 3. 获取表 Schema Table table = odps.tables().get("my_table"); table.reload(); System.out.println(table.getSchema()); // 4. 执行 SQL Instance instance = SQLTask.run(odps, "SELECT * FROM my_table LIMIT 10;"); instance.waitForSuccess(); ``` ## 相关文档 - [初始化 Odps 对象](../getting-started/authentication.md) - [Tunnel 模块](./tunnel.md) - [Storage API 模块](./storage-api.md) --- # modules/storage-api.md # Storage API 高性能读写模块 `odps-sdk-storage-api` 是专为高性能数据读写设计的模块,基于 [Apache Arrow](https://arrow.apache.org/) 列式内存格式,提供高效的数据传输能力。相比传统 Tunnel 接口,Storage API 具备更低的序列化开销和更好的并行处理能力,适合大规模数据批量导入导出场景。 ## Maven 依赖 ```xml com.aliyun.odps odps-sdk-storage-api ${odps.sdk.version} ``` ## 核心功能 | 特性 | 说明 | |------|------| | **高性能** | 基于 Apache Arrow 列式格式,减少序列化/反序列化开销 | | **并行读写** | 支持将数据切分为多个 Split,并行读取或写入 | | **事务性写入** | 批量(Batch)模式下写入为原子操作,提交后才对外可见 | | **流式写入** | Streaming 模式下数据 flush 后立即可见,无需显式提交 | | **列裁剪** | 读取时可指定所需列,减少网络传输量 | | **分区过滤** | 读取时可指定分区,按需加载数据 | | **服务端过滤** | 支持下推过滤谓词,减少数据传输量 | | **增量读取** | 支持增量读取模式,读取表数据的增量变化 | ## 架构概览 ```mermaid graph TB Client["客户端应用"] SC["MaxStorageClient"] Read["读取流程"] Write["写入流程"] RSB["TableReadSessionBuilder"] RS["TableReadSession"] SP["InputSplit[]"] RB["TableReaderBuilder"] AR["ArrowReader"] WSB["TableWriteSessionBuilder"] WS["TableWriteSession"] WB["TableWriterBuilder"] AW["TableArrowWriter"] Client --> SC SC --> Read SC --> Write Read --> RSB --> RS --> SP SP --> RB --> AR Write --> WSB --> WS --> WB --> AW ``` ## 核心类 | 类 | 说明 | |------|------| | `MaxStorageClient` | 客户端入口,线程安全,建议全局复用长期持有 | | `TableReadSessionBuilder` | 构建表读取 Session 的 Builder,支持列裁剪和分区过滤 | | `TableReadSession` | 表读取 Session,包含分片信息和 Schema | | `ArrowReader` | Arrow 格式数据读取器,逐批次读取 VectorSchemaRoot | | `TableWriteSessionBuilder` | 构建表写入 Session 的 Builder,支持选择写入模式 | | `TableWriteSession` | 表写入 Session,管理事务生命周期 | | `TableArrowWriter` | Arrow 格式写入器,将 VectorSchemaRoot 写入服务端 | | `TableIdentifier` | 表标识,包含项目名和表名 | ## 配置 ### 客户端构建 ```java MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); ``` ### 线程安全说明 | 组件 | 线程安全性 | 使用建议 | |------|-----------|----------| | `MaxStorageClient` | 线程安全 | 全局复用,适合作为单例 | | `TableReadSession` / `TableWriteSession` | 非线程安全 | 每个操作使用独立实例 | | `ArrowReader` / `TableArrowWriter` | 非线程安全 | 每个线程独立使用 | ### 资源管理 所有 Session、Reader、Writer 均实现了 `AutoCloseable` 接口,推荐使用 `try-with-resources` 语法: ```java try (TableReadSession session = client.createTableReadSessionBuilder(tableId).build(); ArrowReader reader = session.createReaderBuilder(splits.get(0)).build()) { // 使用 reader } // 自动关闭 reader 和 session ``` ## 使用示例 ### 读取表数据 ```java // 1. 构建客户端 MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); // 2. 创建读取 Session TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "name", "age")) .build(); // 3. 获取 Splits 并行读取 List splits = session.getSplits(); for (InputSplit split : splits) { try (ArrowReader reader = session.createReaderBuilder(split).build()) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); System.out.println("读取行数: " + root.getRowCount()); } } } ``` ### 写入表数据(批量模式) ```java // 1. 构建客户端 MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .build(); // 2. 创建写入 Session(批量模式) TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); try (TableWriteSession session = client.createTableWriteSessionBuilder(tableId).build()) { // 3. 创建 Writer try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { VectorSchemaRoot root = ((TableArrowWriter) writer).createVectorSchemaRoot(); try { // 填充数据并写入 // ... 填充 root 中的数据 ... writer.writeBatch(root); writer.flush(); } finally { root.close(); } } // 4. 提交事务,数据对外可见 session.commit(); } ``` ### 写入表数据(流式模式) ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); try (TableWriteSession session = client.createTableWriteSessionBuilder(tableId) .withWriteMode(WriteMode.STREAMING) .build()) { try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { // flush 后数据立即可见,无需 commit writer.writeBatch(root); writer.flush(); } // Streaming 模式无需显式 commit } ``` ## 相关文档 - [Storage API 详细文档] - [MaxStorageClient](../reference/MaxStorageClient.md) - [读取操作](../guides/read-data/storage-api-read.md) - [写入操作](../guides/write-data/storage-api-write.md) - [Tunnel 模块](./tunnel.md) - 传统数据通道方案对比 --- # modules/tunnel.md # Tunnel 数据通道模块 Tunnel 是 MaxCompute 提供的高效数据通道,用于大规模数据的批量上传和下载。它支持多种上传模式(流式/批量)和写入模式(追加/更新插入),并提供灵活的压缩、重试策略配置,以满足不同业务场景下的数据传输需求。 Tunnel 功能内置于 `odps-sdk-core` 模块中,无需额外引入依赖。 ## Maven 依赖 ```xml com.aliyun.odps odps-sdk-core ${odps.sdk.version} ``` ## 核心功能 | 功能 | 说明 | |------|------| | **批量上传** | 通过 UploadSession 实现手动提交的批量数据写入 | | **流式上传** | 通过 StreamUploadSession 实现自动提交的实时数据写入 | | **Upsert 写入** | 通过 UpsertSession 对 Delta Table 进行更新/插入操作 | | **数据下载** | 通过 DownloadSession 实现表数据的批量下载 | | **数据压缩** | 支持 LZ4、ZSTD、ZLIB、Snappy 等多种压缩算法 | | **自动重试** | 内置智能重试机制,对限流、网络异常等自动恢复 | | **Quota 管理** | 支持指定独享 Tunnel Quota 资源 | ## 核心类 | 类 | 说明 | |------|------| | `TableTunnel` | Tunnel 入口类,通过 `odps.tableTunnel()` 获取 | | `UploadSession` | 批量上传会话,数据需手动 commit 后才对查询可见 | | `DownloadSession` | 数据下载会话,支持按范围读取表数据 | | `StreamUploadSession` | 流式上传会话,数据由服务端自动提交,适合实时场景 | | `UpsertSession` | 更新插入会话,用于 Delta Table 的 upsert 操作 | | `Configuration` | Tunnel 配置类,通过 Builder 模式设置各项参数 | | `TunnelRetryHandler` | 重试处理器,管理错误重试策略和回退机制 | ## 配置 ### Configuration Builder 通过 `Configuration.builder(odps)` 创建配置对象: ```java import com.aliyun.odps.tunnel.Configuration; Configuration configuration = Configuration.builder(odps) .withQuotaName("my_quota") .withTags(Arrays.asList("tag1", "tag2")) .withCompressOptions(new CompressOption( CompressOption.CompressAlgorithm.ODPS_LZ4_FRAME, 0, 0)) .withRetryPolicy(new CustomRetryPolicy()) .withRetryLogger(new CustomRetryLogger()) .build(); TableTunnel tunnel = odps.tableTunnel(configuration); ``` ### 配置项 | 配置项 | 方法 | 说明 | |--------|------|------| | Quota 名称 | `withQuotaName(String)` | 指定独享 Tunnel Quota | | 标签列表 | `withTags(List)` | 设置请求标签 | | 压缩选项 | `withCompressOptions(CompressOption)` | 设置数据传输压缩方式 | | 重试策略 | `withRetryPolicy(RetryPolicy)` | 自定义错误重试逻辑 | | 重试日志 | `withRetryLogger(RetryLogger)` | 重试事件日志记录 | ### 压缩算法对比 | 算法 | 标识 | 特点 | 适用场景 | |------|------|------|----------| | 无压缩 | `ODPS_RAW` | 无 CPU 开销 | 低延迟场景 | | ZLIB | `ODPS_ZLIB` | 压缩率高,CPU 消耗较大 | 带宽敏感场景(默认) | | Snappy | `ODPS_SNAPPY` | 压缩/解压速度快 | 平衡场景 | | LZ4 | `ODPS_LZ4_FRAME` | 极高的压缩/解压速度 | 高吞吐场景 | | Arrow LZ4 | `ODPS_ARROW_LZ4_FRAME` | Arrow 格式专用 LZ4 | Arrow Tunnel | | Arrow ZSTD | `ODPS_ARROW_ZSTD` | Arrow 格式专用 ZSTD,高压缩率 | Arrow Tunnel | ### 重试策略 SDK 内置了对常见服务端错误的自动重试: | 错误码 | 处理方式 | |--------|----------| | **429** | 服务端限流,无限指数退避重试(最大等待 64 秒) | | **502/504** | 触发会话重新加载,指数退避重试(1~64 秒) | | **5xx** | 服务端错误,指数退避重试(1~64 秒) | | **308** | 仅 UpsertSession,更新缓存 IP 后无限重试 | | **其他** | 使用用户自定义 RetryPolicy(默认不重试) | #### 自定义 RetryPolicy ```java public class CustomRetryPolicy implements TunnelRetryHandler.RetryPolicy { @Override public boolean shouldRetry(Exception e, int attempt) { // 自定义重试判断逻辑 return attempt < 5; } @Override public long getRetryWaitTime(int attempt) { // 指数退避 return Math.min(1000L * (1 << attempt), 64000L); } } ``` #### 自定义 RetryLogger ```java public class CustomRetryLogger extends RestClient.RetryLogger { @Override public void onRetryLog(Throwable e, long retryCount, long retrySleepTime) { System.out.println("Retry #" + retryCount + " after " + retrySleepTime + "s due to: " + e.getMessage()); } } ``` ## 使用示例 ### 初始化 TableTunnel ```java // 无参初始化(使用默认配置) TableTunnel tunnel = odps.tableTunnel(); // 使用自定义配置初始化 Configuration config = Configuration.builder(odps) .withCompressOptions(new CompressOption( CompressOption.CompressAlgorithm.ODPS_LZ4_FRAME, 0, 0)) .build(); TableTunnel tunnel = odps.tableTunnel(config); ``` ### 批量上传 ```java TableTunnel.UploadSession session = tunnel.createUploadSession( "my_project", "my_table"); // 获取 RecordWriter 写入数据 RecordWriter writer = session.openRecordWriter(0); Record record = session.newRecord(); record.setString("name", "test"); record.setBigint("id", 1L); writer.write(record); writer.close(); // 提交数据 session.commit(new long[]{0}); ``` ### 数据下载 ```java TableTunnel.DownloadSession session = tunnel.createDownloadSession( "my_project", "my_table"); long count = session.getRecordCount(); RecordReader reader = session.openRecordReader(0, count); Record record; while ((record = reader.read()) != null) { System.out.println(record.getString("name")); } reader.close(); ``` ## 相关文档 - [使用 Tunnel 对表进行上传下载] - [Configuration 配置类] - [RetryPolicy 重试逻辑] - [UploadSession 文档](../reference/UploadSession.md) - [DownloadSession 文档](../reference/DownloadSession.md) - [StreamUploadSession 文档](../reference/StreamUploadSession.md) - [UpsertSession 文档](../reference/UpsertSession.md) --- # modules/udf.md # UDF 开发模块 `odps-sdk-udf` 是 MaxCompute 用户自定义函数的开发支持模块,提供 UDF(标量函数)、UDTF(表值函数)、UDAF(聚合函数)的基类和运行时接口。该模块还支持向量化(Vectorized)批处理模式,通过基于 Apache Arrow 的批量数据处理显著提升执行性能。 ## Maven 依赖 ```xml com.aliyun.odps odps-sdk-udf ${odps.sdk.version} provided ``` > **注意**:UDF 开发时使用 `provided` scope,因为运行时环境由 MaxCompute 计算集群提供。 ## 核心功能 | 功能 | 说明 | |------|------| | **标量函数 (UDF)** | 一对一映射,输入一行输出一个值 | | **表值函数 (UDTF)** | 一对多映射,输入一行可输出多行 | | **聚合函数 (UDAF)** | 多对一映射,对多行数据进行聚合计算 | | **向量化处理** | 基于 Arrow 的批处理模式,减少函数调用开销 | | **自定义 Extractor** | 自定义外部数据源读取逻辑 | | **自定义 Outputer** | 自定义外部数据源写入逻辑 | | **数据分片** | 通过 InputSplitter 自定义数据分片策略 | ## 核心类 | 类 | 说明 | |------|------| | `UDF` | 标量函数基类,实现 `evaluate` 方法定义计算逻辑 | | `UDTF` | 表值函数基类,通过 `process` 方法处理输入并通过 `forward` 输出多行 | | `Aggregator` | 聚合函数基类,实现 `iterate`/`merge`/`terminate` 定义聚合逻辑 | | `VectorizedExtractor` | 向量化数据读取器,批量处理 Arrow 格式数据 | | `VectorizedOutputer` | 向量化数据写入器,批量输出 Arrow 格式数据 | | `VectorizedStorageHandler` | 向量化存储处理器,组合 Extractor 和 Outputer | | `InputSplitter` | 数据分片接口,自定义外部数据的分片策略 | | `ExecutionContext` | 运行时上下文,提供任务参数和资源访问能力 | ## 配置 ### 与 Core 模块的关系 UDF 模块专注于函数开发,不依赖 `odps-sdk-core`。函数的注册和管理(如 `CREATE FUNCTION`)通过 Core 模块的 `Function` 类完成: ```java // 使用 Core 模块注册 UDF(非 UDF 开发代码) odps.functions().create(functionInfo); ``` ### 开发环境配置 UDF 开发项目的典型依赖配置: ```xml com.aliyun.odps odps-sdk-udf ${odps.sdk.version} provided org.apache.arrow arrow-vector 1.0.0 provided ``` ## 使用示例 ### 标量函数 (UDF) ```java import com.aliyun.odps.udf.UDF; public class MyPlus extends UDF { public Long evaluate(Long a, Long b) { if (a == null || b == null) { return null; } return a + b; } } ``` 注册并使用: ```sql CREATE FUNCTION my_plus AS 'com.example.MyPlus' USING 'my_udf.jar'; SELECT my_plus(col1, col2) FROM my_table; ``` ### 表值函数 (UDTF) ```java import com.aliyun.odps.udf.UDTF; import com.aliyun.odps.udf.UDTFCollector; import com.aliyun.odps.udf.annotation.Resolve; @Resolve({"string->string,string"}) public class SplitKV extends UDTF { @Override public void process(Object[] args) throws UDFException { String input = (String) args[0]; if (input == null) return; String[] parts = input.split("=", 2); if (parts.length == 2) { forward(parts[0], parts[1]); } } } ``` ### 聚合函数 (UDAF) ```java import com.aliyun.odps.udf.Aggregator; import com.aliyun.odps.udf.annotation.Resolve; import com.aliyun.odps.io.LongWritable; import com.aliyun.odps.io.Writable; @Resolve({"bigint->bigint"}) public class MySum extends Aggregator { @Override public Writable newBuffer() { return new LongWritable(0L); } @Override public void iterate(Writable buffer, Writable[] args) { LongWritable sum = (LongWritable) buffer; LongWritable value = (LongWritable) args[0]; if (value != null) { sum.set(sum.get() + value.get()); } } @Override public void merge(Writable buffer, Writable partial) { LongWritable sum = (LongWritable) buffer; LongWritable partialSum = (LongWritable) partial; sum.set(sum.get() + partialSum.get()); } @Override public Writable terminate(Writable buffer) { return buffer; } } ``` ### 向量化处理 向量化模式通过批量处理 Arrow 格式数据来提升性能,适用于大数据量场景: ```java import com.aliyun.odps.udf.VectorizedExtractor; import org.apache.arrow.vector.VectorSchemaRoot; public class MyVectorizedExtractor extends VectorizedExtractor { @Override public VectorSchemaRoot extract() throws IOException { // 批量读取并返回 Arrow 格式数据 // 返回 null 表示数据读取完毕 return root; } } ``` ## 相关文档 - [Core 核心模块](./core.md) - 函数注册和管理 - [MaxCompute UDF 开发指南](https://help.aliyun.com/document_detail/27867.html) --- # operations/tunnel-tags.md 在**资源观测**场景下,可能会遇到对 Tunnel 操作打标的需求,用来后续统计 Tunnel 操作的数量和相关信息。本文档介绍如何对 Tunnel 操作打标。 ## 打标方式 ### Tunnel 相关操作 对于 Tunnel 相关操作,可以将需要配置的标记(Tags)配置在 [Configuration](../modules/tunnel.md) 类中。 Configuration 类提供了 [withTags](../modules/tunnel.md) 方法,可以将需要配置的标记(Tags)配置在 Configuration 类中。 **使用示例**: ```java // ingore imports public class TagsTest { private static final String ENDPOINT; private static final String ACCESS_ID; private static final String ACCESS_KEY; private static final String PROJECT; public static void main(String[] args) throws TunnelException, IOException { Account account = new AliyunAccount(ACCESS_ID, ACCESS_KEY); Odps odps = new Odps(account); odps.setEndpoint(ENDPOINT); odps.setDefaultProject(PROJECT); Configuration configuration = Configuration.builder(odps).withTags(ImmutableList.of("test_tag")).build(); TableTunnel.DownloadSession downloadSession = odps.tableTunnel(configuration).buildDownloadSession(odps.getDefaultProject(), "testtable") .build(); long recordCount = downloadSession.getRecordCount(); TunnelRecordReader recordReader = downloadSession.openRecordReader(0, recordCount); recordReader.forEach(record -> { System.out.println(record); }); } } ``` ### 开放存储相关操作 对于 Tunnel 相关操作,可以将需要配置的标记(Tags)配置在 EnvironmentSettings 类中。 EnvironmentSettings.Builder 同样提供了 [withTags] 方法,可以将需要配置的标记(Tags)配置在 EnvironmentSettings 类中。 **使用示例**: ```java // ingore imports public class Main { private static final String ENDPOINT; private static final String ACCESS_ID; private static final String ACCESS_KEY; private static final String PROJECT; public static void main(String[] args) throws IOException { Account account = new AliyunAccount(ACCESS_ID, ACCESS_KEY); Credentials credentials = Credentials.newBuilder().withAccount(account) .build(); EnvironmentSettings settings = EnvironmentSettings.newBuilder().withCredentials(credentials) .withServiceEndpoint(ENDPOINT) .withTags(ImmutableList.of("test_tag")) .build(); TableReadSessionBuilder builder = new TableReadSessionBuilder(); TableBatchReadSession session = builder.withSettings(settings) .identifier(TableIdentifier.of(PROJECT, "tableName")) .buildBatchReadSession(); InputSplitAssigner inputSplitAssigner = session.getInputSplitAssigner(); InputSplit[] allSplits = inputSplitAssigner.getAllSplits(); for (InputSplit split : allSplits) { SplitReader arrowReader = session.createArrowReader(split, ReaderOptions.newBuilder().withSettings(settings).build()); while (arrowReader.hasNext()) { VectorSchemaRoot vectorSchemaRoot = arrowReader.get(); System.out.println(vectorSchemaRoot.contentToTSVString()); } arrowReader.close(); } } } ``` ## 观测方式 对于 Tunnel Tags 的操作记录,经过约 5 分钟左右的处理, 将被保存在 [Information Schema](https://help.aliyun.com/zh/maxcompute/user-guide/tenant-level-information-schema) 中的 tunnels_history 表中。 可以通过下述 SQL 查询经过打标的操作记录 ```sql set odps.namespace.schema = true; select * from SYSTEM_CATALOG.INFORMATION_SCHEMA.TUNNELS_HISTORY where app_tags="test_tag"; ``` :::info 存在表 tunnels_history 中没有 app_tags 字段的场景,是因为该功能在您所在的区域还未开通,请耐心等待。 ::: --- # operations/types.md ## MaxCompute 类型与 Java 类型映射关系 | ODPS 类型 | Java 类型 | |:----------------------------|:---------------------------------| | `TINYINT` | `java.lang.Byte` | | `SMALLINT` | `java.lang.Short` | | `INT` | `java.lang.Integer` | | `BIGINT` | `java.lang.Long` | | `BINARY` | `com.aliyun.odps.data.Binary` | | `FLOAT` | `java.lang.Float` | | `DOUBLE` | `java.lang.Double` | | `DECIMAL(precision, scale)` | `java.math.BigDecimal` | | `VARCHAR(n)` | `com.aliyun.odps.data.Varchar` | | `CHAR(n)` | `com.aliyun.odps.data.Char` | | `STRING` | `java.lang.String` | | `DATE` | `java.time.LocalDate` | | `DATETIME` | `java.time.ZonedDateTime` | | `TIMESTAMP` | `java.time.Instant` | | `BOOLEAN` | `java.lang.Boolean` | | `TIMESTAMP_NTZ` | `java.time.LocalDateTime` | | `ARRAY` | `java.util.List` | | `MAP` | `java.util.Map` | | `STRUCT` | `com.aliyun.odps.data.Struct` | | `JSON` | `com.aliyun.odps.data.JsonValue` | ## MaxCompute Type 到 Arrow Type 映射表 | MaxCompute Type | Arrow Type (`ValueVector`) | 备注 (Notes) | |:----------------|:---------------------------|:------------------------------------------------------------| | **基础类型** | | | | `BOOLEAN` | `BitVector` | | | `TINYINT` | `TinyIntVector` | | | `SMALLINT` | `SmallIntVector` | | | `INT` | `IntVector` | | | `BIGINT` | `BigIntVector` | | | `FLOAT` | `Float4Vector` | | | `DOUBLE` | `Float8Vector` | | | `DECIMAL` | `DecimalVector` | 在 `isExtension` 模式下,,为支持更高精度,可能映射到 `FixedSizeBinaryVector`。 | | **字符串与二进制类型** | | | | `STRING` | `VarCharVector` | | | `VARCHAR` | `VarCharVector` | | | `CHAR` | `VarCharVector` | | | `JSON` | `VarCharVector` | `JSON` 类型在 Arrow 层面被当作字符串处理。 | | `BINARY` | `VarBinaryVector` | | | **日期与时间类型** | | | | `DATE` | `DateDayVector` | 表示自 epoch 以来的天数。 | | `DATETIME` | `TimeStampVector` | | | `TIMESTAMP` | `TimeStampVector` | 在 `isExtension` 模式下,为支持更高精度,会映射到 `StructVector`。 | | `TIMESTAMP_NTZ` | `TimeStampVector` | 在 `isExtension` 模式下,为支持更高精度,会映射到 `StructVector`。 | | **复杂类型** | | | | `ARRAY` | `ListVector` | | | `MAP` | `MapVector` | | | `STRUCT` | `StructVector` | | 注:isExtension 仅在 preview 接口中被使用 --- # reference/DownloadSession.md # DownloadSession `DownloadSession` 用于从 MaxCompute 表下载数据,支持分块并行读取、列投影和 Arrow 格式。 ## 获取实例 通过 `TableTunnel.buildDownloadSession` 构建(推荐): ```java DownloadSession session = tunnel.buildDownloadSession("my_project", "my_table") .setPartitionSpec(new PartitionSpec("dt=20231001")) .build(); ``` ### DownloadSessionBuilder 配置 | 方法 | 参数类型 | 必需 | 默认值 | 说明 | |------|----------|------|--------|------| | `setProjectName(String)` | String | 是 | - | 项目名称 | | `setTableName(String)` | String | 是 | - | 表名称 | | `setSchemaName(String)` | String | 否 | null | Schema 名称(三层模型) | | `setPartitionSpec(PartitionSpec)` | PartitionSpec | 否 | null | 分区表达式 | | `setShardId(Long)` | Long | 否 | null | 指定分片 ID | | `setAsyncMode(boolean)` | boolean | 否 | false | 启用异步初始化 | | `setWaitAsyncBuild(boolean)` | boolean | 否 | false | 阻塞等待异步会话就绪 | | `setDownloadId(String)` | String | 否 | null | 复用已有会话 ID | **异步模式示例**: ```java DownloadSessionBuilder builder = tunnel.buildDownloadSession("my_project", "my_table") .setAsyncMode(true); DownloadSession session = builder.build(); boolean ready = builder.wait(session, 5, 300); // 每5秒检查,最长5分钟 ``` ## 方法列表 ### getSchema 获取表结构信息。 ```java public TableSchema getSchema() ``` **返回值**:`TableSchema` 对象 --- ### getRecordCount 获取可下载的总记录数。 ```java public long getRecordCount() ``` --- ### getId 获取会话 ID。 ```java public String getId() ``` --- ### getStatus 获取会话状态。 ```java public DownloadStatus getStatus() ``` --- ### getSplitCount 获取数据分片数量(用于 Arrow 读取)。 ```java public long getSplitCount() ``` --- ### getQuotaName 获取本次下载使用的 Quota 名称。 ```java public String getQuotaName() ``` --- ### getArrowSchema 获取 Arrow 格式的表结构。 ```java public Schema getArrowSchema() ``` **返回值**:Arrow `Schema` 对象 --- ### openRecordReader 打开数据读取器。 ```java // 基础形式 public TunnelRecordReader openRecordReader(long start, long count) throws TunnelException, IOException // 布尔压缩开关 public TunnelRecordReader openRecordReader(long start, long count, boolean compress) throws TunnelException, IOException // 带压缩选项 public TunnelRecordReader openRecordReader(long start, long count, CompressOption option) throws TunnelException, IOException // 指定列裁剪 public TunnelRecordReader openRecordReader(long start, long count, CompressOption compress, List columns) throws TunnelException, IOException // 列投影 + 压缩 + 版本检查 public TunnelRecordReader openRecordReader(long start, long count, CompressOption option, List columns, boolean disableModifiedCheck) throws TunnelException, IOException ``` | 参数 | 类型 | 约束 | 说明 | |------|------|------|------| | `start` | long | >= 0 | 读取起始位置(行号) | | `count` | long | >= 1 | 读取记录数量 | | `compress` | boolean | - | 是否启用压缩 | | `option` | CompressOption | - | 压缩配置 | | `columns` | List\ | 非空 | 需下载的列集合 | | `disableModifiedCheck` | boolean | - | 禁用数据版本校验 | **返回值**:`TunnelRecordReader` 对象,建议使用 try-with-resources **示例**: ```java long total = session.getRecordCount(); try (TunnelRecordReader reader = session.openRecordReader(0, total)) { Record record; while ((record = reader.read()) != null) { System.out.println(record.get("user_id")); } } ``` --- ### openBufferedRecordReader 打开带缓冲区的记录读取器。 ```java public RecordReader openBufferedRecordReader(long start, long count, long batchSize, long bufferSize, CompressOption compress, List columns, boolean disableModifiedCheck) throws TunnelException, IOException ``` | 参数 | 类型 | 约束 | 说明 | |------|------|------|------| | `start` | long | >= 0 | 读取起始位置(行号) | | `count` | long | >= 1 | 读取记录数量 | | `batchSize` | long | >= 1 | 每批次读取的记录数 | | `bufferSize` | long | >= 1 | 缓冲区大小 | | `compress` | CompressOption | - | 压缩配置 | | `columns` | List\ | 非空 | 需下载的列集合 | | `disableModifiedCheck` | boolean | - | 禁用数据版本校验 | **返回值**:`RecordReader` 对象,建议使用 try-with-resources --- ### openArrowRecordReader 打开 Arrow 格式数据读取器,适合高性能列式数据处理。 ```java // 基础形式 public ArrowRecordReader openArrowRecordReader(long start, long count) throws TunnelException, IOException // 按分片索引读取 public ArrowRecordReader openArrowRecordReader(long splitIndex) throws TunnelException, IOException // 指定压缩 public ArrowRecordReader openArrowRecordReader(long start, long count, CompressOption compress) throws TunnelException, IOException // 指定列 public ArrowRecordReader openArrowRecordReader(long start, long count, List columns) throws TunnelException, IOException // 指定列和内存分配器 public ArrowRecordReader openArrowRecordReader(long start, long count, List columns, BufferAllocator allocator) throws TunnelException, IOException // 完整参数 public ArrowRecordReader openArrowRecordReader(long start, long count, List columns, BufferAllocator allocator, CompressOption compress) throws TunnelException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `start` | long | 起始位置 | | `count` | long | 读取数量 | | `splitIndex` | long | 分片索引(配合 `getSplitCount()` 使用) | | `compress` | CompressOption | 压缩配置 | | `columns` | List\ | 需下载的列集合 | | `allocator` | BufferAllocator | Arrow 内存分配器 | **示例**: ```java try (ArrowRecordReader reader = session.openArrowRecordReader(0, 50000)) { VectorSchemaRoot root = reader.read(); FieldVector idVector = root.getVector("user_id"); for (int i = 0; i < root.getRowCount(); i++) { System.out.println(idVector.getObject(i)); } } ``` --- ### 分块并行下载示例 ```java long totalRecords = session.getRecordCount(); int parallelism = 8; ExecutorService pool = Executors.newFixedThreadPool(parallelism); for (int i = 0; i < parallelism; i++) { long start = i * (totalRecords / parallelism); long count = (i == parallelism - 1) ? totalRecords - start : totalRecords / parallelism; pool.submit(() -> { try (TunnelRecordReader reader = session.openRecordReader(start, count)) { Record record; while ((record = reader.read()) != null) { processRecord(record); } } }); } ``` --- # reference/Functions.md # Functions `Functions` 类用于管理 MaxCompute 项目中的用户自定义函数 (UDF)。通过此类可以创建、更新、删除和查询函数。 ## 获取实例 ```java Functions functions = odps.functions(); ``` ## 方法 ### create 创建函数。 ```java public void create(Function function) throws OdpsException public void create(String projectName, Function function) throws OdpsException public void create(String projectName, String schemaName, Function function) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `function` | `Function` | 函数定义对象 | | `projectName` | `String` | 目标项目名称(可选) | | `schemaName` | `String` | 目标 Schema 名称(可选) | **Function 对象构建**: ```java Function function = new Function(); function.setName("my_function"); function.setClassPath("com.example.MyFunction"); function.setResources(Arrays.asList("resource1.jar", "resource2.py")); ``` --- ### get 获取函数对象。 ```java public Function get(String functionName) public Function get(String projectName, String functionName) public Function get(String projectName, String schemaName, String functionName) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `functionName` | `String` | 函数名称 | | `projectName` | `String` | 项目名称(可选) | | `schemaName` | `String` | Schema 名称(可选) | **返回值**:`Function` 对象 --- ### exists 检查函数是否存在。 ```java public boolean exists(String functionName) throws OdpsException public boolean exists(String projectName, String functionName) throws OdpsException public boolean exists(String projectName, String schemaName, String functionName) throws OdpsException ``` **返回值**:存在返回 `true` --- ### update 更新函数定义。 ```java public void update(Function function) throws OdpsException public void update(String projectName, Function function) throws OdpsException public void update(String projectName, String schemaName, Function function) throws OdpsException ``` --- ### delete 删除函数。 ```java public void delete(String functionName) throws OdpsException public void delete(String projectName, String functionName) throws OdpsException public void delete(String projectName, String schemaName, String functionName) throws OdpsException ``` --- ### iterator 获取函数迭代器。 ```java public Iterator iterator() public Iterator iterator(String projectName) public Iterator iterator(String projectName, String schemaName) ``` --- ### iterable 获取函数 Iterable(支持 foreach 语法)。 ```java public Iterable iterable() public Iterable iterable(String projectName) public Iterable iterable(String projectName, String schemaName) ``` --- ## Function 对象属性 | 方法 | 返回类型 | 说明 | |------|----------|------| | `getName()` | `String` | 函数名称 | | `getClassPath()` | `String` | 函数实现类路径 | | `getResources()` | `List` | 依赖资源列表 | | `getOwner()` | `String` | 函数所有者 | | `getCreatedTime()` | `Date` | 创建时间 | --- ## 使用示例 ```java Functions functions = odps.functions(); // 创建函数 Function func = new Function(); func.setName("my_udf"); func.setClassPath("com.example.MyUDF"); func.setResources(Arrays.asList("my_udf.jar")); functions.create(func); // 遍历所有函数 for (Function f : functions.iterable()) { System.out.println(f.getName() + " -> " + f.getClassPath()); } // 删除函数 functions.delete("my_udf"); ``` --- # reference/Instance.md # Instance `Instance` 类表示 MaxCompute 中计算任务的一次运行实例。每个 SQL 查询、MapReduce 任务等提交后都会创建一个 Instance。通过 Instance 可以管理任务生命周期、查询执行状态、获取运行结果。 ## 获取实例 通过 `Instances` 集合获取 Instance 对象: ```java // 创建任务并获取 Instance Instance instance = odps.instances().create(task); // 通过 ID 获取已有 Instance Instance instance = odps.instances().get("instance_id"); ``` ## Instances 集合操作 以下方法属于 `Instances` 集合管理类,通过 `odps.instances()` 访问。 ### create 提交任务创建 Instance。 ```java public Instance create(Task task) throws OdpsException public Instance create(Task task, int priority) throws OdpsException public Instance create(Task task, int priority, String runningCluster) throws OdpsException public Instance create(String projectName, Task task) throws OdpsException public Instance create(String projectName, Task task, int priority) throws OdpsException public Instance create(String projectName, Task task, int priority, String runningCluster) throws OdpsException public Instance create(Job job) throws OdpsException public Instance create(Task task, CreateInstanceOption option) throws OdpsException public Instance create(List tasks, CreateInstanceOption option) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `task` | `Task` | 任务对象(如 SQLTask) | | `priority` | `int` | 任务优先级(0-9,数值越小优先级越高) | | `runningCluster` | `String` | 指定运行集群 | | `projectName` | `String` | 项目名称 | | `job` | `Job` | 作业对象(包含多个 Task) | | `option` | `CreateInstanceOption` | 创建选项 | --- ### get 获取已有 Instance 对象。 ```java public Instance get(String id) public Instance get(String projectName, String id) ``` --- ### exists 判断 Instance 是否存在。 ```java public boolean exists(String id) throws OdpsException public boolean exists(String projectName, String id) throws OdpsException ``` --- ### iterator / iterable 遍历项目中的 Instance。 ```java public Iterator iterator() public Iterator iterator(String project) public Iterator iterator(InstanceFilter filter) public Iterator iterator(String project, InstanceFilter filter) public Iterable iterable() public Iterable iterable(String project) public Iterable iterable(InstanceFilter filter) public Iterable iterable(String project, InstanceFilter filter) ``` InstanceFilter 属性: | 属性 | 说明 | |------|------| | `status` | 实例状态(Running / Suspended / Terminated) | | `onlyOwner` | 是否只列出当前用户的实例 | | `fromTime` | 起始时间 | | `endTime` | 结束时间 | | `quotaIndex` | Quota 索引 | 示例: ```java InstanceFilter filter = new InstanceFilter(); filter.setStatus(Instance.Status.Running); for (Instance inst : odps.instances().iterable(filter)) { System.out.println(inst.getId() + " " + inst.getStatus()); } ``` --- ### iteratorQueueing 遍历排队中的 Instance 信息。 ```java public Iterator iteratorQueueing() public Iterator iteratorQueueing(String project) public Iterator iteratorQueueing(InstanceFilter filter) public Iterator iteratorQueueing(String project, InstanceFilter filter) ``` --- ### getDefaultRunningCluster / setDefaultRunningCluster 获取或设置默认运行集群。 ```java public String getDefaultRunningCluster() public void setDefaultRunningCluster(String defaultRunningCluster) ``` --- ## 状态枚举 ```java public enum Status { RUNNING, // 正在执行 SUSPENDED, // 被挂起 TERMINATED // 执行结束(包括成功、失败、取消) } ``` ## 方法 ### getId 获取 Instance 的唯一标识符。 ```java public String getId() ``` **返回值**:Instance ID 字符串 --- ### getStatus 获取 Instance 当前运行状态。 ```java public Status getStatus() public Status getStatus(boolean isBlock) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `isBlock` | `boolean` | 是否使用 block 模式。启用后请求会在服务端等待约 5s 再返回状态 | **返回值**:`Instance.Status` 枚举值 --- ### isTerminated 检查 Instance 是否已执行完成。 ```java public boolean isTerminated() ``` **返回值**:`TERMINATED` 状态返回 `true`,否则返回 `false` --- ### isSuccessful 检查 Instance 是否执行成功(所有 Task 状态均为 SUCCESS)。 ```java public boolean isSuccessful() throws OdpsException ``` **返回值**:所有 Task 成功返回 `true` --- ### waitForSuccess 阻塞当前线程,直到 Instance 执行结束。若任务失败则抛出异常。 ```java public void waitForSuccess() throws OdpsException public void waitForSuccess(long interval) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `interval` | `long` | 轮询间隔(毫秒),默认 1000ms | **异常**:Instance 中任何 Task 失败时抛出 `OdpsException` **示例**: ```java Instance instance = odps.instances().create(task); instance.waitForSuccess(); Map results = instance.getTaskResults(); ``` --- ### waitForTerminated 阻塞当前线程直到 Instance 结束,不检查任务是否成功。 ```java public void waitForTerminated(long interval, boolean isBlock) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `interval` | `long` | 轮询间隔(毫秒) | | `isBlock` | `boolean` | 是否启用 block 模式(服务端 long-polling) | --- ### stop 停止正在执行的 Instance。停止动作为异步执行。 ```java public void stop() throws OdpsException ``` > 如需确保 Instance 已停止,可在调用后轮询 `isTerminated()` 直至返回 `true`。 --- ### getTaskResults 获取 Instance 中所有 Task 的运行结果。 ```java public Map getTaskResults() throws OdpsException ``` **返回值**:`Map`,key 为 Task 名称,value 为结果字符串 --- ### getTaskResultsWithFormat 获取 Instance 中所有 Task 的运行结果(带格式信息)。 ```java public Map getTaskResultsWithFormat() throws OdpsException ``` **返回值**:`Map`,key 为 Task 名称,value 为 `Result` 对象 --- ### getTaskSummary 获取指定 Task 的运行汇总信息。 ```java public TaskSummary getTaskSummary(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:`TaskSummary` 对象,包含任务运行汇总;服务端返回格式错误时返回 `null` --- ### getTaskStatus 获取 Instance 中所有 Task 的状态。 ```java public Map getTaskStatus() throws OdpsException ``` **返回值**:`Map`,key 为 Task 名称 **TaskStatus.Status 枚举**: - `WAITING` - 等待中 - `RUNNING` - 运行中 - `SUCCESS` - 执行成功 - `FAILED` - 执行失败 - `SUSPENDED` - 被挂起 - `CANCELLED` - 已取消 --- ### waitForTerminatedAndGetResult 等待任务终止并获取结果。 ```java public Instance.Result waitForTerminatedAndGetResult() throws OdpsException ``` **返回值**:`Instance.Result` 对象,包含任务执行结果 --- ### getRawTaskResults 获取原始任务结果列表。 ```java public List getRawTaskResults() throws OdpsException ``` **返回值**:`List`,包含每个 Task 的原始结果信息 --- ### getStartTime 获取 Instance 开始执行时间。 ```java public Date getStartTime() ``` --- ### getEndTime 获取 Instance 结束执行时间。 ```java public Date getEndTime() ``` --- ### getOwner 获取 Instance 所属用户。 ```java public String getOwner() ``` --- ### getProject 获取 Instance 所属项目名称。 ```java public String getProject() ``` --- ## 任务信息 ### getTaskNames 获取所有任务名称集合。 ```java public Set getTaskNames() throws OdpsException ``` **返回值**:`Set`,包含 Instance 中所有 Task 的名称 --- ### getTaskCost 获取指定任务的资源消耗信息。 ```java public TaskCost getTaskCost(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:`TaskCost` 对象,包含任务资源消耗详情 --- ### getTaskInfo 获取任务的指定信息项。 ```java public String getTaskInfo(String taskName, String infoKey) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | | `infoKey` | `String` | 信息项的 key | **返回值**:对应信息项的值字符串 --- ### setInformation 设置任务信息。 ```java public SetInformationResult setInformation(String taskName, String infoKey, String infoValue) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | | `infoKey` | `String` | 信息项的 key | | `infoValue` | `String` | 信息项的 value | **返回值**:`SetInformationResult` 对象,包含设置操作的结果 --- ## 进度与详情 ### getTaskProgress 获取任务各阶段执行进度。 ```java public List getTaskProgress(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:`List`,包含各阶段的进度信息 --- ### getStageProgressFormattedString 将进度格式化为可读字符串。 ```java public static String getStageProgressFormattedString(List stages) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `stages` | `List` | 阶段进度列表 | **返回值**:格式化后的进度字符串 --- ### getTaskDetailJson 获取任务详情 JSON。 ```java public String getTaskDetailJson(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:任务详情的 JSON 字符串 --- ### getTaskQuotaJson 获取任务 Quota 信息 JSON。 ```java public String getTaskQuotaJson(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:任务 Quota 信息的 JSON 字符串 --- ## 实例属性 ### isSync 判断是否同步执行的实例。 ```java public boolean isSync() ``` **返回值**:同步执行返回 `true`,异步执行返回 `false` --- ### getTasks 获取实例中的所有 Task 列表。 ```java public List getTasks() throws OdpsException ``` **返回值**:`List`,包含 Instance 中所有 Task 对象 --- ### getPriority 获取实例优先级。 ```java public int getPriority() throws OdpsException ``` **返回值**:实例优先级数值 --- ### getJobName 获取作业名称。 ```java public String getJobName() throws OdpsException ``` **返回值**:作业名称字符串 --- ### getQueueingInfo 获取实例排队信息。 ```java public InstanceQueueingInfo getQueueingInfo() throws OdpsException ``` **返回值**:`InstanceQueueingInfo` 对象,包含实例排队详情 --- ## MCQA / Select 相关 ### isSelect 判断指定任务是否为 SELECT 查询。 ```java public boolean isSelect(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:是 SELECT 查询返回 `true`,否则返回 `false` --- ### getResultDescriptor 获取 SELECT 结果的描述信息(列名、类型等)。 ```java public ResultDescriptor getResultDescriptor(String taskName) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `taskName` | `String` | Task 名称 | **返回值**:`ResultDescriptor` 对象,包含结果集的列名、数据类型等元信息 --- ## LogView 通过 `Odps.logview()` 生成 Instance 的 LogView 链接,用于在浏览器中查看任务详情。 ```java LogView logView = odps.logview(); String logViewUrl = logView.generateLogView(instance); ``` ### generateLogView ```java public String generateLogView(Instance instance) throws OdpsException public String generateLogView(Instance instance, long hours) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `instance` | `Instance` | 目标 Instance | | `hours` | `long` | Token 有效时长(小时) | **返回值**:LogView URL 字符串 --- # reference/MaxStorageClient.md # MaxStorageClient `MaxStorageClient` 是 Storage API 的客户端入口类,提供创建表读取/写入 Session、预览表数据和 Blob 数据管理功能。客户端是**线程安全**的,建议全局持有单一实例。 ## 获取实例 使用 Builder 模式构建: ```java MaxStorageClient client = MaxStorageClient.builder() .endpoint("https://service.cn-hangzhou.maxcompute.aliyun.com/api") .credentialsProvider(credentialsProvider) .project("my_project") .build(); ``` ### Builder 配置 | 方法 | 参数类型 | 必需 | 说明 | |------|----------|------|------| | `endpoint(String)` | String | 是 | MaxCompute 服务 Endpoint | | `credentialsProvider(ICredentialsProvider)` | ICredentialsProvider | 推荐 | 认证凭证提供者 | | `project(String)` | String | 否 | 默认项目名称 | | `tunnelEndpoint(String)` | String | 否 | Tunnel 端点(不设置自动获取) | | `region(String)` | String | 否 | 地域标识,如 `cn-hangzhou` | | `quota(String)` | String | 否 | 资源配额名称 | | `httpSettings(HttpSettings)` | HttpSettings | 否 | HTTP 连接配置 | | `bufferAllocator(BufferAllocator)` | BufferAllocator | 否 | Arrow 内存分配器 | | `userAgent(String)` | String | 否 | 自定义 User-Agent 标识 | ## 方法列表 ### createTableReadSessionBuilder 创建表读取 Session 的 Builder。 ```java public TableReadSessionBuilder createTableReadSessionBuilder(TableIdentifier table) ``` | 参数 | 类型 | 说明 | |------|------|------| | `table` | TableIdentifier | 目标表标识符 | **返回值**:`TableReadSessionBuilder` 实例 **示例**: ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "name")) .build(); ``` --- ### createTableWriteSessionBuilder 创建表写入 Session 的 Builder。 ```java public TableWriteSessionBuilder createTableWriteSessionBuilder(TableIdentifier table) ``` | 参数 | 类型 | 说明 | |------|------|------| | `table` | TableIdentifier | 目标表标识符 | **返回值**:`TableWriteSessionBuilder` 实例 **示例**: ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableWriteSession session = client.createTableWriteSessionBuilder(tableId) .withPartition(new PartitionSpec("dt='20250101'")) .build(); ``` --- ### createInstanceReadSessionBuilder 创建 Instance 读取 Session 的 Builder,用于读取 SQL 查询结果。 ```java public InstanceReadSessionBuilder createInstanceReadSessionBuilder(InstanceIdentifier instance) ``` | 参数 | 类型 | 说明 | |------|------|------| | `instance` | InstanceIdentifier | Instance 标识符 | **示例**: ```java InstanceIdentifier instanceId = InstanceIdentifier.of("my_project", "instance_id_xxx"); InstanceReadSession session = client.createInstanceReadSessionBuilder(instanceId).build(); ``` --- ### previewTable 快速预览表数据。 ```java public ArrowReader previewTable(TableIdentifier table, PartitionSpec partition, List columns, Integer limit) ``` | 参数 | 类型 | 说明 | |------|------|------| | `table` | TableIdentifier | 目标表标识符 | | `partition` | PartitionSpec | 分区规格,null 读取全部分区 | | `columns` | List\ | 列名列表,null 读取全部列 | | `limit` | Integer | 最大返回行数,null 不限制 | **返回值**:`ArrowReader`(调用方负责关闭) **示例**: ```java try (ArrowReader reader = client.previewTable(tableId, null, null, 100)) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); // 处理数据 } } ``` --- ### openBlobManager 打开 Blob 管理器,用于读写非结构化大对象数据。 ```java public BlobManager openBlobManager() ``` **返回值**:`BlobManager` 实例 --- ### close 关闭客户端,释放网络连接、线程池和内存资源。 ```java public void close() ``` **示例**: ```java try (MaxStorageClient client = MaxStorageClient.builder() .endpoint("...") .credentialsProvider(credentialsProvider) .build()) { // 使用 client } ``` ## TableIdentifier 用于唯一标识一张 MaxCompute 表。 ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); // 三层模型 TableIdentifier tableId = TableIdentifier.of("my_project", "my_schema", "my_table"); ``` --- # reference/Odps.md # Odps `Odps` 是 MaxCompute Java SDK 的入口类,通过它可以访问 Tables、Instances、Projects 等所有资源集合。 ## 获取实例 ```java Account account = new AliyunAccount("access_id", "access_key"); Odps odps = new Odps(account); odps.setEndpoint("http://service.odps.aliyun.com/api"); odps.setDefaultProject("my_project"); ``` 通过已有 Odps 实例拷贝构造: ```java Odps newOdps = new Odps(odps); ``` 通过已有 Odps 实例克隆: ```java Odps newOdps = odps.clone(); ``` ## 构造方法 ### Odps(Account) 通过 Account 创建 Odps 实例。 ```java public Odps(Account account) ``` | 参数 | 类型 | 说明 | |------|------|------| | `account` | Account | 主账号实例 | --- ### Odps(Odps) 从已有 Odps 实例拷贝构造,复制所有配置。 ```java public Odps(Odps odps) ``` | 参数 | 类型 | 说明 | |------|------|------| | `odps` | Odps | 已有的 Odps 实例 | --- ## Account 相关 ### setAccount 设置主账号。 ```java public void setAccount(Account account) ``` | 参数 | 类型 | 说明 | |------|------|------| | `account` | Account | 主账号实例 | --- ### getAccount 获取当前主账号。 ```java public Account getAccount() ``` **返回值**:当前 `Account` 实例 --- ## 方法列表 ### tables 获取 Tables 集合对象,用于操作项目中的表。 ```java public Tables tables() ``` **返回值**:`Tables` 集合对象 **示例**: ```java Table table = odps.tables().get("table_name"); Table table = odps.tables().get("project_name", "table_name"); ``` --- ### instances 获取 Instances 集合对象,用于操作项目中的作业实例。 ```java public Instances instances() ``` **返回值**:`Instances` 集合对象 --- ### projects 获取 Projects 集合对象,用于操作项目。 ```java public Projects projects() ``` **返回值**:`Projects` 集合对象 --- ### schemas 获取 Schemas 集合对象,用于操作三层模型下的 Schema。 ```java public Schemas schemas() ``` **返回值**:`Schemas` 集合对象 --- ### functions 获取 Functions 集合对象,用于操作用户自定义函数。 ```java public Functions functions() ``` **返回值**:`Functions` 集合对象 --- ### resources 获取 Resources 集合对象,用于操作项目中的资源。 ```java public Resources resources() ``` **返回值**:`Resources` 集合对象 --- ### volumes 获取 Volumes 集合对象,用于操作项目中的 Volume。 ```java public Volumes volumes() ``` **返回值**:`Volumes` 集合对象 --- ### quotas 获取 Quotas 集合对象,用于操作 Quota 配额。 ```java public Quotas quotas() ``` **返回值**:`Quotas` 集合对象 --- ### logview 获取 LogView 工具实例,用于生成作业日志链接。 ```java public LogView logview() ``` **返回值**:`LogView` 工具实例 --- ### xFlows 获取 XFlows 集合对象,用于操作 XFlow 任务。 ```java public XFlows xFlows() ``` **返回值**:`XFlows` 集合对象 --- ### tenant 获取 Tenant 实例,用于访问租户级信息。 ```java public Tenant tenant() ``` **返回值**:`Tenant` 实例 --- ### setEndpoint 设置 MaxCompute 服务的 Endpoint 地址。 ```java public void setEndpoint(String endpoint) ``` | 参数 | 类型 | 说明 | |------|------|------| | `endpoint` | String | MaxCompute 服务地址,如 `http://service.odps.aliyun.com/api` | --- ### setDefaultProject 设置默认 Project 名称。SDK 中多数方法都有两个版本:一个需要传入 projectName,另一个使用默认 Project。 ```java public void setDefaultProject(String defaultProject) ``` | 参数 | 类型 | 说明 | |------|------|------| | `defaultProject` | String | 默认 Project 名称 | --- ### setCurrentSchema 设置当前 Schema(三层模型)。 ```java public void setCurrentSchema(String schema) ``` | 参数 | 类型 | 说明 | |------|------|------| | `schema` | String | Schema 名称,null 或空字符串表示使用默认 Schema | --- ### setTunnelEndpoint 设置 Tunnel 服务端点。未设置时将自动路由。 ```java public void setTunnelEndpoint(String tunnelEndpoint) ``` | 参数 | 类型 | 说明 | |------|------|------| | `tunnelEndpoint` | String | Tunnel 服务端点 URL | --- ### getEndpoint 获取 MaxCompute 服务端点地址。 ```java public String getEndpoint() ``` **返回值**:服务端点 URL 字符串 --- ### getTunnelEndpoint 获取 Tunnel 服务端点地址。 ```java public String getTunnelEndpoint() ``` **返回值**:Tunnel 端点 URL 字符串 --- ### getDefaultProject 获取默认项目名称。 ```java public String getDefaultProject() ``` **返回值**:默认 Project 名称 --- ### getCurrentSchema 获取当前 Schema 名称。 ```java public String getCurrentSchema() ``` **返回值**:当前 Schema 名称,未设置时返回 null --- ### setUserAgent 设置 User-Agent 标识,用于追踪 SDK 调用来源。 ```java public void setUserAgent(String userAgent) ``` | 参数 | 类型 | 说明 | |------|------|------| | `userAgent` | String | 自定义 User-Agent 标识 | --- ### getUserAgent 获取当前 User-Agent 标识。 ```java public String getUserAgent() ``` **返回值**:User-Agent 标识字符串 --- ### getLogViewHost 获取 LogView 服务地址。 ```java public String getLogViewHost() ``` **返回值**:LogView 服务 URL 字符串 --- ### setLogViewHost 设置 LogView 服务地址。 ```java public void setLogViewHost(String host) ``` | 参数 | 类型 | 说明 | |------|------|------| | `host` | String | LogView 服务地址 | --- ### options 获取全局配置选项对象。 ```java public OdpsOptions options() ``` **返回值**:`OdpsOptions` 全局配置对象 --- ### tableTunnel 创建 TableTunnel 实例。 ```java public TableTunnel tableTunnel() public TableTunnel tableTunnel(Configuration configuration) ``` **返回值**:`TableTunnel` 实例 **示例**: ```java TableTunnel tunnel = odps.tableTunnel(); ``` --- ### clone 克隆当前 Odps 实例,复制所有配置。 ```java public Odps clone() ``` **返回值**:新的 `Odps` 实例 --- # reference/Partition.md # Partition `Partition` 类代表 MaxCompute 中的表分区,用于查询分区基本信息和扩展信息。 ## 获取实例 通过 Table 对象获取 Partition 实例: ```java Partition partition = table.getPartition(new PartitionSpec("dt='20250101'")); ``` > 获取 Partition 实例是 lazy 操作,只有调用属性方法时才会加载元数据。分区必须真实存在。 ## 数据加载 `Partition` 实现 `LazyLoad`,首次调用属性方法时自动加载元数据。也可手动触发: ```java partition.reload(); ``` `reload()` 后默认使用缓存,不会重复加载。 ## 基本信息方法 ### getPartitionSpec 获取分区规格。 ```java public PartitionSpec getPartitionSpec() ``` **返回值**:`PartitionSpec` 对象 --- ### getCreatedTime 获取分区创建时间。 ```java public Date getCreatedTime() ``` --- ### getLastMetaModifiedTime 获取分区元数据最后修改时间。 ```java public Date getLastMetaModifiedTime() ``` --- ### getSize 获取分区存储大小。 ```java public long getSize() ``` **返回值**:存储大小(字节) > 此值通常不保证与实际占用存储完全一致。 --- ### getRecordNum 获取分区数据行数。 ```java public long getRecordNum() ``` **返回值**:数据行数;无准确数据时返回 `-1` --- ### getLifeCycle 获取分区生命周期。 ```java public long getLifeCycle() ``` **返回值**:生命周期(天) --- ### getLastDataModifiedTime 获取分区数据最后修改时间。 ```java public Date getLastDataModifiedTime() ``` --- ### getLastDataAccessTime 获取分区数据最后访问时间。 ```java public Date getLastDataAccessTime() ``` --- ### getStorageTierInfo 获取分区存储分层信息。 ```java public StorageTierInfo getStorageTierInfo() ``` --- ### isExstore 判断分区是否为 Exstore 存储。 ```java public boolean isExstore() ``` --- ## 扩展信息方法 扩展信息在首次调用时加载,无法通过 `reload()` 刷新(因为这些信息通常随分区创建而确定)。 ### isArchived 查看分区是否进行过归档(archive)操作。 ```java public boolean isArchived() ``` **返回值**:`true` 已归档;`false` 未归档 --- ### getPhysicalSize 获取分区所占磁盘的物理大小。 ```java public long getPhysicalSize() ``` **返回值**:物理大小(字节),为估计值 --- ### getFileNum 获取分区占用文件数。 ```java public long getFileNum() ``` **返回值**:文件数量,为估计值 --- ### getReserved 获取扩展信息的保留字段。 ```java public String getReserved() ``` **返回值**:JSON 字符串 --- ### getClusterInfo 获取 Cluster 信息。 ```java public ClusterInfo getClusterInfo() ``` --- ### getCdcSize 获取 CDC 数据大小。 ```java public long getCdcSize() ``` --- ### getCdcRecordNum 获取 CDC 数据行数。 ```java public long getCdcRecordNum() ``` --- ## Tag 操作 ### getTags 获取分区标签列表。 ```java public List getTags() ``` --- ### getTags (指定列) 获取分区指定列的标签列表。 ```java public List getTags(String columnName) ``` | 参数 | 类型 | 说明 | |------|------|------| | `columnName` | String | 列名 | --- ### getSimpleTags 获取分区简单标签。 ```java public Map> getSimpleTags() ``` --- ### getSimpleTags (指定列) 获取分区指定列的简单标签。 ```java public Map> getSimpleTags(String columnName) ``` | 参数 | 类型 | 说明 | |------|------|------| | `columnName` | String | 列名 | --- ### addTag 为分区添加标签。 ```java public void addTag(Tag tag) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `tag` | Tag | 标签对象 | --- ### addTag (指定列) 为分区指定列添加标签。 ```java public void addTag(Tag tag, List columnNames) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `tag` | Tag | 标签对象 | | `columnNames` | List\ | 列名列表 | --- ### addSimpleTag 为分区添加简单标签。 ```java public void addSimpleTag(String category, String key, String value) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `category` | String | 标签分类 | | `key` | String | 标签键 | | `value` | String | 标签值 | --- ### addSimpleTag (指定列) 为分区指定列添加简单标签。 ```java public void addSimpleTag(String category, String key, String value, List columnNames) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `category` | String | 标签分类 | | `key` | String | 标签键 | | `value` | String | 标签值 | | `columnNames` | List\ | 列名列表 | --- ### removeTag 删除分区标签。 ```java public void removeTag(Tag tag) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `tag` | Tag | 标签对象 | --- ### removeTag (指定列) 删除分区指定列的标签。 ```java public void removeTag(Tag tag, List columnNames) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `tag` | Tag | 标签对象 | | `columnNames` | List\ | 列名列表 | --- ### removeSimpleTag 删除分区简单标签。 ```java public void removeSimpleTag(String category, String key, String value) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `category` | String | 标签分类 | | `key` | String | 标签键 | | `value` | String | 标签值 | --- ### removeSimpleTag (指定列) 删除分区指定列的简单标签。 ```java public void removeSimpleTag(String category, String key, String value, List columnNames) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `category` | String | 标签分类 | | `key` | String | 标签键 | | `value` | String | 标签值 | | `columnNames` | List\ | 列名列表 | --- ## State 管理 ### setState 设置分区状态(冻结/恢复)。 ```java public void setState(State state) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `state` | State | 分区状态 | --- ## 使用示例 ```java Table table = odps.tables().get("my_table"); Partition partition = table.getPartition(new PartitionSpec("dt='20250101'")); // 基本信息 System.out.println("创建时间: " + partition.getCreatedTime()); System.out.println("存储大小: " + partition.getSize() + " bytes"); System.out.println("数据行数: " + partition.getRecordNum()); // 扩展信息 System.out.println("物理大小: " + partition.getPhysicalSize()); System.out.println("文件数: " + partition.getFileNum()); System.out.println("已归档: " + partition.isArchived()); ``` --- # reference/Project.md # Project 项目管理包含两个核心类:`Projects`(项目集合管理器)和 `Project`(项目实体)。`Projects` 提供项目级的增删改查操作,`Project` 封装单个项目的属性和行为。 ## 获取实例 ```java // 获取 Projects 集合 Projects projects = odps.projects(); // 获取当前默认项目 Project project = projects.get(); // 获取指定项目 Project project = projects.get("project_name"); ``` > `Project` 实现了 `LazyLoad` 接口,调用 `get()` 不会加载完整属性。首次访问属性时自动加载。 ## Projects 方法 ### get ```java public Project get() throws OdpsException public Project get(String projectName) throws OdpsException ``` **返回值**:`Project` 对象(延迟加载) --- ### exists ```java public boolean exists(String projectName) throws OdpsException ``` **返回值**:项目存在返回 `true` --- ### create ```java public void create(CreateProjectParam param) throws OdpsException ``` **CreateProjectParam Builder 方法**: | 方法 | 说明 | 必填 | |------|------|------| | `name(String)` | 项目名称(2-64字符) | 是 | | `owner(String)` | 项目所有者 | 是 | | `defaultCluster(String)` | 默认计算集群 | 是 | | `comment(String)` | 项目描述 | 否 | | `superAdmin(String)` | 超级管理员 | 否 | | `properties(Map)` | 项目属性(覆盖设置) | 否 | | `appendProperty(String, String)` | 追加属性 | 否 | | `groupName(String)` | 项目组名称 | 否 | | `defaultQuotaId(String)` | 默认计算配额 ID | 否 | --- ### createExternalProject 创建外部项目(联邦查询)。 ```java public void createExternalProject(String projectName, String comment, String refProjectName, Project.ExternalProjectProperties extProperties) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `projectName` | `String` | 外部项目名称 | | `comment` | `String` | 项目描述 | | `refProjectName` | `String` | 关联的内部项目名称 | | `extProperties` | `ExternalProjectProperties` | 外部数据源配置 | --- ### updateProject 更新项目属性、状态、所有者等信息。 ```java public void updateProject(String projectName, Map properties) throws OdpsException public void updateProject(String projectName, Project.Status status, String owner, String comment, Map properties, List clusters) throws OdpsException public void updateProject(String projectName, Project.Status status, String owner, String comment, Map properties, List clusters, QuotaIdentifier defaultQuota) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `projectName` | `String` | 项目名称 | | `properties` | `Map` | 项目属性键值对 | | `status` | `Project.Status` | 项目状态:`AVAILABLE`、`FROZEN`、`DELETING` | | `owner` | `String` | 项目所有者账号 | | `comment` | `String` | 项目描述 | | `clusters` | `List` | 计算集群列表 | | `defaultQuota` | `QuotaIdentifier` | 默认计算配额 | **示例**: ```java // 更新项目属性 Map props = new HashMap<>(); props.put("odps.security.ip.whitelist", "10.0.0.0/8"); odps.projects().updateProject("my_project", props); // 冻结项目 odps.projects().updateProject("my_project", Project.Status.FROZEN, null, null, null, null); ``` --- ### delete ```java public Instance delete(String projectName, boolean isImmediate) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `projectName` | `String` | 项目名称 | | `isImmediate` | `boolean` | `true` 物理删除(不可逆);`false` 逻辑删除(可恢复) | **返回值**:物理删除时返回操作 Instance;逻辑删除返回 `null` --- ### iterator / iterable ```java public Iterator iterator(String owner) public Iterable iterable(String owner) public Iterator iteratorByFilter(ProjectFilter filter) ``` **ProjectFilter 属性**: | 属性 | 说明 | |------|------| | `owner` | 项目所有者(精确匹配) | | `name` | 项目名称前缀匹配 | | `user` | 项目使用者 | | `groupName` | 项目组名称 | | `tenantId` | 租户 ID | | `regionId` | 地域 ID | | `quotaNickname` | 配额别名 | --- ## Project 属性方法 ### 基础属性 | 方法 | 返回类型 | 说明 | |------|----------|------| | `getName()` | `String` | 项目名称 | | `getComment()` | `String` | 项目描述 | | `getOwner()` | `String` | 项目所有者 | | `getRegionId()` | `String` | 所属地域(如 `cn-shanghai`) | | `getCreatedTime()` | `Date` | 创建时间 | | `getLastModifiedTime()` | `Date` | 最后修改时间 | --- ### getType ```java public ProjectType getType() ``` **返回值**: | 枚举值 | 说明 | |--------|------| | `MANAGED` | 原生存储项目(默认) | | `EXTERNAL` | 旧版外部存储(兼容) | | `EXTERNAL_V2` | 新版外部存储(推荐) | --- ### getStatus ```java public Status getStatus() ``` **返回值**: | 枚举值 | 说明 | |--------|------| | `AVAILABLE` | 正常可用 | | `READONLY` | 只读模式 | | `DELETING` | 删除中 | | `FROZEN` | 被冻结 | | `UNKOWN` | 状态未知(需 `reload()`) | --- ### getProperties 获取项目显式配置的属性集合(不含继承配置)。 ```java public Map getProperties() ``` --- ### getAllProperties 获取全部配置属性(含从项目组继承的属性)。 ```java public Map getAllProperties() ``` --- ### getProperty 获取指定配置项。 ```java public String getProperty(String key) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `key` | `String` | 配置项名称,如 `"odps.sql.type.system.odps2"` | --- ### getExtendedProperties 获取项目扩展属性。 ```java public Map getExtendedProperties() ``` --- ### getStorageTierInfo 获取分层存储详情。需先调用 `reload()` 刷新数据。 ```java public StorageTierInfo getStorageTierInfo() ``` **StorageTierInfo 结构**: | 字段 | 类型 | 说明 | |------|------|------| | `storageTier` | `String` | 存储策略(STANDARD/LOWFREQUENCY/LONGTERM) | | `storageLastModifiedTime` | `Date` | 最后统计时间 | | `storageSize` | `Map` | 各层级存储量(字节) | --- ### getSecurityManager 获取项目安全管理器。 ```java public SecurityManager getSecurityManager() ``` --- ### getDefaultQuotaNickname 获取项目默认 Quota 的昵称。 ```java public String getDefaultQuotaNickname() ``` **返回值**:`String`,默认 Quota 昵称 --- ### getDefaultQuotaRegion 获取项目默认 Quota 的 Region。 ```java public String getDefaultQuotaRegion() ``` **返回值**:`String`,默认 Quota 所属 Region --- ### getTunnelEndpoint 获取 Tunnel 接入点。 ```java public String getTunnelEndpoint(String quotaName) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `quotaName` | `String` | 计算配额名称,`null` 使用默认配额 | --- ### getTunnelEndpoint(无参) 获取项目的 Tunnel 端点地址。 ```java public String getTunnelEndpoint() throws OdpsException ``` **返回值**:`String`,Tunnel 端点地址 --- ### getTenantId 获取项目所属的租户 ID。 ```java public String getTenantId() ``` **返回值**:`String`,租户 ID --- ### isExternalCatalogBound 判断项目是否绑定了外部 Catalog。 ```java public boolean isExternalCatalogBound() ``` **返回值**:绑定了外部 Catalog 返回 `true` --- # reference/Resources.md # Resources `Resources` 类用于管理 MaxCompute 项目中的资源文件。资源文件是 UDF 函数执行时依赖的文件,包括 JAR 包、Python 脚本、数据文件等。 ## 获取实例 ```java Resources resources = odps.resources(); ``` ## 资源类型 | 类 | 说明 | |------|------| | `FileResource` | 文件资源(JAR、PY、文本等) | | `TableResource` | 表资源 | | `VolumeResource` | 卷资源 | ## 方法 ### create 创建资源。 ```java // 创建文件资源 public void create(Resource resource, InputStream inputStream) throws OdpsException public void create(String projectName, Resource resource, InputStream inputStream) throws OdpsException public void create(String projectName, String schemaName, Resource resource, InputStream inputStream) throws OdpsException // 创建表资源/卷资源(无需 InputStream) public void create(Resource resource) throws OdpsException public void create(String projectName, Resource resource) throws OdpsException public void create(String projectName, String schemaName, Resource resource) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `resource` | `Resource` | 资源对象(FileResource / TableResource / VolumeResource) | | `inputStream` | `InputStream` | 文件资源的数据流 | | `projectName` | `String` | 目标项目名称(可选) | | `schemaName` | `String` | 目标 Schema 名称(可选) | **示例**: ```java FileResource resource = new FileResource(); resource.setName("my_udf.jar"); InputStream is = new FileInputStream("path/to/my_udf.jar"); resources.create(resource, is); ``` --- ### get 获取资源对象。 ```java public Resource get(String resourceName) public Resource get(String projectName, String resourceName) public Resource get(String projectName, String schemaName, String resourceName) ``` **返回值**:`Resource` 对象 --- ### getResourceAsStream 获取资源文件的输入流。 ```java public InputStream getResourceAsStream(String resourceName) throws OdpsException public InputStream getResourceAsStream(String projectName, String resourceName) throws OdpsException public InputStream getResourceAsStream(String projectName, String schemaName, String resourceName) throws OdpsException ``` **返回值**:`InputStream`,资源文件的数据流 --- ### exists 检查资源是否存在。 ```java public boolean exists(String resourceName) throws OdpsException public boolean exists(String projectName, String resourceName) throws OdpsException public boolean exists(String projectName, String schemaName, String resourceName) throws OdpsException ``` **返回值**:存在返回 `true` --- ### update 更新资源。 ```java // 更新文件资源 public void update(Resource resource, InputStream inputStream) throws OdpsException public void update(String projectName, Resource resource, InputStream inputStream) throws OdpsException public void update(String projectName, String schemaName, Resource resource, InputStream inputStream) throws OdpsException // 更新表资源/卷资源 public void update(Resource resource) throws OdpsException public void update(String projectName, Resource resource) throws OdpsException public void update(String projectName, String schemaName, Resource resource) throws OdpsException ``` --- ### delete 删除资源。 ```java public void delete(String resourceName) throws OdpsException public void delete(String projectName, String resourceName) throws OdpsException public void delete(String projectName, String schemaName, String resourceName) throws OdpsException ``` --- ### iterator 获取资源迭代器。 ```java public Iterator iterator() public Iterator iterator(String projectName) public Iterator iterator(String projectName, String schemaName) ``` --- ### iterable 获取资源 Iterable(支持 foreach 语法)。 ```java public Iterable iterable() public Iterable iterable(String projectName) public Iterable iterable(String projectName, String schemaName) ``` --- ### createTempResource 创建临时资源文件。 ```java public FileResource createTempResource(String fileName) throws OdpsException public FileResource createTempResource(String projectName, String fileName) throws OdpsException public FileResource createTempResource(String projectName, String filePath, Type type) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `fileName` | `String` | 临时资源文件名 | | `projectName` | `String` | 目标项目名称(可选) | | `filePath` | `String` | 资源文件路径 | | `type` | `Type` | 资源类型(FILE、JAR、PY、ARCHIVE 等) | **返回值**:`FileResource`,创建的临时资源对象 --- ## Resource 对象属性 | 方法 | 返回类型 | 说明 | |------|----------|------| | `getName()` | `String` | 资源名称 | | `getType()` | `Resource.Type` | 资源类型(FILE、JAR、PY、ARCHIVE、TABLE、VOLUMEFILE) | | `getOwner()` | `String` | 资源所有者 | | `getCreatedTime()` | `Date` | 创建时间 | | `getLastModifiedTime()` | `Date` | 最后修改时间 | | `getSize()` | `long` | 资源大小(字节) | --- ## 使用示例 ```java Resources resources = odps.resources(); // 创建 JAR 资源 FileResource jar = new FileResource(); jar.setName("my_lib.jar"); resources.create(jar, new FileInputStream("my_lib.jar")); // 创建表资源 TableResource tableRes = new TableResource(); tableRes.setName("my_table_res"); tableRes.setProject("my_project"); tableRes.setTable("source_table"); resources.create(tableRes); // 遍历所有资源 for (Resource r : resources.iterable()) { System.out.println(r.getName() + " [" + r.getType() + "]"); } // 删除资源 resources.delete("my_lib.jar"); ``` --- # reference/SQLExecutor.md # SQLExecutor `SQLExecutor` 是 MaxCompute SQL 执行的统一接口,支持离线作业、MCQA 和 MaxQA 三种执行模式。 ## 获取实例 通过 `SQLExecutorBuilder` 构建: ```java SQLExecutor executor = SQLExecutorBuilder.builder() .odps(odps) .executeMode(ExecuteMode.INTERACTIVE_V2) .quotaName("my_quota") .build(); ``` ### SQLExecutorBuilder 配置 | 方法 | 参数类型 | 必需 | 说明 | |------|----------|------|------| | `odps(Odps)` | Odps | 是 | ODPS 对象 | | `executeMode(ExecuteMode)` | ExecuteMode | 否 | `INTERACTIVE_V2`(MaxQA) / `INTERACTIVE`(MCQA) / `OFFLINE` | | `quotaName(String)` | String | MaxQA必填 | 计算资源组名称 | | `taskName(String)` | String | 否 | 任务名称 | | `tunnelEndpoint(String)` | String | 否 | Tunnel 服务端点 | | `enableCommandApi(boolean)` | boolean | 否 | 启用扩展命令支持 | | `enableOdpsNamespaceSchema(boolean)` | boolean | 否 | 启用三层模型 | | `useInstanceTunnel(boolean)` | boolean | 否 | 是否使用 InstanceTunnel 获取结果(默认 true) | | `tunnelSocketTimeout(int)` | int | 否 | Tunnel 链接超时(毫秒) | | `tunnelReadTimeout(int)` | int | 否 | Tunnel 读取超时(毫秒) | | `tunnelGetResultMaxRetryTime(int)` | int | 否 | 获取结果最大重试次数 | | `recoverFrom(Instance)` | Instance | 否 | 从指定实例恢复 | | `offlineJobPriority(Integer)` | Integer | 否 | 离线作业优先级 | | `fallbackPolicy(FallbackPolicy)` | FallbackPolicy | 否 | MCQA 失败回退策略 | | `enableReattach(boolean)` | boolean | 否 | MCQA 重连 Session | | `serviceName(String)` | String | 否 | MCQA Session 名称 | | `enableMaxQA(boolean)` | boolean | 否 | 启用 MaxQA | | `properties(Map)` | Map\ | 否 | 设置额外属性 | | `runningCluster(String)` | String | 否 | 指定运行集群 | | `regionId(String)` | String | 否 | 设置 Region ID | | `builder()` | — | — | 静态方法,创建 Builder 实例 | | `build()` | — | — | 构建 SQLExecutor 实例 | ## 方法列表 ### run 执行 SQL 查询。 ```java void run(String sql, Map hint) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `sql` | String | SQL 语句 | | `hint` | Map\ | 查询提示参数 | **示例**: ```java executor.run("SELECT * FROM my_table LIMIT 100", new HashMap<>()); ``` --- ### getResult 获取查询结果列表(全部加载到内存)。 ```java List getResult() throws OdpsException, IOException List getResult(Long countLimit) throws OdpsException, IOException List getResult(Long offset, Long countLimit, Long sizeLimit) throws OdpsException, IOException List getResult(Long offset, Long countLimit, Long sizeLimit, boolean limitEnabled) throws OdpsException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `offset` | Long | 偏移量,默认 0 | | `countLimit` | Long | 数量限制 | | `sizeLimit` | Long | 大小限制 | | `limitEnabled` | boolean | false 时限制 1W 条;true 拉取全部(需更高权限) | --- ### getResultSet 获取结果迭代器(分批加载,减少内存占用)。 ```java ResultSet getResultSet() throws OdpsException, IOException ResultSet getResultSet(Long countLimit) throws OdpsException, IOException ResultSet getResultSet(Long offset, Long countLimit, Long sizeLimit) throws OdpsException, IOException ResultSet getResultSet(Long offset, Long countLimit, Long sizeLimit, boolean limitEnabled) throws OdpsException, IOException ``` 参数同 `getResult`。`ResultSet` 实现了 `Iterator` 和 `Iterable`。 --- ### cancel 取消当前查询。 ```java void cancel() throws OdpsException ``` --- ### getQueryId 获取当前查询 ID。MCQA 返回 `instanceId_subqueryId`,其他返回 instanceId。 ```java String getQueryId() ``` **返回值**:查询 ID,未初始化时返回 null --- ### getLogView 获取当前查询的 Logview URL(有效期 7 天)。 ```java String getLogView() ``` --- ### getExecutionLog 获取当前查询的执行日志。 ```java List getExecutionLog() ``` --- ### isActive 检查 Executor 是否活跃。离线和 MaxQA 模式始终返回 false,MCQA 返回 Session 状态。 ```java boolean isActive() ``` --- ### getProgress 获取查询进度信息。 ```java List getProgress() throws OdpsException ``` --- ### getSummary 获取查询概要信息。 ```java String getSummary() throws OdpsException ``` --- ### getInstance 获取当前查询的 Instance 对象。MCQA 返回 Session 实例,其他返回 SQLTask 实例。 ```java Instance getInstance() ``` --- ### getId 获取 Executor 的唯一标识(UUID)。 ```java String getId() ``` --- ### getTaskName 获取当前任务名称。 ```java String getTaskName() ``` --- ### getSubqueryId 获取当前子查询 ID。 ```java int getSubqueryId() ``` --- ### getResult (带行数和大小限制) 获取结果(指定行数和大小限制)。 ```java List getResult(Long countLimit, Long sizeLimit) throws OdpsException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `countLimit` | Long | 行数限制 | | `sizeLimit` | Long | 大小限制 | --- ### getResultSet (带行数和大小限制) 获取 ResultSet(指定行数和大小限制)。 ```java ResultSet getResultSet(Long countLimit, Long sizeLimit) throws OdpsException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `countLimit` | Long | 行数限制 | | `sizeLimit` | Long | 大小限制 | --- ### isRunningInInteractiveMode 判断是否运行在交互式模式。 ```java boolean isRunningInInteractiveMode() ``` --- ### getExecuteMode 获取当前执行模式。 ```java ExecuteMode getExecuteMode() ``` --- ### close 关闭 Executor。连接池模式下归还至池中。 ```java void close() ``` --- # reference/Schemas.md # Schemas `Schemas` 类用于管理 MaxCompute 项目中的 Schema。Schema 是命名空间概念,用于组织和管理表、函数等对象。 ## 获取实例 ```java Schemas schemas = odps.schemas(); ``` ## 方法 ### create 创建 Schema。 ```java public void create(String schemaName) throws OdpsException public void create(String projectName, String schemaName) throws OdpsException public void create(String projectName, String schemaName, String comment, boolean ifNotExists) throws OdpsException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `schemaName` | `String` | Schema 名称 | | `projectName` | `String` | 目标项目名称(可选) | | `comment` | `String` | Schema 描述(可选) | | `ifNotExists` | `boolean` | `true` 已存在时不报错;`false` 已存在时抛异常 | --- ### get 获取 Schema 对象。 ```java public Schema get() public Schema get(String schemaName) public Schema get(String projectName, String schemaName) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `schemaName` | `String` | Schema 名称;无参版本返回默认 Schema | | `projectName` | `String` | 项目名称(可选) | **返回值**:`Schema` 对象 --- ### exists 检查 Schema 是否存在。 ```java public boolean exists(String schemaName) throws OdpsException public boolean exists(String projectName, String schemaName) throws OdpsException ``` **返回值**:存在返回 `true` --- ### delete 删除 Schema。 ```java public void delete(String schemaName) throws OdpsException public void delete(String projectName, String schemaName) throws OdpsException ``` --- ### iterator 获取 Schema 迭代器。 ```java public Iterator iterator() public Iterator iterator(String projectName) public Iterator iterator(String projectName, SchemaFilter filter) ``` --- ### iterable 获取 Schema Iterable(支持 foreach 语法)。 ```java public Iterable iterable() public Iterable iterable(String projectName) ``` --- ## SchemaFilter | 方法 | 说明 | |------|------| | `setOwner(String)` | 按所有者过滤 | | `setName(String)` | 按名称前缀过滤 | **示例**: ```java SchemaFilter filter = new SchemaFilter(); filter.setOwner("user_id"); Iterator iterator = schemas.iterator("project_name", filter); ``` --- ## Schema 对象属性 | 方法 | 返回类型 | 说明 | |------|----------|------| | `getName()` | `String` | Schema 名称 | | `getOwner()` | `String` | 所有者 | | `getComment()` | `String` | 描述信息 | | `getCreateTime()` | `Date` | 创建时间 | | `getModifiedTime()` | `Date` | 最后修改时间 | | `getProjectName()` | `String` | Schema 所属的项目名称 | | `getType()` | `String` | Schema 类型 | --- ## 使用示例 ```java Schemas schemas = odps.schemas(); // 创建 Schema schemas.create("my_project", "my_schema", "业务数据Schema", true); // 检查是否存在 boolean exists = schemas.exists("my_schema"); // 遍历所有 Schema for (Schema schema : schemas.iterable()) { System.out.println(schema.getName()); } // 删除 Schema schemas.delete("my_schema"); ``` --- # reference/StreamUploadSession.md # StreamUploadSession `StreamUploadSession` 专为流式数据上传设计,支持写入即可见(auto-commit)特性,无需手动提交会话。 ## 获取实例 通过 `TableTunnel.buildStreamUploadSession` 构建: ```java StreamUploadSession session = tunnel.buildStreamUploadSession("my_project", "my_table") .setPartitionSpec("dt=20231001/city=shanghai") .build(); ``` ### Builder 配置 | 方法 | 参数类型 | 默认值 | 说明 | |------|----------|--------|------| | `setProjectName(String)` | String | - | 项目名称(必填) | | `setTableName(String)` | String | - | 表名称(必填) | | `setSchemaName(String)` | String | null | Schema 名称(三层模型) | | `setPartitionSpec(String)` | String | null | 分区表达式 | | `setSlotNum(long)` | long | 0 | 并行槽位数(0 自动分配) | | `setCreatePartition(boolean)` | boolean | false | 自动创建不存在的分区 | | `setSchemaVersion(String)` | String | 最新版本 | 指定 Schema 版本号 | | `allowSchemaMismatch(boolean)` | boolean | true | 是否允许字段类型不匹配 | | `setDynamicPartition(boolean)` | boolean | false | 设置是否启用动态分区写入 | ## 方法列表 ### getId 获取会话 ID。 ```java public String getId() ``` --- ### getSchema 获取表结构信息。 ```java public TableSchema getSchema() ``` --- ### getSchemaVersion 获取表 Schema 版本号。 ```java public String getSchemaVersion() ``` --- ### getQuotaName 获取本次流式上传使用的 Quota 名称。 ```java public String getQuotaName() ``` --- ### getLastBatchId 获取最后一个已提交的 batch ID。 ```java public long getLastBatchId() throws TunnelException ``` --- ### getLastBatchCommitTime 获取最后一次 batch 提交时间(毫秒时间戳)。 ```java public long getLastBatchCommitTime() throws TunnelException ``` --- ### newRecordPack 创建流式数据包。 ```java public StreamRecordPack newRecordPack() throws IOException public StreamRecordPack newRecordPack(CompressOption option) throws IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `option` | CompressOption | 压缩选项,默认 ODPS_ZLIB | **压缩算法可选值**:`ODPS_RAW`、`ODPS_ZLIB`、`ODPS_SNAPPY`、`ODPS_LZ4_FRAME` --- ### newRecord 创建空记录对象。 ```java public Record newRecord() ``` --- ### StreamRecordPack 接口 #### append 向数据包中追加一条记录。 ```java public void append(Record record) throws IOException ``` --- #### getRecordCount 获取当前包中的记录数。 ```java public long getRecordCount() ``` --- #### getDataSize 获取当前数据大小(由于多层缓冲区,值变化可能不连续)。 ```java public long getDataSize() ``` --- #### flush 将数据发送到服务端。flush 成功后 pack 可复用。 ```java // 基础提交 public String flush() throws IOException // 带选项提交 public FlushResult flush(FlushOption flushOption) throws IOException ``` **FlushOption 配置**: ```java FlushOption option = new FlushOption().timeout(3000); // 3秒超时 ``` **FlushResult 结构**: | 字段 | 类型 | 说明 | |------|------|------| | `traceId` | String | 请求追踪 ID | | `recordCount` | long | 本次 flush 的记录数 | | `flushSize` | long | 本次 flush 的数据大小 | | `batchId` | long | 批次 ID | --- #### reset 重置缓冲区(flush 成功后自动调用)。 ```java public void reset() throws IOException ``` --- ### 使用示例 ```java StreamUploadSession session = tunnel.buildStreamUploadSession("my_project", "my_table") .setCreatePartition(true) .setPartitionSpec("dt=20231001") .build(); StreamRecordPack pack = session.newRecordPack(); while (hasMoreData()) { Record record = session.newRecord(); record.set("id", 1001); record.set("action", "purchase"); pack.append(record); if (pack.getDataSize() > 64 * 1024 * 1024) { // 64MB 触发提交 pack.flush(); } } // 最后一批 if (pack.getRecordCount() > 0) { pack.flush(); } ``` **注意事项**: - 单个 StreamUploadSession 支持创建多个 RecordPack,但需避免跨线程并发操作同一个 pack - 长时间未 flush 的数据包会持续占用内存 - 修改表结构后需重新创建会话以获取最新 Schema --- # reference/Table.md # Table `Table` 类代表 MaxCompute 中的表,提供表元数据查询、数据读取、DDL 操作、分区管理和标签管理功能。 ## 获取实例 ```java Table table = odps.tables().get("table_name"); Table table = odps.tables().get("project_name", "table_name"); // 三层模型 Table table = odps.tables().get("project_name", "schema_name", "table_name"); ``` 获取表实例为 lazy 操作,只有调用其他方法时才会请求元数据。可手动触发加载: ```java table.reload(); ``` ## Tables 集合操作 以下方法属于 `Tables` 类,通过 `odps.tables()` 访问。 ### get 获取表对象(延迟加载,不立即请求服务端)。 ```java public Table get(String tableName) public Table get(String projectName, String tableName) public Table get(String projectName, String schemaName, String tableName) ``` --- ### exists 判断表是否存在。 ```java public boolean exists(String tableName) throws OdpsException public boolean exists(String projectName, String tableName) throws OdpsException public boolean exists(String projectName, String schemaName, String tableName) throws OdpsException ``` --- ### create 创建表。推荐使用 `newTableCreator` 的 Builder 模式。 ```java public void create(String tableName, TableSchema schema) throws OdpsException public void create(String tableName, TableSchema schema, boolean ifNotExists) throws OdpsException public void create(String projectName, String tableName, TableSchema schema, boolean ifNotExists) throws OdpsException ``` --- ### newTableCreator 使用 Builder 模式创建表,支持分区表、Delta 表、外部表等高级建表选项。 ```java public TableCreator newTableCreator(String tableName, TableSchema schema) public TableCreator newTableCreator(String projectName, String tableName, TableSchema schema) ``` TableCreator 链式方法: | 方法 | 说明 | |------|------| | `withSchemaName(String)` | 指定 Schema | | `withComment(String)` | 表注释 | | `withLifecycle(long)` | 生命周期(天) | | `ifNotExists()` | 表存在时不报错 | | `withPartitionDef(TableSchema)` | 分区定义 | | `withPrimaryKeys(List)` | 主键列(Delta 表) | | `transactionTable()` | 标记为事务表 | | `withStorageHandler(String)` | 外部表 StorageHandler | | `withLocation(String)` | 外部表数据位置 | | `withSerDeProperties(Map)` | 序列化属性 | | `withJars(List)` | 关联 JAR 资源 | | `withClusterInfo(ClusterInfo)` | 聚簇信息 | | `create()` | 执行建表 | 示例: ```java odps.tables().newTableCreator("my_table", schema) .withComment("demo table") .withLifecycle(30) .ifNotExists() .create(); ``` --- ### createExternal 创建外部表。 ```java public void createExternal(String projectName, String tableName, TableSchema schema, String location, String storageHandler, Map serDeProperties, List jars, String comment) throws OdpsException ``` --- ### delete 删除表。 ```java public void delete(String tableName) throws OdpsException public void delete(String tableName, boolean ifExists) throws OdpsException public void delete(String projectName, String tableName) throws OdpsException public void delete(String projectName, String schemaName, String tableName) throws OdpsException public void delete(String projectName, String schemaName, String tableName, boolean ifExists) throws OdpsException ``` --- ### iterator / iterable 遍历项目中的表。 ```java public Iterator
iterator() public Iterator
iterator(String projectName) public Iterator
iterator(TableFilter filter) public Iterator
iterator(String projectName, TableFilter filter) public Iterator
iterator(String projectName, String schemaName, TableFilter filter) public Iterable
iterable() public Iterable
iterable(String projectName) public Iterable
iterable(TableFilter filter) public Iterable
iterable(String projectName, TableFilter filter) public Iterable
iterable(String projectName, String schemaName, TableFilter filter) ``` TableFilter 属性: | 属性 | 说明 | |------|------| | `name` | 表名前缀匹配 | | `owner` | 表所有者 | | `type` | 表类型筛选 | 示例: ```java TableFilter filter = new TableFilter(); filter.setName("user_"); for (Table t : odps.tables().iterable(filter)) { System.out.println(t.getName()); } ``` --- ### loadTables 批量加载表元数据(一次 API 请求获取多张表的信息)。 ```java public List
loadTables(Collection tableNames) throws OdpsException public List
loadTables(String projectName, Collection tableNames) throws OdpsException public List
loadTables(String projectName, String schemaName, Collection tableNames) throws OdpsException ``` --- ### reloadTables 批量刷新已有表对象的元数据。 ```java public List
reloadTables(Collection
tables) throws OdpsException public List
reloadTables(Iterator
tables) throws OdpsException ``` ## 基本信息 ### getName ```java public String getName() ``` **返回值**:表名称 --- ### getSchema ```java public TableSchema getSchema() ``` **返回值**:`TableSchema` 对象,包含列定义和分区列定义 ```java tableSchema.getColumns(); // data 列 tableSchema.getPartitionColumns(); // 分区列 ``` --- ### getJsonSchema ```java public String getJsonSchema() ``` **返回值**:JSON 格式的表结构描述 --- ### getProject ```java public String getProject() ``` **返回值**:表所在项目名 --- ### getSchemaName ```java public String getSchemaName() ``` **返回值**:表所在 Schema 名(需开启三层模型) --- ### getComment ```java public String getComment() ``` **返回值**:表注释 --- ### getOwner ```java public String getOwner() ``` **返回值**:表所属用户 --- ### getType ```java public Table.TableType getType() ``` **返回值**:`TableType` 枚举,可选值:`MANAGED_TABLE`、`VIRTUAL_VIEW`、`EXTERNAL_TABLE`、`MATERIALIZED_VIEW` 衍生判断方法: ```java table.isVirtualView(); table.isMaterializedView(); table.isExternalTable(); ``` --- ### getCreatedTime ```java public Date getCreatedTime() ``` --- ### getLastMetaModifiedTime ```java public Date getLastMetaModifiedTime() ``` --- ### getSize ```java public long getSize() ``` **返回值**:表存储大小(bytes),通常为估计值 --- ### getRecordNum ```java public long getRecordNum() ``` **返回值**:表数据行数,无准确数据时返回 -1 --- ### getLife ```java public long getLife() ``` **返回值**:表生命周期(天) --- ### getTableID 获取表的唯一标识符。 ```java public String getTableID() ``` **返回值**:表的唯一 ID --- ### getTableLabel 获取表的安全标签等级。 ```java public String getTableLabel() ``` **返回值**:表的安全标签等级 --- ### getTableExtendedLabels 获取表的扩展标签列表。 ```java public List getTableExtendedLabels() ``` **返回值**:扩展标签列表 --- ### getMaxLabel 获取表和列的最高安全标签等级。 ```java public String getMaxLabel() ``` **返回值**:最高安全标签等级 --- ### getCryptoAlgoName 获取表的加密算法名称。 ```java public String getCryptoAlgoName() ``` **返回值**:加密算法名称 --- ### getLastDataModifiedTime 获取数据最后修改时间。 ```java public Date getLastDataModifiedTime() ``` **返回值**:数据最后修改时间 --- ### getLastDataAccessTime 获取数据最后访问时间。 ```java public Date getLastDataAccessTime() ``` **返回值**:数据最后访问时间 --- ### getHubLifecycle 获取 DataHub 生命周期(天)。 ```java public long getHubLifecycle() ``` **返回值**:DataHub 生命周期天数 --- ### isPartitioned 判断是否为分区表。 ```java public boolean isPartitioned() ``` **返回值**:`true` 表示为分区表 ## 数据操作 ### read ```java public RecordReader read(int limit) throws OdpsException public RecordReader read(PartitionSpec partition, List columns, int limit) throws OdpsException public RecordReader read(PartitionSpec partition, List columns, int limit, String timezone) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `limit` | int | 最多读取行数,最大 10000 | | `partition` | PartitionSpec | 分区表达式,null 表示全表 | | `columns` | List\ | 列名列表,null 表示全部列 | | `timezone` | String | datetime 时区,如 `"Asia/Shanghai"` | **注意**:最多返回 1W 行,数据不超过 10MB。大量数据请使用 Tunnel。 --- ### truncate 清空表数据。 ```java public void truncate() throws OdpsException ``` ## DDL ### setLifeCycle ```java public void setLifeCycle(int days) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `days` | int | 生命周期天数,正整数 | --- ### changeOwner ```java public void changeOwner(String newOwner) throws OdpsException ``` --- ### changeComment ```java public void changeComment(String newComment) throws OdpsException ``` --- ### touch 更新表的最后修改时间为当前时间。 ```java public void touch() throws OdpsException ``` --- ### rename ```java public void rename(String newName) throws Exception ``` --- ### addColumns ```java public void addColumns(List columns, boolean ifNotExists) throws Exception ``` --- ### dropColumns ```java public void dropColumns(List columnNames) throws Exception ``` --- ### alterColumnType ```java public void alterColumnType(String columnName, TypeInfo columnType) throws Exception ``` --- ### changeColumnName ```java public void changeColumnName(String oldColumnName, String newColumnName) throws Exception ``` --- ### changeClusterInfo ```java public void changeClusterInfo(ClusterInfo clusterInfo) throws OdpsException ``` ## 分区 ### getPartition ```java public Partition getPartition(PartitionSpec partitionSpec) ``` --- ### getPartitions ```java public List getPartitions() ``` --- ### getPartitionSpecs 仅返回分区值,不含详细信息,效率更高。 ```java public List getPartitionSpecs() ``` --- ### hasPartition ```java public boolean hasPartition(PartitionSpec partitionSpec) ``` --- ### createPartition ```java public void createPartition(PartitionSpec partitionSpec) throws OdpsException ``` --- ### deletePartition ```java public void deletePartition(PartitionSpec partitionSpec) throws OdpsException ``` --- ### createPartition (ifNotExists) 创建分区,可指定 ifNotExists。 ```java public void createPartition(PartitionSpec spec, boolean ifNotExists) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `spec` | PartitionSpec | 分区表达式 | | `ifNotExists` | boolean | 为 `true` 时,分区已存在不抛异常 | --- ### deletePartition (ifExists) 删除分区,可指定 ifExists。 ```java public void deletePartition(PartitionSpec spec, boolean ifExists) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `spec` | PartitionSpec | 分区表达式 | | `ifExists` | boolean | 为 `true` 时,分区不存在不抛异常 | --- ### getPartitionIterator 获取所有分区的迭代器。 ```java public Iterator getPartitionIterator() ``` **返回值**:分区迭代器 --- ### getPartitionIterator (指定父分区) 获取指定父分区下的分区迭代器。 ```java public Iterator getPartitionIterator(PartitionSpec spec) ``` | 参数 | 类型 | 说明 | |------|------|------| | `spec` | PartitionSpec | 父分区表达式 | **返回值**:分区迭代器 --- ### getPartitionIterator (高级) 获取分区迭代器,支持反序、分页和数量限制。 ```java public Iterator getPartitionIterator(PartitionSpec spec, boolean reverse, Long batchSize, Long limit) ``` | 参数 | 类型 | 说明 | |------|------|------| | `spec` | PartitionSpec | 父分区表达式,null 表示全部 | | `reverse` | boolean | 是否反序返回 | | `batchSize` | Long | 每次请求的分区数量 | | `limit` | Long | 最多返回的分区总数 | **返回值**:分区迭代器 ## 标签 ### getTags ```java // 表级别 public List getTags() // 字段级别 public List getTags(String columnName) ``` --- ### addTag ```java public void addTag(Tag tag) public void addTag(Tag tag, List columns) ``` --- ### removeTag ```java public void removeTag(Tag tag) ``` --- ### getSimpleTags ```java public Map> getSimpleTags() public Map> getSimpleTags(String columnName) ``` --- ### addSimpleTag ```java public void addSimpleTag(String category, String key, String value) public void addSimpleTag(String category, String key, String value, List columns) ``` --- ### removeSimpleTag ```java public void removeSimpleTag(String category, String key, String value) public void removeSimpleTag(String category, String key, String value, List columns) ``` ## 扩展信息 这部分信息通过 lazy 加载获取,无法通过 `reload()` 刷新。 ### isArchived ```java public boolean isArchived() ``` --- ### isTransactional ```java public boolean isTransactional() ``` --- ### isDeltaTable 判断是否为 Delta Table(带主键的事务表)。 ```java public boolean isDeltaTable() ``` **返回值**:`true` 表示为 Delta Table --- ### hasRowAccessPolicy 判断表是否设置了行级访问策略。 ```java public boolean hasRowAccessPolicy() ``` **返回值**:`true` 表示设置了行级访问策略 --- ### getPrimaryKey 获取主键列名列表。 ```java public List getPrimaryKey() ``` **返回值**:主键列名列表,无主键时返回空列表 --- ### getAcidDataRetainHours 获取 ACID 数据保留时长(小时)。 ```java public int getAcidDataRetainHours() ``` **返回值**:数据保留时长(小时) --- ### getPhysicalSize ```java public long getPhysicalSize() ``` **返回值**:表磁盘物理大小(估计值) --- ### getFileNum ```java public long getFileNum() ``` **返回值**:表占用文件数(估计值) --- ### getClusterInfo ```java public ClusterInfo getClusterInfo() ``` --- ### getReserved ```java public String getReserved() ``` **返回值**:JSON 格式的保留字段信息 --- ### 视图相关 ```java public String getViewText() public String getViewExpandedText() public boolean isMaterializedViewRewriteEnabled() public boolean isMaterializedViewOutdated() ``` --- ### isAutoRefreshEnabled 获取物化视图是否启用自动刷新。 ```java public boolean isAutoRefreshEnabled() ``` **返回值**:`true` 表示启用自动刷新 --- ### isAutoSubstituteEnabled 获取物化视图是否启用自动替换(可为 null)。 ```java public Boolean isAutoSubstituteEnabled() ``` **返回值**:`true` 表示启用自动替换,`null` 表示未设置 --- ### getRefreshInterval 获取物化视图刷新间隔(分钟,可为 null)。 ```java public Integer getRefreshInterval() ``` **返回值**:刷新间隔(分钟),`null` 表示未设置 --- ### getRefreshCron 获取物化视图刷新 cron 表达式。 ```java public String getRefreshCron() ``` **返回值**:cron 表达式 --- ### getRefreshHistory 获取物化视图刷新历史(最多 10 条)。 ```java public List> getRefreshHistory() ``` **返回值**:刷新历史列表,每条记录为 key-value 形式 --- ### 外部表相关 ```java public String getLocation() public String getResources() public String getStorageHandler() public Map getSerDeProperties() ``` ## CDC (Change Data Capture) ### getCdcSize 获取 CDC 数据大小。 ```java public long getCdcSize() ``` **返回值**:CDC 数据大小(bytes) --- ### getCdcRecordNum 获取 CDC 记录数。 ```java public long getCdcRecordNum() ``` **返回值**:CDC 记录数 --- ### getCdcLatestVersion 获取最新 CDC 版本号。 ```java public long getCdcLatestVersion() ``` **返回值**:最新 CDC 版本号 --- ### getCdcLatestTimestamp 获取最新 CDC 时间戳。 ```java public Date getCdcLatestTimestamp() ``` **返回值**:最新 CDC 时间戳 ## 存储分层 / 生命周期 ### getStorageTierInfo 获取存储分层信息(仅非分区表)。 ```java public StorageTierInfo getStorageTierInfo() ``` **返回值**:`StorageTierInfo` 对象,包含存储分层详细信息 --- ### getTableLifecycleConfig 获取表生命周期配置。 ```java public TableLifecycleConfig getTableLifecycleConfig() ``` **返回值**:`TableLifecycleConfig` 对象,包含生命周期配置 ## Stream ### newStream 在事务表上创建新的 Stream。 ```java public Stream newStream(String streamName) throws OdpsException ``` | 参数 | 类型 | 说明 | |------|------|------| | `streamName` | String | Stream 名称 | **返回值**:创建的 `Stream` 对象 --- # reference/TableReadSession.md # TableReadSession `TableReadSession` 是 Storage API 的读取会话,用于从 MaxCompute 表中读取 Arrow 格式数据。支持列裁剪、谓词下推、分区过滤和并行分片读取。 ## 获取实例 通过 `MaxStorageClient.createTableReadSessionBuilder()` 创建 Builder 并构建: ```java MaxStorageClient client = MaxStorageClient.builder() .endpoint(endpoint) .credentialsProvider(credentialsProvider) .build(); TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "name")) .build(); ``` ## TableReadSessionBuilder 通过 `client.createTableReadSessionBuilder(tableId)` 获取。 ### withColumns 指定需要读取的数据列(列裁剪)。 ```java public TableReadSessionBuilder withColumns(List requiredDataColumns) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `requiredDataColumns` | `List` | 列名列表;不设置则读取所有列 | --- ### withPartitionColumns 指定需要返回的分区列。 ```java public TableReadSessionBuilder withPartitionColumns(List requiredPartitionColumns) ``` --- ### withPartitions 指定仅读取特定分区。 ```java public TableReadSessionBuilder withPartitions(List requiredPartitions) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `requiredPartitions` | `List` | 分区规格列表 | --- ### withFilter 服务端谓词下推过滤,减少网络传输量。 ```java public TableReadSessionBuilder withFilter(String filterPredicate) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `filterPredicate` | `String` | 过滤表达式,如 `"age > 18 AND city = 'Beijing'"` | --- ### enableFilterFallback 当服务端不支持过滤谓词时是否回退为全量读取。 ```java public TableReadSessionBuilder enableFilterFallback(boolean enable) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `enable` | `boolean` | `true` 允许回退;`false`(默认)不支持时抛异常 | --- ### withSplitOptions 配置数据切分策略,控制并行度。 ```java public TableReadSessionBuilder withSplitOptions(SplitOptions splitOptions) ``` **SplitOptions Builder 方法**: | 方法 | 默认值 | 说明 | |------|--------|------| | `withSplitSize(long bytes)` | 256MB | 按字节大小分片 | | `withSplitRowCount(long rowCount)` | - | 按行数分片 | | `withCrossPartition(boolean)` | `true` | 是否允许跨分区 Split | --- ### withBucketIds 对分桶表指定读取特定 Bucket。 ```java public TableReadSessionBuilder withBucketIds(List requiredBucketIds) ``` --- ### withIncrementalReadOptions 配置增量读取参数。 ```java public TableReadSessionBuilder withIncrementalReadOptions(IncrementalReadOptions options) ``` **IncrementalReadOptions Builder 方法**: | 方法 | 说明 | |------|------| | `withMode(String)` | `"timestamp"` 或 `"version"` | | `withStartTimeStamp(String)` | 起始时间,格式 `"yyyy-MM-dd HH:mm:ss"` | | `withEndTimeStamp(String)` | 结束时间 | | `withStartVersion(long)` | 起始版本号 | | `withEndVersion(long)` | 结束版本号 | --- ### withSessionId 通过已有 Session ID 复用 Session。 ```java public TableReadSessionBuilder withSessionId(String sessionId) ``` --- ### withSessionReadyTimeout 设置等待 Session 就绪的超时时间。 ```java public TableReadSessionBuilder withSessionReadyTimeout(long timeoutSeconds) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `timeoutSeconds` | `long` | 超时秒数,默认 3600(1小时) | --- ### withArrowOptions 设置 Arrow 序列化选项。 ```java public TableReadSessionBuilder withArrowOptions(ArrowOptions arrowOptions) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `arrowOptions` | `ArrowOptions` | Arrow 序列化配置 | --- ### withMaxFilesPerSplit 设置每个分片的最大文件数。 ```java public TableReadSessionBuilder withMaxFilesPerSplit(int maxFilesPerSplit) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `maxFilesPerSplit` | `int` | 每个 Split 包含的最大文件数 | --- ### withIncrementalReadEnabled 设置是否启用增量读取。 ```java public TableReadSessionBuilder withIncrementalReadEnabled(boolean incrementalRead) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `incrementalRead` | `boolean` | `true` 启用增量读取;`false` 禁用 | --- ### build ```java public TableReadSession build() ``` **返回值**:`TableReadSession` 实例 **异常**:`ClientException`(创建失败或超时)、`MaxStorageException`(服务端错误) --- ## TableReadSession 方法 ### getSplits 获取所有 InputSplit,每个 Split 可独立并行读取。 ```java public List getSplits() ``` **返回值**:`List` --- ### getArrowSchema 获取 Session 数据的 Arrow Schema。 ```java public Schema getArrowSchema() ``` **返回值**:Apache Arrow `Schema` 对象 --- ### getTableSchema 获取 Session 数据的 MaxCompute 表 Schema。 ```java public TableSchema getTableSchema() ``` --- ### getId 获取 Session 唯一标识符。 ```java public String getId() ``` --- ### createReaderBuilder 针对指定 InputSplit 创建 Reader Builder。 ```java public TableReaderBuilder createReaderBuilder(InputSplit split) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `split` | `InputSplit` | 来自 `getSplits()` 的数据分片 | **返回值**:`TableReaderBuilder` 实例 --- ## TableReaderBuilder 通过 `session.createReaderBuilder(split)` 获取。 | 方法 | 说明 | |------|------| | `withMaxBatchRows(long)` | 每批最大行数 | | `withMaxBatchRawSize(long)` | 每批最大原始数据大小(字节) | | `withSkipRowNum(long)` | 跳过的行数 | | `withDataColumns(List)` | Split 级别列选择 | | `build()` | 构建 `ArrowReader` 实例 | --- ## 使用示例 ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableReadSession session = client.createTableReadSessionBuilder(tableId) .withColumns(Arrays.asList("id", "name", "score")) .withFilter("score >= 60") .withSplitOptions(SplitOptions.newBuilder().withSplitSize(256 * 1024 * 1024L).build()) .build(); List splits = session.getSplits(); for (InputSplit split : splits) { try (ArrowReader reader = session.createReaderBuilder(split).build()) { while (reader.loadNextBatch()) { VectorSchemaRoot root = reader.getVectorSchemaRoot(); int rowCount = root.getRowCount(); // 处理数据... } } } ``` --- # reference/TableTunnel.md # TableTunnel `TableTunnel` 是 MaxCompute Tunnel 服务的入口类,用于创建数据上传、下载和流式写入会话。 ## 获取实例 ```java // 方式一:通过 Odps 对象创建 TableTunnel tunnel = odps.tableTunnel(); // 方式二:直接构造 TableTunnel tunnel = new TableTunnel(odps); tunnel.setEndpoint("http://dt.odps.aliyun.com"); // 方式三:自定义 Configuration TableTunnel tunnel = new TableTunnel(odps, configuration); ``` ## 方法列表 ### setEndpoint 设置 Tunnel 服务地址。未设置时自动路由。 ```java public void setEndpoint(String endpoint) ``` | 参数 | 类型 | 说明 | |------|------|------| | `endpoint` | String | Tunnel 服务地址 | --- ### getConfig 获取 Tunnel 配置对象。 ```java public Configuration getConfig() ``` **返回值**:`Configuration` 对象 --- ### createUploadSession 在表上创建上传会话。 ```java // 非分区表 public UploadSession createUploadSession(String projectName, String tableName) throws TunnelException public UploadSession createUploadSession(String projectName, String tableName, boolean overwrite) throws TunnelException public UploadSession createUploadSession(String projectName, String schemaName, String tableName, boolean overwrite) throws TunnelException // 分区表 public UploadSession createUploadSession(String projectName, String tableName, PartitionSpec partitionSpec) throws TunnelException public UploadSession createUploadSession(String projectName, String tableName, PartitionSpec partitionSpec, boolean overwrite) throws TunnelException public UploadSession createUploadSession(String projectName, String schemaName, String tableName, PartitionSpec partitionSpec, boolean overwrite) throws TunnelException ``` | 参数 | 类型 | 必需 | 说明 | |------|------|------|------| | `projectName` | String | 是 | 项目名称 | | `schemaName` | String | 否 | Schema 名称(三层模型) | | `tableName` | String | 是 | 表名称 | | `partitionSpec` | PartitionSpec | 否 | 分区表达式(分区表必填) | | `overwrite` | boolean | 否 | 是否覆盖已有数据,默认 false | **示例**: ```java UploadSession session = tunnel.createUploadSession("my_project", "my_table"); ``` --- ### getUploadSession 获取已有的上传会话。 ```java public UploadSession getUploadSession(String projectName, String tableName, String id) throws TunnelException public UploadSession getUploadSession(String projectName, String tableName, PartitionSpec partitionSpec, String id) throws TunnelException public UploadSession getUploadSession(String projectName, String tableName, String id, long shares, long shareId) throws TunnelException ``` | 参数 | 类型 | 说明 | |------|------|------| | `id` | String | 上传会话 ID | | `shares` | long | 共享会话的实例总数 | | `shareId` | long | 当前实例的唯一标识(从 0 开始) | --- ### buildDownloadSession 创建下载会话构建器(推荐方式)。 ```java public DownloadSessionBuilder buildDownloadSession(String projectName, String tableName) ``` **返回值**:`DownloadSessionBuilder` 实例 **示例**: ```java DownloadSession session = tunnel.buildDownloadSession("my_project", "my_table") .setPartitionSpec(new PartitionSpec("dt=20231001")) .setAsyncMode(true) .build(); ``` --- ### createDownloadSession 在表上创建下载会话(已废弃,推荐使用 `buildDownloadSession`)。 ```java @Deprecated public DownloadSession createDownloadSession(String projectName, String tableName) throws TunnelException @Deprecated public DownloadSession createDownloadSession(String projectName, String tableName, PartitionSpec partitionSpec) throws TunnelException ``` --- ### buildStreamUploadSession 创建流式上传会话构建器。 ```java public StreamUploadSession.Builder buildStreamUploadSession(String projectName, String tableName) ``` **返回值**:`StreamUploadSession.Builder` 实例 **示例**: ```java StreamUploadSession session = tunnel.buildStreamUploadSession("my_project", "my_table") .setPartitionSpec("dt=20231001") .build(); ``` --- ### buildUpsertSession 创建 Upsert 会话构建器。 ```java public UpsertSession.Builder buildUpsertSession(String projectName, String tableName) ``` **返回值**:`UpsertSession.Builder` 实例 --- ### preview 数据预览,最多返回 5000 行,返回 Arrow 格式数据流。 ```java public ArrowStreamReader preview(String projectName, String schemaName, String tableName) throws TunnelException public ArrowStreamReader preview(String projectName, String schemaName, String tableName, String partitionSpec) throws TunnelException public ArrowStreamReader preview(String projectName, String schemaName, String tableName, String partitionSpec, Long limit) throws TunnelException public ArrowStreamReader preview(String projectName, String schemaName, String tableName, String partitionSpec, Long limit, List requiredColumns) throws TunnelException ``` | 参数 | 类型 | 说明 | |------|------|------| | `projectName` | String | 项目名称 | | `schemaName` | String | Schema 名称,可为 null | | `tableName` | String | 表名称 | | `partitionSpec` | String | 分区表达式,可为 null | | `limit` | Long | 最多读取行数,最大 5000 | | `requiredColumns` | List\ | 指定列名,可为 null | **返回值**:`ArrowStreamReader`,可使用 `ArrowStreamRecordReader` 转换为 RecordReader **示例**: ```java ArrowStreamReader reader = tunnel.preview("my_project", null, "my_table", null, 100L); ``` --- # reference/TableWriteSession.md # TableWriteSession `TableWriteSession` 是 Storage API 的写入会话,支持以 Arrow 格式向 MaxCompute 表写入数据。提供批量(Batch)和流式(Streaming)两种写入模式。 ## 获取实例 通过 `MaxStorageClient.createTableWriteSessionBuilder()` 创建 Builder 并构建: ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); TableWriteSession session = client.createTableWriteSessionBuilder(tableId) .withWriteMode(WriteMode.BATCH) .build(); ``` ## WriteMode 枚举 | 枚举值 | 说明 | |--------|------| | `WriteMode.BATCH` | 批量模式(默认),`commit()` 后数据可见,支持事务回滚 | | `WriteMode.STREAMING` | 流式模式,`flush()` 后数据立即可见,不支持回滚 | ## TableWriteSessionBuilder 通过 `client.createTableWriteSessionBuilder(tableId)` 获取。 ### withWriteMode 设置写入模式。 ```java public TableWriteSessionBuilder withWriteMode(WriteMode writeMode) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `writeMode` | `WriteMode` | `BATCH`(默认)或 `STREAMING` | --- ### withPartition 设置写入目标分区。分区表必须指定。 ```java public TableWriteSessionBuilder withPartition(PartitionSpec partitionSpec) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `partitionSpec` | `PartitionSpec` | 分区规格,如 `new PartitionSpec("dt='20250101'")` | --- ### withOverwrite 是否覆盖目标表或分区中的现有数据。 ```java public TableWriteSessionBuilder withOverwrite(boolean overwrite) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `overwrite` | `boolean` | `true` 覆盖写入;`false`(默认)追加写入 | --- ### withSessionId 复用已有 Session。 ```java public TableWriteSessionBuilder withSessionId(String sessionId) ``` --- ### build ```java public TableWriteSession build() ``` **返回值**:`TableWriteSession` 实例 --- ## TableWriteSession 方法 ### createWriterBuilder 创建写入 Builder。 ```java public TableWriterBuilder createWriterBuilder(String streamId, long streamVersion) ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `streamId` | `String` | Stream 唯一标识,同一 Session 下不同 Writer 使用不同 ID | | `streamVersion` | `long` | Stream 版本号,必须 >= 1,用于幂等重试 | **返回值**:`TableWriterBuilder` 实例 > `streamId` + `streamVersion` 组合用于幂等重试:相同组合重新写入时服务端只保留一份结果。 --- ### commit 提交写入事务,使所有已写入数据对外可见。 ```java public void commit() ``` - **Batch 模式**:必须调用才能使数据可见 - **Streaming 模式**:空操作,数据已在 `flush()` 时可见 --- ### abort 中止写入事务,丢弃所有已写入数据。 ```java public void abort() ``` - **Batch 模式**:放弃所有未提交数据 - **Streaming 模式**:无效操作 --- ### close 关闭 Session。若未显式 `commit()`,自动执行 `abort()`。 ```java public void close() ``` --- ### getId ```java public String getId() ``` **返回值**:Session 唯一标识符 --- ### getWriteMode ```java public WriteMode getWriteMode() ``` **返回值**:当前 Session 的写入模式 --- ## TableWriterBuilder 通过 `session.createWriterBuilder(streamId, version)` 获取。 | 方法 | 说明 | |------|------| | `withBufferSize(long)` | 缓冲区大小(字节),默认 64MB | | `withAutoFlushEnabled(boolean)` | 是否启用自动 Flush,默认 `true` | | `withExecutorService(ExecutorService)` | 异步 Flush 线程池 | | `withBatchBlobUploadEnabled(boolean)` | 启用批量 Blob 上传 | | `build()` | 构建 `ArrowWriter`(实际为 `TableArrowWriter`) | --- ## TableArrowWriter ### createVectorSchemaRoot 创建与表 Schema 匹配的空 `VectorSchemaRoot`。 ```java public VectorSchemaRoot createVectorSchemaRoot() ``` --- ### writeBatch 写入一个 Arrow 数据批次到缓冲区。调用返回后 `root` 可安全复用。 ```java public void writeBatch(VectorSchemaRoot root) ``` --- ### flush 将缓冲区数据发送到服务端。 ```java public void flush() ``` --- ### bytesWritten 返回已写入的总字节数。 ```java public long bytesWritten() ``` --- ## 使用示例 ```java TableIdentifier tableId = TableIdentifier.of("my_project", "my_table"); try (TableWriteSession session = client.createTableWriteSessionBuilder(tableId) .withPartition(new PartitionSpec("dt='20250101'")) .build()) { try (ArrowWriter writer = session.createWriterBuilder("stream-1", 1).build()) { TableArrowWriter arrowWriter = (TableArrowWriter) writer; try (VectorSchemaRoot root = arrowWriter.createVectorSchemaRoot()) { root.allocateNew(); VarCharVector nameVec = (VarCharVector) root.getVector("name"); for (int i = 0; i < 1000; i++) { nameVec.setSafe(i, ("user_" + i).getBytes()); } root.setRowCount(1000); writer.writeBatch(root); } writer.flush(); } session.commit(); } ``` --- # reference/UploadSession.md # UploadSession `UploadSession` 用于向 MaxCompute 表批量上传数据,支持基于 blockId 的多线程写入和事务性提交。会话服务端生命周期为 24 小时。 ## 获取实例 ```java // 创建新会话 UploadSession session = tunnel.createUploadSession("my_project", "my_table"); // 分区表 + 覆盖写 UploadSession session = tunnel.createUploadSession("my_project", "my_table", new PartitionSpec("dt=20231001"), true); // 复用已有会话 UploadSession session = tunnel.getUploadSession("my_project", "my_table", sessionId); ``` ## 方法列表 ### getSchema 获取表结构信息。 ```java public TableSchema getSchema() ``` --- ### getId 获取会话 ID。 ```java public String getId() ``` --- ### getStatus 获取会话状态。 ```java public UploadStatus getStatus() throws TunnelException, IOException ``` **返回值**:`UploadStatus` 枚举,可选值:`UNKNOWN`、`NORMAL`、`CLOSING`、`CLOSED`、`EXPIRED`、`CRITICAL` --- ### newRecord 创建空记录对象。 ```java public Record newRecord() ``` --- ### openRecordWriter 打开基于 blockId 的记录写入器。每个 blockId 应仅有一个写入者。 ```java public RecordWriter openRecordWriter(long blockId) throws TunnelException, IOException public RecordWriter openRecordWriter(long blockId, boolean compress) throws TunnelException, IOException public RecordWriter openRecordWriter(long blockId, CompressOption option) throws TunnelException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `blockId` | long | 数据块 ID(0 ~ 19999) | | `compress` | boolean | 是否启用压缩 | | `option` | CompressOption | 压缩选项 | **注意**: - 单个 Block 上限 100GB,建议大于 64MB - Writer 120 秒无网络活动将被服务端关闭 **示例**: ```java RecordWriter writer = session.openRecordWriter(0); Record record = session.newRecord(); record.set("id", 1L); record.set("name", "test"); writer.write(record); writer.close(); ``` --- ### openBufferedWriter 打开带缓冲区的写入器,自动管理 blockId。 ```java public RecordWriter openBufferedWriter() throws TunnelException, IOException public RecordWriter openBufferedWriter(boolean compress) throws TunnelException, IOException public RecordWriter openBufferedWriter(CompressOption option) throws TunnelException, IOException public RecordWriter openBufferedWriter(CompressOption option, long timeout) throws TunnelException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `compress` | boolean | 是否压缩 | | `option` | CompressOption | 压缩选项 | | `timeout` | long | 超时时间(毫秒),≤0 无超时 | --- ### openArrowRecordWriter 打开 Arrow 格式写入器。 ```java public ArrowRecordWriter openArrowRecordWriter(long blockId) throws TunnelException, IOException public ArrowRecordWriter openArrowRecordWriter(long blockId, CompressOption option) throws TunnelException, IOException public ArrowRecordWriter openArrowRecordWriter(long blockId, CompressOption option, long blockVersion) throws TunnelException, IOException ``` --- ### writeBlock 通过 RecordPack 写入数据块。 ```java public void writeBlock(long blockId, RecordPack pack) throws IOException public void writeBlock(long blockId, RecordPack pack, long timeout) throws IOException public void writeBlock(long blockId, RecordPack pack, long timeout, long blockVersion) throws IOException, TunnelException ``` --- ### getBlockList 获取已上传的 block 列表。 ```java public Long[] getBlockList() ``` **返回值**:已上传成功的 blockId 数组 --- ### commit 提交上传,完成数据持久化。 ```java // 无校验提交 public void commit() throws TunnelException, IOException // 带 block 列表校验提交 public void commit(Long[] blocks) throws TunnelException, IOException ``` | 参数 | 类型 | 说明 | |------|------|------| | `blocks` | Long[] | 已上传的 blockId 列表,用于完整性校验 | **示例**: ```java writer.close(); session.commit(new Long[]{0L, 1L, 2L}); ``` --- ### getArrowSchema 获取 Arrow 格式的表结构。 ```java public Schema getArrowSchema() ``` --- ### getQuotaName 获取本次上传使用的 quota 名称。 ```java public String getQuotaName() ``` --- # reference/UpsertSession.md # UpsertSession `UpsertSession` 用于管理对 Transactional 表(主键表)的数据插入或更新会话。通过该接口可以获取会话信息、提交或中止会话、构建 `UpsertStream` 执行写入操作。 ## 获取实例 通过 `TableTunnel.buildUpsertSession()` 创建 Builder 并构建: ```java TableTunnel tunnel = odps.tableTunnel(); UpsertSession session = tunnel.buildUpsertSession(projectName, tableName) .setSchemaName(schemaName) .setPartitionSpec(partitionSpec) .build(); ``` ## UpsertSession 方法 ### getId ```java String getId() ``` **返回值**:当前会话 ID,可用于重建会话 --- ### getQuotaName ```java public String getQuotaName() ``` **返回值**:当前使用的 Quota 名称 --- ### getStatus ```java String getStatus() throws TunnelException ``` **返回值**:会话状态码 | 状态 | 说明 | |------|------| | `normal` | 正常 | | `committing` | 提交中 | | `committed` | 已提交 | | `expired` | 过期 | | `critical` | 错误 | | `aborted` | 已中止 | --- ### getSchema ```java TableSchema getSchema() ``` **返回值**:当前会话写入的表的表结构 --- ### commit 提交当前会话,使数据可见。 ```java void commit(boolean async) throws TunnelException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `async` | `boolean` | `true` 异步提交(立即返回,数据不立即可见);`false` 同步提交 | --- ### abort ```java void abort() throws TunnelException ``` 中止当前会话,丢弃未提交数据。 --- ### close ```java void close() ``` 清理客户端本地资源。 --- ### newRecord ```java Record newRecord() ``` **返回值**:`UpsertRecord` 实例,包含 upsert 操作所需的元数据隐藏列 > 执行 Upsert 操作时必须使用此方法获取 Record,不要手动创建 `ArrayRecord`。 --- ### buildUpsertStream ```java UpsertStream.Builder buildUpsertStream() ``` **返回值**:`UpsertStream.Builder` 对象 --- ## Builder 接口 `UpsertSession.Builder` 用于配置和构建 `UpsertSession`。通过 `TableTunnel.buildUpsertSession()` 获取。 ### 配置方法 | 方法 | 说明 | |------|------| | `setUpsertId(String)` | 设置已有会话 ID(用于重建) | | `setSchemaName(String)` | 设置 Schema 名称 | | `setPartitionSpec(PartitionSpec)` | 设置分区规格 | | `setPartitionSpec(String)` | 设置分区规格(字符串形式) | | `setSlotNum(long)` | 设置 Slot 数量 | | `setCommitTimeout(long)` | 设置提交超时时间(毫秒) | | `setNetworkThreadNum(int)` | 设置 Netty 网络 IO 线程数,默认 1 | | `setConcurrentNum(int)` | 设置最大并发 Channel 数,默认 20,负值表示无限制 | | `setConnectTimeout(long)` | 设置连接超时(毫秒),默认 180000 | | `setReadTimeout(long)` | 设置响应超时(毫秒),默认 300000 | | `setLifecycle(long)` | 设置 Session 生命周期(小时),有效范围 1-24 | ### build ```java UpsertSession build() throws TunnelException, IOException ``` **返回值**:`UpsertSession` 实例 --- ## UpsertStream `UpsertStream` 是执行数据插入、更新、删除操作的核心接口,支持数据缓冲和批量提交。 ### 获取实例 ```java UpsertStream stream = session.buildUpsertStream().build(); ``` ### upsert 插入或更新一条记录。 ```java void upsert(Record record) throws IOException, TunnelException void upsert(Record record, List upsertCols) throws IOException, TunnelException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `record` | `Record` | `UpsertRecord` 实例(通过 `session.newRecord()` 创建) | | `upsertCols` | `List` | 需要更新的列名列表(部分列更新) | --- ### delete 删除一条记录。 ```java void delete(Record record) throws IOException, TunnelException ``` **参数**: | 参数 | 类型 | 说明 | |------|------|------| | `record` | `Record` | `UpsertRecord` 实例,设置主键值即可 | --- ### flush 刷新缓冲区,将缓冲区中的数据提交到服务器。 ```java void flush() throws IOException, TunnelException ``` --- ### close 关闭流,提交所有未提交的数据。 ```java void close() throws IOException, TunnelException ``` --- ### reset 重置缓冲区,清空所有未提交的数据。 ```java void reset() throws IOException ``` --- ## UpsertStream.Builder 通过 `session.buildUpsertStream()` 获取。 | 方法 | 说明 | |------|------| | `setMaxBufferSize(long)` | 设置数据缓冲最大容量(字节) | | `setSlotBufferSize(long)` | 设置每个桶的缓冲区大小(字节) | | `setCompressOption(CompressOption)` | 设置压缩策略 | | `setListener(Listener)` | 设置事件监听器(用于重试逻辑) | | `build()` | 构建 UpsertStream 实例 | --- ## 使用示例 ```java TableTunnel tunnel = odps.tableTunnel(); try (UpsertSession session = tunnel.buildUpsertSession(projectName, tableName) .setSchemaName(schemaName) .setPartitionSpec("dt='20250101'") .build()) { UpsertStream stream = session.buildUpsertStream().build(); Record record = session.newRecord(); // 插入/更新 record.setString("key", "k1"); record.setString("value", "v1"); stream.upsert(record); // 删除 record.setString("key", "k2"); stream.delete(record); stream.flush(); session.commit(false); } ```