0

0

Java 8与SQL:高效获取每日最新货币数据教程

碧海醫心

碧海醫心

发布时间:2025-09-30 10:31:21

|

281人浏览过

|

来源于php中文网

原创

Java 8与SQL:高效获取每日最新货币数据教程

本教程探讨如何使用Java 8 Stream API和SQL查询,从包含多个时间戳的货数据中,高效地提取每个货币在每天的最新记录。文章将详细介绍基于分组和聚合的Stream API实现,以及利用数据库窗口函数的SQL解决方案,旨在帮助开发者处理时间序列数据的去重与筛选。

在处理金融或交易数据时,一个常见的需求是,针对特定实体(如货币)在每天的记录中,只保留最新(即时间戳最大)的一条。例如,我们可能有一系列关于某种货币在不同时间点接收到的数据,但我们只关心每天的最终更新状态。

假设我们有以下 Currency 类定义:

import java.time.LocalDateTime;
import java.time.LocalDate; // 用于日期部分

class Currency {
    private Integer id;
    private String name;
    private LocalDateTime lastReceived;

    public Currency(Integer id, String name, LocalDateTime lastReceived) {
        this.id = id;
        this.name = name;
        this.lastReceived = lastReceived;
    }

    public Integer getId() { return id; }
    public String getName() { return name; }
    public LocalDateTime getLastReceived() { return lastReceived; }

    @Override
    public String toString() {
        return "Currency{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", lastReceived=" + lastReceived +
               '}';
    }
}

以及以下示例数据:

ID NAME LAST_RECEIVED
1 USD 2022-05-18 09:04:01.545
2 USD 2022-05-18 08:04:01.545
3 USD 2022-05-19 08:04:01.545
4 USD 2022-05-20 08:04:01.545
5 USD 2022-05-20 11:04:01.545
6 BUSD 2022-05-18 08:04:01.545

我们的目标是获取如下结果:

ID NAME LAST_RECEIVED
1 USD 2022-05-18 09:04:01.545
3 USD 2022-05-19 08:04:01.545
5 USD 2022-05-20 11:04:01.545

可以看到,对于每天的USD记录,我们只保留了时间戳最晚的那一条。

立即学习Java免费学习笔记(深入)”;

一、使用Java 8 Stream API实现

Java 8的Stream API提供了强大的数据处理能力,我们可以利用 groupingBy 和 collectingAndThen 组合来解决这个问题。核心思路是:首先根据货币名称和日期对数据进行分组,然后在每个组内找到时间戳最大的记录。

1.1 获取所有货币在每天的最新记录

为了实现对所有货币的每日最新记录提取,我们需要一个复合键来分组:货币名称和 lastReceived 的日期部分。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CurrencyProcessor {

    public static void main(String[] args) {
        List<Currency> data = Arrays.asList(
            new Currency(1, "USD", LocalDateTime.parse("2022-05-18T09:04:01.545")),
            new Currency(2, "USD", LocalDateTime.parse("2022-05-18T08:04:01.545")),
            new Currency(3, "USD", LocalDateTime.parse("2022-05-19T08:04:01.545")),
            new Currency(4, "USD", LocalDateTime.parse("2022-05-20T08:04:01.545")),
            new Currency(5, "USD", LocalDateTime.parse("2022-05-20T11:04:01.545")),
            new Currency(6, "BUSD", LocalDateTime.parse("2022-05-18T08:04:01.545")),
            new Currency(7, "EUR", LocalDateTime.parse("2022-05-18T10:00:00.000")),
            new Currency(8, "EUR", LocalDateTime.parse("2022-05-18T09:00:00.000"))
        );

        // 1. 获取所有货币在每天的最新记录 (无序)
        List<Currency> lastByDate = new ArrayList<>(data
                .stream() // Stream<Currency>
                .collect(Collectors.groupingBy(
                    // 分组键:货币名称 + 日期 (LocalDate)
                    curr -> Arrays.asList(curr.getName(), curr.getLastReceived().toLocalDate()),
                    // 在每个组内,找到lastReceived最大的Currency对象
                    Collectors.collectingAndThen(
                        Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)),
                        Optional::get // 将Optional<Currency>转换为Currency
                    )
                )) // 结果是一个 Map<List<Object>, Currency>
                .values() // 获取Map中所有的Currency值,形成一个Collection
        );

        System.out.println("所有货币在每天的最新记录 (无序):");
        lastByDate.forEach(System.out::println);

        // 2. 如果需要排序,可以对结果列表进行排序
        lastByDate.sort(
            Comparator.comparing(Currency::getName)
                      .thenComparing(Currency::getLastReceived)
        );
        System.out.println("\n所有货币在每天的最新记录 (按名称和时间排序):");
        lastByDate.forEach(System.out::println);
    }
}

代码解析:

智川X-Agent
智川X-Agent

中科闻歌推出的一站式AI智能体开发平台

下载
  • Collectors.groupingBy(keyMapper, valueCollector): 这是核心操作。
    • keyMapper: curr -> Arrays.asList(curr.getName(), curr.getLastReceived().toLocalDate()) 创建了一个复合键,确保我们按货币名称和日期进行分组。toLocalDate() 方法将 LocalDateTime 转换为 LocalDate,只保留日期部分。
    • valueCollector: Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)), Optional::get)。
      • Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)): 在每个分组内部,找到 lastReceived 值最大的 Currency 对象。这个操作返回一个 Optional
      • Optional::get: 由于我们知道每个分组至少包含一个元素(否则它不会被创建),所以 maxBy 总是会找到一个值,因此可以直接使用 Optional::get 来提取 Currency 对象。在实际应用中,如果分组可能为空,应使用 orElse 或 orElseThrow 进行更安全的处理。
  • .values(): 从 Map 中获取所有值(即我们筛选出的 Currency 对象),形成一个 Collection。
  • new ArrayList(...): 将 Collection 转换为 ArrayList,以便后续进行排序。
  • lastByDate.sort(...): 如果需要对最终结果进行排序,可以根据 name 和 lastReceived 进行多字段排序。

1.2 获取特定货币在每天的最新记录

如果我们的需求是针对某个特定的货币(例如 "USD"),那么我们可以先进行过滤,然后简化分组键。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class CurrencyProcessorSpecific {

    public static void main(String[] args) {
        List<Currency> data = Arrays.asList(
            new Currency(1, "USD", LocalDateTime.parse("2022-05-18T09:04:01.545")),
            new Currency(2, "USD", LocalDateTime.parse("2022-05-18T08:04:01.545")),
            new Currency(3, "USD", LocalDateTime.parse("2022-05-19T08:04:01.545")),
            new Currency(4, "USD", LocalDateTime.parse("2022-05-20T08:04:01.545")),
            new Currency(5, "USD", LocalDateTime.parse("2022-05-20T11:04:01.545")),
            new Currency(6, "BUSD", LocalDateTime.parse("2022-05-18T08:04:01.545"))
        );

        String targetCurrency = "USD";

        List<Currency> lastUSDByDate = new ArrayList<>(data
                .stream()
                .filter(curr -> targetCurrency.equalsIgnoreCase(curr.getName())) // 过滤特定货币
                .collect(Collectors.groupingBy(
                    curr -> curr.getLastReceived().toLocalDate(), // 分组键现在只需日期
                    Collectors.collectingAndThen(
                        Collectors.maxBy(Comparator.comparing(Currency::getLastReceived)),
                        Optional::get
                    )
                ))
                .values()
        );

        // 对结果按时间排序
        lastUSDByDate.sort(Comparator.comparing(Currency::getLastReceived));

        System.out.println("\n特定货币 (" + targetCurrency + ") 在每天的最新记录:");
        lastUSDByDate.forEach(System::out::println);
    }
}

代码解析:

  • filter(curr -> targetCurrency.equalsIgnoreCase(curr.getName())): 在分组之前,先过滤出我们感兴趣的货币。
  • curr -> curr.getLastReceived().toLocalDate(): 过滤后,分组键就只需要 lastReceived 的日期部分了。
  • 其余逻辑与之前相同。

二、使用原生SQL查询(窗口函数)

对于存储在数据库中的数据,使用原生SQL查询通常是更高效和更推荐的方法,特别是当数据集非常大时。SQL中的窗口函数(Window Functions)非常适合解决这类“每个分组内取Top N”的问题。

以PostgreSQL为例,我们可以使用 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 来实现。

SELECT id, name, last_received
FROM (
    SELECT 
        c.*,
        ROW_NUMBER() OVER (
            PARTITION BY name, to_char(last_received, 'yyyy-MM-dd') -- 按货币名称和日期分区
            ORDER BY last_received DESC                             -- 在每个分区内按时间倒序排序
        ) AS rn                                                     -- 为每行分配一个行号
    FROM Currency c
    WHERE c.name = :currName -- 如果需要筛选特定货币
) tbl
WHERE rn = 1                 -- 只保留每个分区的第一行(即时间最新的记录)
ORDER BY last_received;      -- 最后按时间排序

SQL查询解析:

  • 内层查询 (子查询 tbl):
    • ROW_NUMBER() OVER (...): 这是一个窗口函数。
    • PARTITION BY name, to_char(last_received, 'yyyy-MM-dd'): 这定义了窗口(分组)。它告诉数据库将数据按 name 和 last_received 的日期部分进行分组。to_char(last_received, 'yyyy-MM-dd') 用于从时间戳中提取日期字符串。
    • ORDER BY last_received DESC: 在每个窗口(分组)内部,数据会根据 last_received 字段进行降序排序。这意味着最新的时间戳会排在最前面。
    • AS rn: ROW_NUMBER() 会为每个窗口中的每一行分配一个从1开始的连续整数。因为我们是降序排序,所以时间戳最新的那一行会得到 rn = 1。
  • 外层查询:
    • WHERE rn = 1: 这一步是关键,它筛选出每个分组中 rn 为1的记录,即每个货币在每天的最新记录。
    • ORDER BY last_received: 对最终结果进行排序,使其按时间顺序显示。
  • WHERE c.name = :currName: 如果只查询特定货币,可以在内层查询中添加此条件,这样可以减少处理的数据量,提高效率。

JPA集成注意事项:

目前,JPA(Java Persistence API)标准通常不支持直接在JPQL中使用窗口函数。因此,如果你正在使用JPA,你可能需要通过以下方式来执行此类查询:

  • 原生查询(Native Query): 如上述示例所示,使用 @Query(nativeQuery = true, value = "...") 注解在你的Repository接口中定义一个方法。
  • 存储过程或视图: 将复杂的逻辑封装在数据库的存储过程或视图中,JPA可以直接调用存储过程或查询视图。
  • MyBatis/JOOQ等其他ORM框架: 这些框架通常提供更灵活的SQL构建能力,可以更好地支持高级SQL特性。

三、总结与注意事项

  • Java 8 Stream API: 适用于内存中的小型到中型数据集。它提供了简洁、函数式的编程风格。需要注意的是,当数据量非常大时,在JVM内存中处理所有数据可能会导致性能瓶颈或内存溢出。
  • SQL窗口函数: 适用于数据库中存储的大型数据集。数据库引擎针对这类操作进行了高度优化,通常效率更高。对于复杂的聚合和排名需求,SQL是更强大的选择。
  • 性能考量: 对于生产环境中的大量数据,优先考虑在数据库层面解决问题,利用数据库的索引和优化器。Java Stream API更适合对已经加载到内存中的数据进行二次处理。
  • Optional::get 的安全性: 在Java Stream API的示例中,我们使用了 Optional::get。这假设每个分组至少包含一个元素。如果你的数据源可能导致某个分组为空(例如,如果 maxBy 的输入流为空),那么 Optional::get 会抛出 NoSuchElementException。在更严谨的代码中,你应该使用 orElse(null) 或 orElseThrow(...) 来处理 Optional 为空的情况。
  • 日期处理: 在Java中,LocalDateTime.toLocalDate() 是提取日期部分的标准方法。在SQL中,不同的数据库有不同的函数(例如PostgreSQL的 to_char(date_column, 'yyyy-MM-dd') 或 date_trunc('day', date_column),MySQL的 DATE(date_column))。

选择哪种方法取决于你的数据量、数据存储位置以及对性能和代码可维护性的要求。通常,数据库原生查询是处理大规模数据这类问题的首选方案。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

1133

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

340

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

381

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

2152

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

380

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

1683

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

585

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

440

2024.04.29

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
MySQL 教程
MySQL 教程

共48课时 | 2.5万人学习

MySQL 初学入门(mosh老师)
MySQL 初学入门(mosh老师)

共3课时 | 0.3万人学习

简单聊聊mysql8与网络通信
简单聊聊mysql8与网络通信

共1课时 | 848人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号