Skip to content

Files

Latest commit

c70125a · Jan 8, 2022

History

History
1018 lines (812 loc) · 43 KB

4.md

File metadata and controls

1018 lines (812 loc) · 43 KB

四、在应用中使用 Cassandra

到目前为止,我们主要关注 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 的 Cassandra

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

index.js

    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.jsroutes.js模块。web 应用程序正在端口 8080 上运行。如果一切正常,你应该会看到console.log的消息。如果您计划继续构建这个示例,最好我们不必仅仅为了查看某些更改是否生效而重新启动 Node 应用程序。最佳实践是安装nodemon工具,然后用它运行示例,如下面的代码所示。

    # npm install -g nodemon
    # nodemon index.js

代码清单 110

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

config.js

这个文件包含了 Express 和我们正在使用的其他框架的全部配置。我们还必须加载 Express 模块,以便定义静态目录。车把模块有更多的配置,我们需要做的。Handlebars 框架没有现成的日期格式化和字符串比较支持,所以我们需要定义formatDateif_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。

cassandra.js

    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 文件中。您可以跳转到视图源,并在定义路由逻辑时将它们添加到示例中。

    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()函数生成了一个。参数作为字符串发送到服务器。大多数情况下,这很好,但是有些参数必须转换成其他类型。我们使用了parseIntparseFloat 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 语句也将使插入值变得更加容易,因为我们需要插入报价的所有可能组合。此外,brandcolor值总是以小写形式保存和查询,这样我们就不必担心由于不正确的大小写而导致搜索结果不显示。

在每个语句中,datecountry参数总是被插入。brandcolorprice_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

查询搜索索引将返回名为datecountry字段和timeuuid字段列表。两者结合,我们可以唯一识别offers表中存储的所有数据。Node.js 本质上是一个非常异步的环境,但是有时我们希望事情按顺序发生,或者只是等待某件事情完成,然后再继续其他任务。我们在前面模块中的问题是从offers表中加载每个搜索结果的数据。这对于 Node.js 本身来说是比较困难的,所以我们使用 Promise 库来实现。对于结果中的每一行,我们都会创造一个新的承诺。然后这些承诺被prevPromise变量所链接。每个承诺都会查询offers表并获取结果。当承诺完成获取结果时,它将结果推送到accumulator数组。最后一个承诺随后呈现带有加载的搜索结果的视图。搜索表单的每一次呈现都会传入发布的值,这样用户在提交表单后就不会丢失这些值。至此,我们已经完成了示例背后的逻辑。现在我们将进入演示部分。

main.html

该文件用作生成所有其他视图的容器。这是包含所有页眉、页脚和导航脚本的最佳位置。为了让我们的例子更加引人注目,我们将使用 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标记之前。我们将继续其他观点。

home.html

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

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

以下模板用于添加用户。

    <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

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

    <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

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

    <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

二手车市场在行动

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

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

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

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

Cassandra 用 Java

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-simplecom.google.gsonguavajbossmetrics模块。

这个例子相对容易理解。唯一棘手的部分是从timeUUID列中提取时间戳。最常见的错误是将timestamp粘贴到Date构造函数中。在获取时间戳之前,我们需要对其进行调整,以匹配自 1970 年 1 月 1 日 st 午夜以来常用的毫秒数。UUIDs 从 10 月 15 日午夜 1582 以 100 纳秒的间隔测量。两个时代之间的转换在前面列出的getTimeFromUUID功能中完成。

C# 的 Cassandra

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