教程:构建一个简单的Rust应用程序并将其连接到ScyllaDB NoSQL上

399 阅读5分钟

教程:构建一个简单的Rust应用程序并将其连接到ScyllaDB NoSQL上

学习如何建立一个简单的Rust应用程序,将其连接到NoSQL数据库集群并执行基本查询。

ScyllaDB是一个开源数据库,用于需要高性能和低延迟的数据密集型应用,与Rust非常匹配。与Rust编程语言Tokio框架类似,ScyllaDB建立在一个异步、非阻塞的运行时间上,对于构建高度可靠的低延迟分布式应用非常有效。

ScyllaDB团队已经开发了scylla-rust-driver,这是一个用于Rust的开源ScyllaDB(和Apache Cassandra)驱动程序。它是用纯Rust编写的,使用Tokio的完全异步的API。你可以阅读更多关于它的基准测试结果,以及我们的开发人员如何解决性能退步的问题。

我们最近开发了一个新的免费培训课程,使用新驱动与ScyllaDB集群进行交互。在这篇文章中,我将介绍该课程的基本内容,你将建立一个简单的Rust应用程序,连接到ScyllaDB集群并执行基本查询。

为什么是Rust?

在我们深入学习新的Rust课程之前,让我们先解决一个明显的问题:为什么是Rust?

Rust是一种现代的、性能良好的语言,它正在受到欢迎并得到更广泛的使用。它是一种系统编程语言。然而,你几乎可以用它开发任何东西。它是为了快速安全地运行,防止大多数崩溃,因为所有的内存访问都被检查过。它还消除了数据竞赛。

此外,Rust还实现了一个独特而有趣的异步模型。也就是说,Rust的futures代表了计算,而推进这些异步计算的责任属于程序员。这允许以一种非常有效的方式创建异步程序,最大限度地减少对分配的需求,因为Rust的异步函数所代表的状态机在编译时是已知的。

现在,进入新的Rust课程...

创建数据模式

本课的Rust示例程序将能够存储和查询温度时间序列数据。每个测量值都将包含以下信息。

  • 测量温度的传感器的传感器ID
  • 测量温度的时间
  • 温度值

首先,创建一个名为tutorial 的键空间。

Rust

CREATE KEYSPACE IF NOT EXISTS tutorial
  WITH REPLICATION = {
    'class': 'SimpleStrategy',
    'replication_factor': 1
};

基于所需的查询是特定设备在给定时间间隔内报告的温度,创建以下表格。

Rust

CREATE TABLE IF NOT EXISTS tutorial.temperature (
  device UUID,
  time timestamp,
  temperature smallint,
  PRIMARY KEY(device, time)
);

你要建立的应用程序将能够查询特定设备在选定时间范围内测量的所有温度。这就是为什么你将使用以下SELECT 查询。

Rust

SELECT * FROM tutorial.temperature
WHERE device = ?
AND time > ?
AND time < ?;

其中? 将被替换为实际值:设备ID、时间从和时间到,分别为。

用Rust连接到数据库

应用程序的名称是温度,所需的依赖项在Cargo.toml文件中定义。

Rust

uuid = {version = "0.8", features = ["v4"]}
tokio = {version = "1.1.0", features = ["full"]}
scylla = "0.3.1"
futures = "0.3.6"
chrono = "0.4.0"

其中:

  • uuid- 提供UUID的包。
  • tokio- 提供异步运行时间,在其中执行数据库查询。
  • scylla- Rust ScyllaDB/Casandra驱动。
  • chrono- 用于处理时间的包。

main 函数通过使用tokio ,以异步方式工作。下面的内容可以确保它返回结果。

Rust

#[tokio::main]
async fn main() -> Result<()> {
...
}

文件/src/db.rs ,将保存与ScyllaDB实例一起工作的逻辑。第一步是建立一个数据库会话。

Rust

use scylla::{Session, SessionBuilder};

use crate::Result;

pub async fn create_session(uri: &str) -> Result<Session> {
  SessionBuilder::new()
    .known_node(uri)
    .build()
    .await
    .map_err(From::from)
}

来初始化会话。

Rust

#[tokio::main]
async fn main() -> Result<()> {
  println!("connecting to db");
  let uri = std::env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());
  let session = db::create_session(&uri).await?;
  todo!()
}

注意在create_session 后面的.await 。这是因为async 函数返回一个Future。期货可以被await-ed在其他async 函数中,以获得其实际值,在本例中是Result<Session, Error> 。最后,在await 后面的? ,我们要确保如果我们从create_session 得到一个错误而不是一个会话,这个错误将被向上传播,并且应用程序将终止,打印出错误。

接下来,文件/src/db.rs ,定义了创建键空间和表以存储温度测量的函数。你将使用查询来创建键空间和表。

Rust

use scylla::{IntoTypedRows, Session, SessionBuilder};
use uuid::Uuid;

use crate::{Duration, Result, TemperatureMeasurement};

static CREATE_KEYSPACE_QUERY: &str = r#"
  CREATE KEYSPACE IF NOT EXISTS tutorial
  WITH REPLICATION = {
    'class': 'SimpleStrategy',
    'replication_factor': 1
  };
"#;

static CREATE_TEMPERATURE_TABLE_QUERY: &str = r#"
  CREATE TABLE IF NOT EXISTS tutorial.temperature (
    device UUID,
    time timestamp,
    temperature smallint,
    PRIMARY KEY(device, time)
  );
"#;

pub async fn initialize(session: &Session) -> Result<()> {
  create_keyspace(session).await?;
  create_temperature_table(session).await?;
  Ok(())
}

async fn create_keyspace(session: &Session) -> Result<()> {
  session
    .query(CREATE_KEYSPACE_QUERY, ())
    .await
    .map(|_| ())
    .map_err(From::from)
}

async fn create_temperature_table(session: &Session) -> Result<()> {
  session
    .query(CREATE_TEMPERATURE_TABLE_QUERY, ())
    .await
    .map(|_| ())
    .map_err(From::from)
}

文件/src/db.rs ,定义了插入查询。ScyllaDB将使用每个值作为替换?

Rust

static ADD_MEASUREMENT_QUERY: &str = r#"
INSERT INTO tutorial.temperature (device, time, temperature)
VALUES (?, ?, ?);
"#;

pub async fn add_measurement(session: &Session, measurement: TemperatureMeasurement) -> Result<()> {
  session
    .query(ADD_MEASUREMENT_QUERY, measurement)
    .await
    .map(|_| ())
    .map_err(From::from)
}

读取测量值

接下来,选择查询逻辑被定义在/src/db.rs 模块中。

Rust

static SELECT_MEASUREMENTS_QUERY: &str = r#"
  SELECT * FROM fast_logger.temperature
    WHERE device = ?
      AND time > ?
      AND time < ?;
"#;

pub async fn select_measurements(
  session: &Session,
  device: Uuid,
  time_from: Duration,
  time_to: Duration,
) -> Result<Vec<TemperatureMeasurement>> {
  session
    .query(SELECT_MEASUREMENTS_QUERY, (device, time_from, time_to))
    .await?
    .rows
    .unwrap_or_default()
    .into_typed::<TemperatureMeasurement>()
    .map(|v| v.map_err(From::from))
    .collect()
}

重要的步骤是:

  • 用指定的参数(设备ID,开始和结束日期)做一个选择查询。
  • 等待响应并将其转换为行。
  • 行可能是空的。unwrap_or_default ,确保在这种情况下你会得到一个空的Vec
  • 一旦获得了行,通过使用into_typed::<TemperatureMeasurement>() 来转换每一行,这将使用FromRow 衍生宏。
  • 由于into_typed 返回一个Result ,这意味着转换每个结果都可能失败。通过.map(|v| v.map_err(From::from)) ,你可以确保每一行的错误都将被转换为/src/result.rs 中定义的通用错误。
  • 最后,collect 将迭代的值保存到一个向量中。

现在,回到/src/main.rs ,你可以看到main 的其余函数、导入和模块。

Rust

use uuid::Uuid;

use crate::duration::Duration;
use crate::result::Result;
use crate::temperature_measurement::TemperatureMeasurement;

mod db;
mod duration;
mod result;
mod temperature_measurement;

#[tokio::main]
async fn main() -> Result<()> {
  println!("connecting to db");
  let uri = std::env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());
  let session = db::create_session(&uri).await?;
  db::initialize(&session).await?;

  println!("Adding measurements");
  let measurement = TemperatureMeasurement {
    device: Uuid::parse_str("72f6d49c-76ea-44b6-b1bb-9186704785db")?,
    time: Duration::seconds(1000000000001),
    temperature: 40,
  };
  db::add_measurement(&session, measurement).await?;

  let measurement = TemperatureMeasurement {
    device: Uuid::parse_str("72f6d49c-76ea-44b6-b1bb-9186704785db")?,
    time: Duration::seconds(1000000000003),
    temperature: 60,
  };
  db::add_measurement(&session, measurement).await?;

  println!("Selecting measurements");
  let measurements = db::select_measurements(
    &session,
    Uuid::parse_str("72f6d49c-76ea-44b6-b1bb-9186704785db")?,
    Duration::seconds(1000000000000),
    Duration::seconds(10000000000009),
  )
  .await?;
  println!("     >> Measurements: {:?}", measurements);

  Ok(())

}