# 四、在应用中使用 Cassandra

到目前为止，我们主要关注 Cassandra 和 CQL。理解 Cassandra 是如何工作的，了解 CQL 对于能够在应用程序中使用 Cassandra 至关重要。Cassandra 拥有当今大多数流行语言的驱动程序。

表 6:Cassandra 驱动程序

| 语言 | 驾驶员 |
| Java 语言(一种计算机语言，尤用于创建网站) | [https://github.com/datastax/java-driver](https://github.com/datastax/java-driver) |
| C# | [https://github.com/datastax/csharp-driver](https://github.com/datastax/csharp-driver) |
| 计算机编程语言 | [https://github . com/data tax/python 驱动程序](https://github.com/datastax/python-driver) |
| Clojure | [https://github . com/clojurewerkz/保险箱](https://github.com/clojurewerkz/cassaforte) |
| 占线小时 | [https://github . com/iaaleksey/海星](https://github.com/iamaleksey/seestar) |
| 去 | [https://github . com/tux 21b/goc QL](https://github.com/tux21b/gocql) |
| 哈斯克尔 | [https://github . com/sostone/cassy](https://github.com/Soostone/cassy) |
| Node.js | [https://github . com/jorg ebay/node-Cassandra-cql](https://github.com/jorgebay/node-cassandra-cql) |
| Perl 语言 | [https://github . com/mkjemman/percaza](https://github.com/mkjellman/perlcassa) |
| 服务器端编程语言（Professional Hypertext Preprocessor 的缩写） | [http://github . com/thobbs/phpcasa](http://github.com/thobbs/phpcassa) |
| 红宝石 | [https://github . com/iconara/cql-Rb](https://github.com/iconara/cql-rb) |
| 斯卡拉 | [https://github . com/websudos/phantom](https://github.com/websudos/phantom) |

在本章中，我们将展示如何使用 Node.js、C# 和 Java 中的 Cassandra。本章的大部分内容将面向 Node.js 应用程序。

## 带 Node.js 的 Cassandra

Node.js 是当今非常流行的技术，目前被微软、VMWare、易贝、雅虎和许多其他公司使用。Node.js 变得流行的部分原因是大多数 web 开发人员不必学习一门新语言就可以开始使用它。有人把 Node.js 描述成服务器端的 JavaScript。JavaScript 有一个非常有趣的事件模型，非常适合异步编程。还有很多其他因素导致 Node.js 越来越受欢迎

一些重要的 Node.js 特性包括:

*   配置用 JSON 完成。
*   代码可以在客户端和服务器端重用。
*   充满活力的社区和非常容易的包共享。
*   简单可靠的包管理器。
*   简约轻巧的环境；用户只包括他们想要使用的内容。
*   支持该技术生产使用的几种工具。

为了完成这一部分，您将需要安装 Node.js。安装 Node.js 是一个简单明了且记录良好的过程；所有的信息都可以在[nodejs.org](http://nodejs.org/)找到。在本节中，我们将基于我们的二手车市场示例构建一个应用程序。提供的示例不会 100%生产就绪；它更倾向于展示如何从 Node.js 应用程序中与 Cassandra 交互。大多数时候，我们会忽略安全最佳实践和用户输入验证，但是我们会使用最常见和最流行的 Node.js 库来构建我们的示例。

### 初始设置

在做任何工作之前，我们必须确定存放源代码的目录。选择您喜欢的任何文件夹，但在命名文件夹和选择位置时始终避免使用空格，因为这可能会导致各种平台上出现不良行为。为了启动一切，我们需要创建一个名为“cass_node_used_cars”的空文件夹，然后将自己定位在这个文件夹中。我们要运行的第一个命令将是`npm init`；该命令创建`package.json`文件。该文件包含项目的所有元数据。要创建它，只需发出以下命令，并回答该命令将向您提出的问题。

```sql
    # npm init

```

代码清单 103

生成的`package.json`文件应该如下所示。

```sql
    {
        "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`文件，我们还需要创建额外的文件。

```sql
    .
    ├── 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`文件启动应用程序。目前，最好通过发出以下命令来安装所需的模块。

```sql
    # 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`部分。

```sql
      "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%针对示例应用程序定制的，我们使用它们来展示各种技术，因此我们需要再创建两个表。

```sql
    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 几乎没有提供额外的见解。在接下来的部分中，我们将仔细研究应用程序中的示例文件。

### index.js

```sql
    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`工具，然后用它运行示例，如下面的代码所示。

```sql
    # npm install -g nodemon
    # nodemon index.js

```

代码清单 110

用`nodemon`启动应用后，会跟踪变化，有变化时会自动重新加载应用。我们正在建立的例子还没有完全发挥作用。如果您在尝试运行 index.js 文件后收到错误消息，不要感到惊讶，如前面的清单所示。继续编辑文件。

### config.js

这个文件包含了 Express 和我们正在使用的其他框架的全部配置。我们还必须加载 Express 模块，以便定义静态目录。车把模块有更多的配置，我们需要做的。Handlebars 框架没有现成的日期格式化和字符串比较支持，所以我们需要定义`formatDate`和`if_eq`助手。对于日期格式化，我们将使用矩。Express 框架外部化了对解析帖子请求的支持，因此我们必须使用`body-parser`模块。我们还必须定义布局目录和配置视图引擎。所有这些变化都显示在下面的代码清单中。

```sql
    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。

### cassandra.js

```sql
    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.js

这是最复杂的文件，所以我们将分别检查每个函数。通常情况下，逻辑会被分成多个模块，但是在这个示例应用程序中，逻辑只是放在 routes 文件中。您可以跳转到视图源，并在定义路由逻辑时将它们添加到示例中。

```sql
    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`来做到这一点。

```sql
    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 查询没有抛出任何错误，我们将把客户端重定向到主视图。在主视图中，应用程序用户将拥有在应用程序中创建用户配置文件的按钮。创建用户配置文件的按钮将指向下面代码列表中的第一条路线。

```sql
    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`页面。

```sql
    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`路线。

```sql
    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 哲学中，磁盘很便宜，写入也很快。

```sql
    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`函数完成的，该函数将价格映射到一个简单的字符串。

现在，我们将进入搜索表单。首先，我们需要向用户显示搜索表单。

```sql
    app.get('/show/search', function (req, res) {
        res.render('search', {});
    });

```

代码清单 119

然后，搜索表单会将搜索参数发布到搜索路线。`search`路线将处理参数，然后对索引表进行`SELECT`。

```sql
    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`数组。最后一个承诺随后呈现带有加载的搜索结果的视图。搜索表单的每一次呈现都会传入发布的值，这样用户在提交表单后就不会丢失这些值。至此，我们已经完成了示例背后的逻辑。现在我们将进入演示部分。

### main.html

该文件用作生成所有其他视图的容器。这是包含所有页眉、页脚和导航脚本的最佳位置。为了让我们的例子更加引人注目，我们将使用 Twitter Bootstrap。包含起来很简单，使用起来也相对容易，但使示例看起来更好:

```sql
    <!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`标记之前。我们将继续其他观点。

### home.html

主屏幕将显示所有可用的优惠。如果没有报价，将显示一条警告消息。在主屏幕上，用户可以添加优惠、添加用户或开始搜索。

```sql
    {{#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

### add_user.html

以下模板用于添加用户。

```sql
    <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

### add_offer.html

以下模板用于添加报价。在我们的示例应用程序中，该表单的输入字段最多。

```sql
    <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

### search.html

这是我们示例中的最后一个模板。它使用自定义指令进行字符串比较，并使用视图逻辑显示搜索结果。

```sql
    <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}}>
                &lt; 1000</option>
            <option value="2" {{#if_eq price_range "2"}}selected{{/if_eq}}>
                &gt;= 1000 &lt; 3000</option>
            <option value="3" {{#if_eq price_range "3"}}selected{{/if_eq}}>
                &gt;= 3000 &lt; 5000</option>
            <option value="4" {{#if_eq price_range "4"}}selected{{/if_eq}}>
                &lt;= 5000 &lt; 10000</option>
            <option value="5" {{#if_eq price_range "5"}}selected{{/if_eq}}>
                &gt; 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

### 二手车市场在行动

下面的截图会让你知道应用程序最终应该是什么样子。

![](img/image055.png)

图 45:演示应用程序主屏幕

![](img/image056.jpg)

图 46:演示应用程序搜索屏幕

![](img/image057.jpg)

图 47:演示应用程序添加用户表单

## Cassandra 用 Java

Java 是一种非常成熟、稳定、经得起考验的技术。事实上，Cassandra 是用 Java 写的。一开始，大多数使用 Cassandra 的开发人员每天都在使用 Java。从早期开始，情况发生了很大的变化，但是，就 Cassandra 的平台而言，Java 仍然是一种非常重要的语言。

我们在 Node.js 一节中介绍的技术也适用于 Java。Java 有自己的一套框架；其中一些类似于 Express，但我们不会深入探讨。要使用 Java 中的 Cassandra，请使用您喜欢的 IDE，并确保将`com.datastax.driver`作为依赖项添加到项目中。在本节中，我们将构建一个 Java 客户端，其中列出了以前使用的`used_cars`键空间的所有报价。

```sql
    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 日 <sup>st</sup> 午夜以来常用的毫秒数。UUIDs 从 10 月 15 日午夜<sup>至</sup> 1582 以 100 纳秒的间隔测量。两个时代之间的转换在前面列出的`getTimeFromUUID`功能中完成。

## C# 的 Cassandra

C# 也是一种非常流行的技术。在使用驱动程序做任何工作之前，我们需要使用包管理器将它导入到我们的项目中。在标准的包管理器控制台窗口中键入`Install-Package CassandraCSharpDriver`就足够了。如果你在跑步。NET 在 Windows 以外的平台上，通过搜索“用于 Apache Cassandra 的 datatax C# 驱动程序”向项目中添加一个包。下面的 C# 示例将与 Java 示例相同，它将在控制台中显示输出。与 Java 示例一样，代码中最复杂的部分是从`GUID`类型中提取时间戳。其余的在以前的任何语言中都非常相似，并且比应用程序或计算机语言更具体。下面的清单显示了 C# 示例。

```sql
    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 提供额外的洞察力，因此在最后两个部分中，我们只研究了如何连接到集群、检索报价并将其打印到控制台。