使用Java Streams(流)查詢數據庫

在本文中,您將瞭解如何編寫純Java應用程序,這些應用程序能夠使用來自現有數據庫的數據,而無需編寫一行SQL(或類似的語言,如HQL),也無需花費大量時間將所有內容組合在一起。在應用程序準備好之後,您將學習如何使用 in-JVM-acceleration(僅添加兩行代碼)加速超過1,000倍的延遲性能。

在本文中,我們將使用Speedment,它是一種Java stream ORM,可以直接從數據庫模式生成代碼,並可以自動將Java Streams呈現爲SQL,允許您使用純Java編寫代碼。

您還將發現,數據訪問性能可以通過直接從RAM運行流的in-JVM-memory技術顯著提高。

示例數據庫

我們將使用來自MySQL的示例數據庫Sakila。它有電影、演員、類別等表格,可以免費下載 here.

步驟 1: 連接到數據庫

我們將開始配置pom。您可以在這裏找到使用Speedment Initializer的xml文件,點擊 here下載. 您將得到帶有主文件夾的項目。java文件自動生成。

接下來,解壓項目文件夾zip文件,打開命令行,然後轉到解壓文件夾(pom所在的文件夾).xml文件)。

接下來,執行命令行:

mvn speedment:tool

這將啓動加速工具,並提示您輸入許可證密鑰。選擇“Start Free”,您將自動免費獲得許可證。現在您可以連接到數據庫並開始:

步驟 2: 生產代碼

從數據庫加載模式數據之後,可以通過按“Generate”按鈕生成完整的Java領域模型。

這只需要一兩秒鐘。

步驟 3: Write the Application Code

與步驟2中的域模型一起,將自動生成Speedment實例的構建器。打開main.java文件,並將main()方法中的代碼替換爲以下代碼片段:

SakilaApplication app = new SakilaApplicationBuilder()
    .withPassword("sakila-password") // Replace with your own password
    .build();

接下來,我們將編寫一個應用程序來打印所有的電影。誠然,這是一個小應用程序,但是我們將在本文中對其進行改進。

// Obtains a FilmManager that allows us to
// work with the "film" table
FilmManager films = app.getOrThrow(FilmManager.class);
// Create a stream of all films and print
// each and every film
films.stream()
    .forEach(System.out::println);

是不是很簡單?

在運行時,Java流將自動執行幕後的SQL。爲了實際查看呈現的SQL代碼,請修改我們的應用程序構建器並啓用使用流日誌類型的日誌記錄:

SakilaApplication app = new SakilaApplicationBuilder()
    .withPassword("sakila-password")
    .withLogging(ApplicationBuilder.LogType.STREAM)
    .build();

這是運行應用程序時SQL代碼的樣子:

SELECT
    `film_id`,`title`,`description`,`release_year`,
    `language_id`,`original_language_id`,`rental_duration`,`rental_rate`,
    `length`,`replacement_cost`,`rating`,`special_features`,`last_update`
 FROM
     `sakila`.`film`,
values:[]

呈現的SQL代碼可能因所選擇的數據庫類型而異(例如MySQL、MariaDB、PostgreSQL、Oracle、MS SQL Server、DB2、AS400等)。這些變化是自動的。

上面的代碼將產生以下輸出(爲了簡潔而縮短):

FilmImpl { filmId = 1, title = ACADEMY DINOSAUR, ..., length = 86, ... }
FilmImpl { filmId = 2, title = ACE GOLDFINGER, ..., length = 48, ...}
FilmImpl { filmId = 3, title = ADAPTATION HOLES, ..., length = 50, ...}
...

步驟 4: 過濾

高速流支持包括過濾器在內的所有流操作。假設我們只過濾那些超過60分鐘的電影。這可以通過向我們的應用程序添加這一行代碼來實現:

films.stream()
    .filter(Film.LENGTH.greaterThan(60))
    .forEach(System.out::println);

SQL:

SELECT
    `film_id`,`title`,`description`,`release_year`,
    `language_id`,`original_language_id`,`rental_duration`,`rental_rate`,
     `length`,`replacement_cost`,`rating`,`special_features`,
    `last_update`
FROM
    `sakila`.`film`
WHERE
    (`length` > ?),
 values:[60]

生成的輸出:

FilmImpl { filmId = 1, title = ACADEMY DINOSAUR, ..., length = 86, ... }
FilmImpl { filmId = 4, title = AFFAIR PREJUDICE, ..., length = 117, ...}
FilmImpl { filmId = 5, title = AFRICAN EGG, ... length = 130, ...}

過濾器可以結合創建更復雜的表達式如下所示:

films.stream()
    .filter(
        Film.LENGTH.greaterThan(60).or(Film.LENGTH.lessThan(30))
    )
    .forEach(System.out::println);

這將返回所有小於30分鐘或大於1小時的影片。檢查您的日誌文件,您將看到這個流也被呈現給SQL。

Step 5:控制順序

默認情況下,流中元素出現的順序是未定義的。要定義特定的順序,您可以對流應用一個sort()操作,如下所示:

films.stream()

    .filter(Film.LENGTH.greaterThan(60))
    .sorted(Film.TITLE)
    .forEach(System.out::println);

Rendered SQL:

SELECT
    `film_id`,`title`,`description`,`release_year`,
    `language_id`,`original_language_id`,`rental_duration`,`rental_rate`,
    `length`,`replacement_cost`,`rating`,`special_features`,
    `last_update`
FROM
    `sakila`.`film`
WHERE
    (`length` > ?)
ORDER BY
    `length` ASC,
values:[60]

輸出:

FilmImpl { filmId = 77, title = BIRDS PERDITION,..., length = 61,...}
FilmImpl { filmId = 106, title = BULWORTH COMMANDMENTS,..., length = 61,}
FilmImpl { filmId = 114, title = CAMELOT VACATION,..., length = 61,..}
...

您還可以組合多個排序器來定義主順序、次順序等等。

films.stream()
    .filter(Film.LENGTH.greaterThan(60))
    .sorted(Film.LENGTH.thenComparing(Film.TITLE.reversed()))
    .forEach(System.out::println);

將按長度順序(升序)和標題順序(降序)對電影元素進行排序。您可以組合任意數量的字段。

NB:如果要按升序組合兩個或多個字段,應該使用字段的method.comparator()。

I.e. sorted(Film.LENGTH.thenComparing(Film.TITLE.comparator())) rather than just sorted(Film.LENGTH.thenComparing(Film.TITLE))

步驟 6: 分頁和避免大對象塊

通常,人們希望對結果進行分頁,以避免使用不必要的大型對象塊。假設我們希望每頁看到50個記錄,我們可以編寫以下通用方法:

private static final int PAGE_SIZE = 50;
public static <T> Stream<T> page(
    Manager<T> manager,
    Predicate<? super T> predicate,
    Comparator<? super T> comparator,
    int pageNo
) {
    return manager.stream()
        .filter(predicate)
        .sorted(comparator)
        .skip(pageNo * PAGE_SIZE)
        .limit(PAGE_SIZE);
}

此實用程序方法可以使用任何過濾器來分頁任何表,並按任何順序對其進行排序。

例如,調用:

page(films, Film.LENGTH.greaterThan(60), Film.TITLE, 3)

將返回一個超過60分鐘的電影流,並按顯示第三頁的標題進行排序(跳過150部電影並顯示以下50部電影)。

Rendered SQL:

SELECT
    `film_id`,`title`,`description`,`release_year`,
    `language_id`,`original_language_id`,`rental_duration`,`rental_rate`,
    `length`,`replacement_cost`,`rating`,`special_features`,
    `last_update`
FROM
    `sakila`.`film`
WHERE
    (`length` > ?)
ORDER BY
     `title` ASC
LIMIT ? OFFSET ?,
values:[60, 50, 150]

Generated output:

FilmImpl { filmId = 165, title = COLDBLOODED DARLING, ... length = 70,...}
FilmImpl { filmId = 166, title = COLOR PHILADELPHIA, ..., length = 149... }
FilmImpl { filmId = 167, title = COMA HEAD, ... length = 109,...}
...

同樣,如果我們使用另一種數據庫類型,SQL代碼會略有不同。

步驟 7: In-JVM-Memory 加速

由於在初始化器中使用了標準配置,所以pom.xml中啓用了in - jvm -memory加速文件。要在應用程序中激活加速,只需修改初始化代碼如下:

SakilaApplication app = new SakilaApplicationBuilder()
    .withPassword("sakila-password")
    .withBundle(InMemoryBundle.class)
    .build();
    // Load data from the database into an in-memory snapshot
    app.getOrThrow(DataStoreComponent.class).load();

現在,表流將直接從RAM中提供,而不是呈現sql查詢。內存中的索引也會加速過濾、排序和跳過。內存中的表和索引都是堆外存儲的,因此它們不會增加垃圾收集的複雜性。

在我的筆記本電腦(Mac Pro,15英寸,2015年中期,16 GB,i7 2.2 GHz),查詢延遲降低了流的因素超過1000,我計算匹配過濾和排序的電影流相比,針對標準安裝運行的MySQL數據庫(版本5.7.16)在我的本地機器上運行。

總結

在本文中,您已經瞭解了使用純Java流查詢現有數據庫是多麼容易。您還看到了如何使用in-JVM-memory stream技術加速對數據的訪問。Sakila數據庫和Speedment都是免費下載和使用的,所以自己試試吧。

請關注公衆號:程序你好

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章