到目前为止,我们主要关注 Cassandra 和 CQL。理解 Cassandra 是如何工作的,了解 CQL 对于能够在应用程序中使用 Cassandra 至关重要。Cassandra 拥有当今大多数流行语言的驱动程序。
表 6:Cassandra 驱动程序
| 语言 | 驾驶员 | | Java 语言(一种计算机语言,尤用于创建网站) | https://github.com/datastax/java-driver | | C# | https://github.com/datastax/csharp-driver | | 计算机编程语言 | https://github . com/data tax/python 驱动程序 | | Clojure | https://github . com/clojurewerkz/保险箱 | | 占线小时 | https://github . com/iaaleksey/海星 | | 去 | https://github . com/tux 21b/goc QL | | 哈斯克尔 | https://github . com/sostone/cassy | | Node.js | https://github . com/jorg ebay/node-Cassandra-cql | | Perl 语言 | https://github . com/mkjemman/percaza | | 服务器端编程语言(Professional Hypertext Preprocessor 的缩写) | http://github . com/thobbs/phpcasa | | 红宝石 | https://github . com/iconara/cql-Rb | | 斯卡拉 | https://github . com/websudos/phantom |
在本章中,我们将展示如何使用 Node.js、C# 和 Java 中的 Cassandra。本章的大部分内容将面向 Node.js 应用程序。
Node.js 是当今非常流行的技术,目前被微软、VMWare、易贝、雅虎和许多其他公司使用。Node.js 变得流行的部分原因是大多数 web 开发人员不必学习一门新语言就可以开始使用它。有人把 Node.js 描述成服务器端的 JavaScript。JavaScript 有一个非常有趣的事件模型,非常适合异步编程。还有很多其他因素导致 Node.js 越来越受欢迎
一些重要的 Node.js 特性包括:
- 配置用 JSON 完成。
- 代码可以在客户端和服务器端重用。
- 充满活力的社区和非常容易的包共享。
- 简单可靠的包管理器。
- 简约轻巧的环境;用户只包括他们想要使用的内容。
- 支持该技术生产使用的几种工具。
为了完成这一部分,您将需要安装 Node.js。安装 Node.js 是一个简单明了且记录良好的过程;所有的信息都可以在nodejs.org找到。在本节中,我们将基于我们的二手车市场示例构建一个应用程序。提供的示例不会 100%生产就绪;它更倾向于展示如何从 Node.js 应用程序中与 Cassandra 交互。大多数时候,我们会忽略安全最佳实践和用户输入验证,但是我们会使用最常见和最流行的 Node.js 库来构建我们的示例。
在做任何工作之前,我们必须确定存放源代码的目录。选择您喜欢的任何文件夹,但在命名文件夹和选择位置时始终避免使用空格,因为这可能会导致各种平台上出现不良行为。为了启动一切,我们需要创建一个名为“cass_node_used_cars”的空文件夹,然后将自己定位在这个文件夹中。我们要运行的第一个命令将是npm init
;该命令创建package.json
文件。该文件包含项目的所有元数据。要创建它,只需发出以下命令,并回答该命令将向您提出的问题。
# npm init
代码清单 103
生成的package.json
文件应该如下所示。
{
"name": "cass_node_used_cars",
"version": "0.0.1",
"description": "Cassandra used cars example in node.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Marko Švaljek (http//msvaljek.blogspot.com/)",
"license": "ISC"
}
代码清单 104
除了借助npm init
命令生成的package.json
文件,我们还需要创建额外的文件。
.
├── cassandra.js
├──config.js
├──index.js
├──node_modules
├──package.json
├──public
├──routes.js
└──views
├── add_offer.html
├── add_user.html
├── home.html
├── search.html
└── layouts
└── main.html
代码清单 105
节点模块目录将在后面的步骤中自动创建,因此您不必马上创建。下一步是安装库。为了与 Cassandra 互动,我们将使用一个名为node-cassandra-cql
的库。默认情况下,安装 Node.js CQL 库是为了与以前版本的 Cassandra 兼容。
在我们的演示应用程序中,我们将展示如何使用轻量级事务和批处理语句。要使用这些 Cassandra 功能,我们必须使用默认情况下不使用的v2
协议。要安装它,我们只需命令npm
安装模块的protocol12
版本。要使用特定的软件包,我们必须在安装模块时在@
符号后指定其名称。我们将安装模块并使用save
选项,以便npm
更新package.json
文件。公共文件夹包含公开可用的资源,如图像、JavaScript 和 CSS 文件。
请记住,每个人都可以看到这个文件夹。视图文件夹包含应用程序的模板文件。扩展名为.js
的文件包含应用程序的逻辑。稍后,我们将使用index.js
文件启动应用程序。目前,最好通过发出以下命令来安装所需的模块。
# npm install node-cassandra-cql@protocol2 -save
# npm install express –save
# npm install express3-handlebars –save
# npm install moment –save
# npm install body-parser -save
# npm install promise –save
代码清单 106
我们将为我们的应用程序构建一个最小的 web 界面。Node.js 最流行的 web 应用程序框架是 Express,所以我们将使用它。为了编写模板逻辑,我们将使用车把。为了处理日期,我们将使用“时刻”库。我们的应用程序将处理 HTTP POST
请求。在早期版本中,Express 开箱即用地处理这类请求,但现在我们不得不使用名为body-parser
的中间件组件。
我们还将演示一种在 Cassandra 中使用的搜索技术,它不需要在表上定义 Cassandra 索引,也不需要在 Cassandra 上使用某种搜索技术。这项技术将使用 Promise 库。运行之前的install
命令后,package.json
文件应该有一个dependencies
部分。
"dependencies": {
"node-cassandra-cql": "^0.4.4",
"express": "^4.8.2",
"express3-handlebars": "^0.5.2",
"moment": "^2.8.1",
"body-parser": "^1.6.2",
"promise": "^5.0.0"
}
代码清单 107
前面几章中的表不是 100%针对示例应用程序定制的,我们使用它们来展示各种技术,因此我们需要再创建两个表。
CREATE TABLE car_offers (
country text,
date timeuuid,
username text,
brand text,
color text,
equipment map<text, text>,
mileage int,
model text,
price float,
year int,
PRIMARY KEY (country, date)
) WITH CLUSTERING ORDER BY (date DESC);
CREATE TABLE car_offers_search_index (
brand text,
color text,
price_range text,
creation_time timeuuid,
date timeuuid,
country text,
PRIMARY KEY ((brand, color, price_range), creation_time)
);
代码清单 108
在本例中,我们将按照报价国家对数据进行划分。该应用程序将把国家固定为美国。如果我们增加更多的国家,只会增加复杂性,而不会使解释变得更容易。除了新的offers
表,我们将创建一个索引来搜索报价。构建一个更复杂的搜索索引也需要大量时间,并且对于在应用程序中使用 Cassandra 几乎没有提供额外的见解。在接下来的部分中,我们将仔细研究应用程序中的示例文件。
var express = require('express');
var app = express();
require('./config')(app);
require('./routes')(app);
app.listen(8080);
console.log('Application is running on port 8080');
代码清单 109
该模块初始化快速框架。express
初始化后,我们初始化config.js
和routes.js
模块。web 应用程序正在端口 8080 上运行。如果一切正常,你应该会看到console.log
的消息。如果您计划继续构建这个示例,最好我们不必仅仅为了查看某些更改是否生效而重新启动 Node 应用程序。最佳实践是安装nodemon
工具,然后用它运行示例,如下面的代码所示。
# npm install -g nodemon
# nodemon index.js
代码清单 110
用nodemon
启动应用后,会跟踪变化,有变化时会自动重新加载应用。我们正在建立的例子还没有完全发挥作用。如果您在尝试运行 index.js 文件后收到错误消息,不要感到惊讶,如前面的清单所示。继续编辑文件。
这个文件包含了 Express 和我们正在使用的其他框架的全部配置。我们还必须加载 Express 模块,以便定义静态目录。车把模块有更多的配置,我们需要做的。Handlebars 框架没有现成的日期格式化和字符串比较支持,所以我们需要定义formatDate
和if_eq
助手。对于日期格式化,我们将使用矩。Express 框架外部化了对解析帖子请求的支持,因此我们必须使用body-parser
模块。我们还必须定义布局目录和配置视图引擎。所有这些变化都显示在下面的代码清单中。
var express = require('express'),
bodyParser = require('body-parser'),
handlebars = require('express3-handlebars'),
moment = require('moment');
module.exports = function(app){
app.engine('html', handlebars({
defaultLayout: 'main',
extname: ".html",
layoutsDir: __dirname + '/views/layouts',
helpers: {
formatDate: function(value) {
return moment(value).format('YYYY-MM-DD HH:mm:ss');
},
if_eq : function(a, b, opts) {
if(a == b) {
return opts.fn(this);
}
else {
return opts.inverse(this);
}
}
}
}));
app.use(bodyParser.urlencoded({extended: false}));
app.set('view engine', 'html');
app.set('views', __dirname + '/views');
app.use(express.static(__dirname + '/public'));
};
代码清单 111
Express 支持 20 多种其他视图技术,其中最受欢迎的是 Jade、Haml 和下划线。在我们的例子中,我们将坚持使用普通的旧 HTML。
var cql = require('node-cassandra-cql');
var client = new cql.Client({
hosts: ['127.0.0.1:9042'], keyspace: 'used_cars'
});
module.exports = {
client: client,
cql: cql
};
代码清单 112
这个文件初始化了 Cassandra 驱动程序。hosts
值和keyspace
参数通常会从配置文件中加载,但是在这个示例应用程序中,直接在文件中加载就足够了。该模块导出client
以使其他模块能够查询 Cassandra 和cql
,从而使后面示例中的类型处理更加容易。
这是最复杂的文件,所以我们将分别检查每个函数。通常情况下,逻辑会被分成多个模块,但是在这个示例应用程序中,逻辑只是放在 routes 文件中。您可以跳转到视图源,并在定义路由逻辑时将它们添加到示例中。
var cassandra = require('./cassandra'),
client = cassandra.client,
cql = cassandra.cql,
Promise = require('promise');
module.exports = function (app) {
// all of the following methods go here
};
代码清单 113
现在,我们将从添加路由和定义路由逻辑开始。让我们定义主页的逻辑。主页应该立即显示所有的汽车优惠。请求将以get
形式出现,参数中没有指定的 URL 或页面。我们通过简单地为请求处理定义一个带有路由和回调的app.get
来做到这一点。
app.get('/', function (req, res) {
client.execute('SELECT dateOf(date) as date, username,\
brand, color, mileage, model, price, year \
FROM car_offers', [],
function(err, result) {
if (err) {
res.render('500');
} else {
res.render('home', {rows: result.rows});
}
});
});
代码清单 114
要显示主页,我们只需选择 Cassandra 的所有报价。要使用 Cassandra 客户端的execute
方法,我们必须提供以下参数:
- 查询字符串
- 因素
- 回调函数
我们使用的SELECT
查询返回日期的timeuuid
类型。为了简化数据处理,我们在查询中直接将timeuuid
转换为date
。如果 Cassandra 查询没有抛出任何错误,我们将把客户端重定向到主视图。在主视图中,应用程序用户将拥有在应用程序中创建用户配置文件的按钮。创建用户配置文件的按钮将指向下面代码列表中的第一条路线。
app.get('/show/adduser', function (req, res) {
res.render('add_user', {});
});
app.post('/adduser', function (req, res) {
client.execute('INSERT INTO users (\
username, emails, first_name, last_name,\
password, state)\
VALUES (?, null, ?, ?, null, null) IF NOT EXISTS',
[req.param('username'), req.param('first_name'),
req.param('last_name')],
function(err) {
if (err) {
res.render('add_user', {
username: req.param('username'),
first_name: req.param('first_name'),
last_name: req.param('last_name'),
errorOccured: true
});
} else {
res.redirect('/');
}
});
});
代码清单 115
显示add_user
视图比较简单;我们只需要展示视图,不需要为它准备任何数据。当来自视图的表单向adduser
路由发送一个帖子请求时,我们从帖子中提取参数,并填写参数以插入到users
表中。我们不会使用示例中的所有字段,因此我们只需在查询中将它们设置为null
。我们还使用了与IF NOT EXISTS
的轻量级事务,因此永远不会创建两个具有相同用户名的用户。
这是一个简洁的 Cassandra 特性,但是要使用它,我们必须安装protocol12
版本的驱动程序。如果出现错误,我们会再次显示add_user
页面,但这一次我们传入了参数,这样用户就不会丢失表单中的值,我们还为视图提供了错误标志,以便向用户显示错误消息。如果INSERT
上没有错误,我们只需将用户重定向回主页。
现在我们已经将用户添加到应用程序中,我们想为他们创建优惠。优惠表单将在选择框中显示users
列表,因此在显示add_offer
页面之前,我们需要从 Cassandra 获取用户。SELECT
的说法很简单;我们只是列出我们需要的列。请注意,我们只使用用户名以及名和姓。如果我们无法从 Cassandra 获取用户,我们将简单地呈现一个内部错误页面。如果我们得到用户,我们将渲染add_offer
页面。
app.get('/show/addoffer', function (req, res) {
client.execute('SELECT username, first_name, last_name \
FROM users', [],
function(err, users) {
if (err) {
res.render('500');
} else {
res.render('add_offer', {users: users.rows});
}
});
});
代码清单 116
当用户在add_offer
视图中完成提供详细信息后,表单将被发布到addoffer
路线。
app.post('/addoffer', function (req, res) {
var offer_timeuuid = cql.types.timeuuid();
client.execute('INSERT INTO car_offers (\
country, date, brand, color, equipment,\
mileage, model, price, username, year)\
VALUES (?, ?, ?, ?, null, ?, ?, ?, ?, ?)',
[req.param('country'),
offer_timeuuid,
req.param('brand'),
req.param('color'),
parseInt(req.param('mileage'), 10),
req.param('model'),
{
value: parseFloat(req.param('price')),
hint: cql.types.dataTypes.float
},
req.param('username'),
parseInt(req.param('year'), 10)],
function(err) {
if (err) {
res.render('add_offer', {
brand: req.param('brand'),
color: req.param('color'),
mileage: req.param('mileage'),
model: req.param('model'),
price: req.param('price'),
username: req.param('username'),
year: req.param('year'),
errorOccured: true
});
} else {
makeSearchIndexEntries(
req.param('brand'),
req.param('color'),
parseFloat(req.param('price')),
offer_timeuuid, req.param('country'));
res.redirect('/');
}
});
});
代码清单 117
我们没有让 Cassandra 在now
函数的帮助下自动生成timeuuid
日期字段,因为我们需要这个值来创建搜索索引条目。相反,我们在应用程序端用timeuuid()
函数生成了一个。参数作为字符串发送到服务器。大多数情况下,这很好,但是有些参数必须转换成其他类型。我们使用了parseInt
和parseFloat
JavaScript 函数来做到这一点。如果我们简单地将解析后的浮点数传递给参数,驱动程序会插入一些非常奇怪的值,这些值有很大的负指数。所以我们把价格作为一个有两个字段的对象插入。第一个是value
,第二个是hint
,我们在里面对司机说这是一个float
值。然后,驱动程序表现良好,正常插入浮动。
如果INSERT
语句抛出错误,我们返回到add_offer
页面并传播发布的数据,这样用户就不会丢失输入的值。如果一切都如预期的那样运行,我们会创建索引搜索条目并返回到主页。我们为该领域的品牌、颜色和价格范围建立了搜索索引。我们将要构建的搜索索引不需要对 Cassandra 的安装进行任何修改,好的一面是我们可以对其进行备份,它将在集群中的节点之间进行分发。这种搜索索引的缺点是,它会生成大量对 Cassandra 的写入,但这没什么,因为在 Cassandra 哲学中,磁盘很便宜,写入也很快。
function makeSearchIndexEntries(brand, color, price, date, country) {
var insertQuery = 'INSERT INTO car_offers_search_index\
(brand, color, price_range, creation_time, date, country)\
VALUES (?, ?, ?, now(), ?, ?)';
brand = brand.toLowerCase();
color = color.toLowerCase();
var price_range = convertPriceToRange(price);
var queries = [
{query: insertQuery, params: ['', '', '', date, country]},
{query: insertQuery, params: ['', '', price_range, date, country]},
{query: insertQuery, params: ['', color, '', date, country]},
{query: insertQuery, params: ['', color, price_range, date, country]},
{query: insertQuery, params: [brand, '', '', date, country]},
{query: insertQuery, params: [brand, '', price_range, date, country]},
{query: insertQuery, params: [brand, color, '', date, country]},
{query: insertQuery, params: [brand, color, price_range, date, country]}
];
var consistency = cql.types.consistencies.one;
client.executeBatch(queries, consistency, function(err) {
if (err) {
console.log('error inserting ');
}
});
}
function convertPriceToRange(price) {
if (price > 0) {
if (price < 1000) {
return '1';
}
if (price < 3000) {
return '2';
}
if (price < 5000) {
return '3';
}
if (price < 10000) {
return '4';
}
return '5';
}
return '0';
}
代码清单 118
为了尽可能快地建立索引,我们将使用批处理语句。executeBatch
在驾驶员的version12
中可用。batch 语句也将使插入值变得更加容易,因为我们需要插入报价的所有可能组合。此外,brand
和color
值总是以小写形式保存和查询,这样我们就不必担心由于不正确的大小写而导致搜索结果不显示。
在每个语句中,date
和country
参数总是被插入。brand
、color
和price_range
以各种可能的方式结合在一起。为了简单起见,我们使用了三个变量,得到了八个INSERT
语句,这对于演示目的来说已经足够了。将法定级别设置为one
也加快了速度,因为它只需要一个节点的确认就可以继续。价格不直接存储在索引中。相反,我们使用一个价格范围。建立价格范围是通过convertPriceToRange
函数完成的,该函数将价格映射到一个简单的字符串。
现在,我们将进入搜索表单。首先,我们需要向用户显示搜索表单。
app.get('/show/search', function (req, res) {
res.render('search', {});
});
代码清单 119
然后,搜索表单会将搜索参数发布到搜索路线。search
路线将处理参数,然后对索引表进行SELECT
。
app.post('/search', function (req, res) {
var accumulator = [];
var brand = req.param('brand').toLowerCase();
var color = req.param('color').toLowerCase();
var price_range = req.param('price_range');
client.execute('SELECT country, date \
FROM car_offers_search_index \
WHERE brand=? AND color=? AND price_range = ?',
[brand, color, price_range],
function(err, result) {
if (!err) {
var prevPromise = Promise.resolve();
result.rows.forEach(function (row) {
prevPromise = prevPromise.then(function () {
return new Promise(function (resolve, reject) {
client.execute(
'SELECT dateOf(date) as date, username,\
brand, color, mileage, model, price, year \
FROM car_offers \
WHERE country = ? AND date = ?',
[row.get('country'), row.get('date')],
function(err, result) {
resolve(result.rows[0]);
});
});
}).then(function (value) {
accumulator.push(value);
});
});
prevPromise.then(function () {
res.render('search', {
brand: req.param('brand'),
color: req.param('color'),
price_range: req.param('price_range'),
results: accumulator
});
});
}
else {
res.render('search', {
brand: req.param('brand'),
color: req.param('color'),
price_range: req.param('price_range')
});
}
});
});
代码清单 120
查询搜索索引将返回名为date
的country
字段和timeuuid
字段列表。两者结合,我们可以唯一识别offers
表中存储的所有数据。Node.js 本质上是一个非常异步的环境,但是有时我们希望事情按顺序发生,或者只是等待某件事情完成,然后再继续其他任务。我们在前面模块中的问题是从offers
表中加载每个搜索结果的数据。这对于 Node.js 本身来说是比较困难的,所以我们使用 Promise 库来实现。对于结果中的每一行,我们都会创造一个新的承诺。然后这些承诺被prevPromise
变量所链接。每个承诺都会查询offers
表并获取结果。当承诺完成获取结果时,它将结果推送到accumulator
数组。最后一个承诺随后呈现带有加载的搜索结果的视图。搜索表单的每一次呈现都会传入发布的值,这样用户在提交表单后就不会丢失这些值。至此,我们已经完成了示例背后的逻辑。现在我们将进入演示部分。
该文件用作生成所有其他视图的容器。这是包含所有页眉、页脚和导航脚本的最佳位置。为了让我们的例子更加引人注目,我们将使用 Twitter Bootstrap。包含起来很简单,使用起来也相对容易,但使示例看起来更好:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Used Cars</title>
<link rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
</head>
<body>
<div class="container">
<h1>Used Cars</h1>
{{{body}}}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
</body>
</html>
代码清单 121
所有其他视图都将在前面的代码清单中的{{{body}}}
标记内呈现。大多数情况下,脚本是加载时间最长的,所以最好将脚本放在结束body
标记之前。我们将继续其他观点。
主屏幕将显示所有可用的优惠。如果没有报价,将显示一条警告消息。在主屏幕上,用户可以添加优惠、添加用户或开始搜索。
{{#if rows}}
<table class="table table-striped">
<thead>
<tr>
<td>date</td>
<td>username</td>
<td>brand</td>
<td>color</td>
<td>mileage</td>
<td>model</td>
<td>price</td>
<td>year</td>
</tr>
</thead>
<tbody>
{{#each rows}}
<tr>
<td>{{formatDate date}}</td>
<td>{{username}}</td>
<td>{{brand}}</td>
<td>{{color}}</td>
<td>{{mileage}}</td>
<td>{{model}}</td>
<td>{{price}}</td>
<td>{{year}}</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<div class="alert alert-warning" role="alert">
<strong>Warning!</strong>
There are no car offers at the moment
</div>
{{/if}}
<p>
<a href="./show/addoffer" class="btn btn-primary">Add Offer</a>
<a href="./show/adduser" class="btn btn-info">Add User</a>
<a href="./show/search" class="btn btn-success">Search</a>
</p>
代码清单 122
以下模板用于添加用户。
<p class="text-center bg-info">Add User</p>
{{#if errorOccured}}
<div class="alert alert-danger" role="alert">
<strong>Warning!</strong> An error occured.
</div>
{{/if}}
<form class="form-horizontal" role="form" action="/adduser" method="post">
<div class="form-group">
<label class="col-sm-2 control-label">username</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="username"
value="{{username}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">first_name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="first_name"
value="{{first_name}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">last_name</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="last_name"
value="{{last_name}}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-info">Add</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
代码清单 123
以下模板用于添加报价。在我们的示例应用程序中,该表单的输入字段最多。
<p class="text-center bg-primary">Add Offer</p>
{{#if errorOccured}}
<div class="alert alert-danger" role="alert">
<strong>Warning!</strong> An error occured.
</div>
{{/if}}
<form class="form-horizontal" role="form" action="/addoffer" method="post">
<input type="hidden" name="country" value="USA">
{{#if username}}
<input type="hidden" name="username" value="{{username}}">
{{else}}
<div class="form-group">
<label class="col-sm-2 control-label">username</label>
<div class="col-sm-10">
<select name="username" class="form-control">
{{#each users}}
<option value="{{username}}">
{{first_name}} {{last_name}}
</option>
{{/each}}
</select>
</div>
</div>
{{/if}}
<div class="form-group">
<label class="col-sm-2 control-label">brand</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="brand"
value="{{brand}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">color</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="color"
value="{{color}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">mileage</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="mileage"
value="{{mileage}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">model</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="model"
value="{{model}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">price</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="price"
value="{{price}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">year</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="year"
value="{{year}}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">Add</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
代码清单 124
这是我们示例中的最后一个模板。它使用自定义指令进行字符串比较,并使用视图逻辑显示搜索结果。
<p class="text-center bg-success">Search Cars</p>
<form class="form-horizontal" role="form" action="/search" method="post">
<div class="form-group">
<label class="col-sm-2 control-label">brand</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="brand"
value="{{brand}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">color</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="color"
value="{{color}}">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">price_range</label>
<div class="col-sm-10">
<select name="price_range" class="form-control">
<option value=""></option>
<option value="1" {{#if_eq price_range "1"}}selected{{/if_eq}}>
< 1000</option>
<option value="2" {{#if_eq price_range "2"}}selected{{/if_eq}}>
>= 1000 < 3000</option>
<option value="3" {{#if_eq price_range "3"}}selected{{/if_eq}}>
>= 3000 < 5000</option>
<option value="4" {{#if_eq price_range "4"}}selected{{/if_eq}}>
<= 5000 < 10000</option>
<option value="5" {{#if_eq price_range "5"}}selected{{/if_eq}}>
> 10000</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-success">Search</button>
<a href="/" class="btn btn-default">Back</a>
</div>
</div>
</form>
{{#if results}}
<table class="table table-striped">
<thead>
<tr>
<td>date</td>
<td>username</td>
<td>brand</td>
<td>color</td>
<td>mileage</td>
<td>model</td>
<td>price</td>
<td>year</td>
</tr>
</thead>
<tbody>
{{#each results}}
<tr>
<td>{{formatDate date}}</td>
<td>{{username}}</td>
<td>{{brand}}</td>
<td>{{color}}</td>
<td>{{mileage}}</td>
<td>{{model}}</td>
<td>{{price}}</td>
<td>{{year}}</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<div class="alert alert-success" role="success">
There are no search results
</div>
{{/if}}
代码清单 125
下面的截图会让你知道应用程序最终应该是什么样子。
图 45:演示应用程序主屏幕
图 46:演示应用程序搜索屏幕
图 47:演示应用程序添加用户表单
Java 是一种非常成熟、稳定、经得起考验的技术。事实上,Cassandra 是用 Java 写的。一开始,大多数使用 Cassandra 的开发人员每天都在使用 Java。从早期开始,情况发生了很大的变化,但是,就 Cassandra 的平台而言,Java 仍然是一种非常重要的语言。
我们在 Node.js 一节中介绍的技术也适用于 Java。Java 有自己的一套框架;其中一些类似于 Express,但我们不会深入探讨。要使用 Java 中的 Cassandra,请使用您喜欢的 IDE,并确保将com.datastax.driver
作为依赖项添加到项目中。在本节中,我们将构建一个 Java 客户端,其中列出了以前使用的used_cars
键空间的所有报价。
package com.cassandra.example;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
public class CassandraExample {
public static void main(String[] args) {
String host = "127.0.0.1";
Cluster cluster = Cluster.builder().addContactPoint(host).build();
Session session = cluster.connect("used_cars");
PreparedStatement statement = session
.prepare("SELECT * FROM car_offers WHERE country = ?");
BoundStatement boundStatement = statement.bind("USA");
ResultSet results = session.execute(boundStatement);
System.out.println(String.format(
"%-23s\t%-15s\t%-7s\t%-7s\t%-7s\t%-7s\t%-10s\t%-7s", "date",
"brand", "color", "mileage", "model", "price", "username",
"year"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
for (Row row : results.all()) {
UUID dateUUID = row.getUUID("date");
Date date = new Date(getTimeFromUUID(dateUUID));
String brand = row.getString("brand");
String color = row.getString("color");
Integer mileage = row.getInt("mileage");
String model = row.getString("model");
Float price = row.getFloat("price");
String username = row.getString("username");
Integer year = row.getInt("year");
System.out.println(String.format(
"%-23s\t%-15s\t%-7s\t%-7s\t%-7s\t%-7s\t%-10s\t%-7s",
sdf.format(date), brand, color, mileage, model, price,
username, year));
}
session.close();
cluster.close();
}
static final long INTERVALS_SINCE_UUID_EPOCH_100NS = 0x01b21dd213814000L;
public static long getTimeFromUUID(UUID uuid) {
return (uuid.timestamp() - INTERVALS_SINCE_UUID_EPOCH_100NS) / 10000;
}
}
代码清单 126
前面列出的模块依赖于 DataStax Cassandra 驱动程序。如果您打算在没有任何依赖管理系统的情况下尝试该示例,请注意驱动程序需要运行sl4j-api, sl4j-simple
、com.google.gson
、guava
、jboss
、metrics
模块。
这个例子相对容易理解。唯一棘手的部分是从timeUUID
列中提取时间戳。最常见的错误是将timestamp
粘贴到Date
构造函数中。在获取时间戳之前,我们需要对其进行调整,以匹配自 1970 年 1 月 1 日 st 午夜以来常用的毫秒数。UUIDs 从 10 月 15 日午夜至 1582 以 100 纳秒的间隔测量。两个时代之间的转换在前面列出的getTimeFromUUID
功能中完成。
C# 也是一种非常流行的技术。在使用驱动程序做任何工作之前,我们需要使用包管理器将它导入到我们的项目中。在标准的包管理器控制台窗口中键入Install-Package CassandraCSharpDriver
就足够了。如果你在跑步。NET 在 Windows 以外的平台上,通过搜索“用于 Apache Cassandra 的 datatax C# 驱动程序”向项目中添加一个包。下面的 C# 示例将与 Java 示例相同,它将在控制台中显示输出。与 Java 示例一样,代码中最复杂的部分是从GUID
类型中提取时间戳。其余的在以前的任何语言中都非常相似,并且比应用程序或计算机语言更具体。下面的清单显示了 C# 示例。
using Cassandra;
using System;
namespace CassandraExample
{
class ExampleApp
{
private Cluster cluster;
private ISession session;
public void Connect (String node)
{
cluster = Cluster.Builder ().AddContactPoint (node).Build ();
session = cluster.Connect ("used_cars");
}
public void PrinResults ()
{
RowSet results = session.Execute ("SELECT * FROM car_offers");
Console.WriteLine (String.Format (
"{0, -23}\t{1, -15}\t{2, -7}\t{3, -7}\t{4, -7}\t{5, -7}\t{6, -10}\t{7, -7}",
"date", "brand", "color", "mileage", "model", "price", "username", "year"));
foreach (Row row in results.GetRows()) {
Guid date = new Guid (row ["date"].ToString ());
Console.WriteLine (String.Format (
"{0, -23}\t{1, -15}\t{2, -7}\t{3, -7}\t{4, -7}\t{5, -7}\t{6, -10}\t{7, -7}",
GetDateTime (date), row ["brand"], row ["color"], row ["mileage"], row ["model"], row ["price"], row ["username"], row ["year"]));
}
}
public void Close ()
{
cluster.Shutdown ();
}
static void Main (string[] args)
{
ExampleApp client = new ExampleApp ();
client.Connect ("127.0.0.1");
client.PrinResults ();
client.Close ();
}
private const int VersionByte = 7;
private const int VersionByteMask = 0x0f;
private const int VersionByteShift = 4;
private const byte TimestampByte = 0;
private static readonly DateTimeOffset GregorianCalendarStart =
new DateTimeOffset (1582, 10, 15, 0, 0, 0, TimeSpan.Zero);
private static DateTimeOffset GetDateTimeOffset (Guid guid)
{
byte[] bytes = guid.ToByteArray ();
bytes [VersionByte] &= (byte)VersionByteMask;
bytes [VersionByte] |= (byte)((byte)0x01 >> VersionByteShift);
byte[] timestampBytes = new byte[8];
Array.Copy (bytes, TimestampByte, timestampBytes, 0, 8);
long timestamp = BitConverter.ToInt64 (timestampBytes, 0);
long ticks = timestamp + GregorianCalendarStart.Ticks;
return new DateTimeOffset (ticks, TimeSpan.Zero);
}
private static DateTime GetDateTime (Guid guid)
{
return GetDateTimeOffset (guid).DateTime;
}
}
}
代码清单 127
在本章中,我们已经展示了如何在当今最流行的开发环境中使用 Cassandra。这一章的大部分内容都是关于 Node.js 的,在这里我们看到了如何构建一个完全依赖 Cassandra 的 web 应用程序。完整的应用程序侧重于作为底层数据平台的 Cassandra,而不是使用它的应用程序。对 Java 和 C# 重复相同的应用程序不会为 Cassandra 提供额外的洞察力,因此在最后两个部分中,我们只研究了如何连接到集群、检索报价并将其打印到控制台。