在如何在回调之前在水线中创建一个独特的slug?

时间:2015-06-25 01:13:41

标签: node.js waterline

问题:我在NodeJS应用程序中使用Waterline作为我的ORM和OrientDB。 OrientDB使用数字ID,因此在收到帖子时我不希望它们出现在我的网址中。为什么?因为只需增加ID就可以轻松查询完整数据。

解决方案:创建一个独特的slu ..

问题:如何在具有异步回调的Waterline中实现这一目标?我需要这样的东西,但没有找到解决方案。流程可能是这样的:

  • 创建slug
  • 检查是否存在slug
  • 如果否,请继续进行验证
  • 如果是,请更改slug并重新开始

2 个答案:

答案 0 :(得分:2)

以下是我目前正在使用的乐观解决方案:

我的帮助班:

=SUM(INDIRECT("'"&F2&"'!C4"),INDIRECT("'"&F3&"'!C4")),

我的模型定义:

// databaseExtensions.js

var _ = require('lodash');

function getUnique(property, value, separator, criteria){
  separator = separator || '-';
  criteria = criteria || {};
  var searchObject = {};
  searchObject[property] = { like: value + '%' };
  _.mergeDefaults(searchObject, criteria);
  return this.find(searchObject)
    .then(function(models){
      if(!models || models.length === 0)
        return value;  // value is unique
      var values = _.pluck(models, property);
      return getUniqueFromArray(values, value, separator);
    });
}


function getUniqueFromArray(existingValues, newValue, separator){
  var valuesArray = _.clone(existingValues);
  var version = 2;  // starting version
  var currentValue = newValue;
  var unique;

  while(version < 10000) { //just to be safe and we don't end up in a infinite     loop
    unique = true;
    for(var i=0; i<valuesArray.length; i++){
      if(currentValue === valuesArray[i]){
        unique = false;
        valuesArray.splice(i, 1);
        break;
      }
    }
    if (unique) {
      return currentValue;
    }
    currentValue = newValue + separator + version;
    version++;
  }
}

module.exports.getUnique = getUnique;
module.exports.getUniqueFromArray = getUniqueFromArray;

在我的控制器中:

// post.model.js
{
  //..
  atributes: {
    //...
    urlSlug : {
      type : 'string',
      required : true,
      index : true
    }
},
  },
  getUnique: require('path/to/databaseExtensions');.getUnique
}

在我的情况下,冲突是不太可能的,所以我不太担心并发问题,其中2个模型同时以相同的标题到达。但在成千上万的用户创建帖子的背景下,这可能是一个问题。

如果这没有帮助,请告诉我。

答案 1 :(得分:1)

我使用async提出了自己的解决方案。最后,我决定不使用一个独特的slug作为我的标识符,现在使用一个随机字符串的组合,我称之为hash_id,以及一个不必是唯一的slug,它就是SEO。但是这个答案也包含了对独特slu to的解决方案。所以我的网址有这种格式:

http://example.com/posts/23hlj2l2/i_am_a_slug or
http://example.com/posts/:hash_id/:slug

我创建了一个辅助模块来进行字符串转换/创建。他们只是处理这个问题,对ORM一无所知或者价值是唯一的。

ModelHelpers模块导出两个方法,一个用于规范化输入(如标题)以创建一个slug。它接受一个可选参数,该参数是一个可以添加到slug末尾的数字。

第二种方法创建一个随机字母数字字符串。您可以传入一个参数作为字符串的长度。

var ModelHelpers = function() {
  // Init
}

ModelHelpers.prototype.createSlugString = function(input_string, added_number) {
  added_number = typeof added_number !== 'undefined' ? added_number : '';

  // First replace all whitespaces and '-' and make sure there are no double _ 
  var clean_string = input_string.replace(/[\s\n\-]+/g, '_').replace(/_{2,}/g, '_');
  // Replace Umlaute and make lowercase
  clean_string = clean_string.toLowerCase().replace(/ä/ig, 'ae').replace(/ö/ig, 'oe').replace(/ü/ig, 'ue');
  // Replace any special characters and _ at the beginning or end
  clean_string = clean_string.replace(/[^\w]/g, '').replace(/^_+|_$/g, '');
  // Only return the first 8 words
  clean_string = clean_string.split("_").slice(0,8).join("_");

  // Add number if needed
  if(added_number !== '') {
    clean_string = clean_string + '_' + added_number.toString();
  }
  return clean_string;
}

ModelHelpers.prototype.makeHashID = function(hash_length)
{
  hash_length = typeof hash_length !== 'undefined' ? hash_length : 10;
  var text = "";
  var possible = "abcdefghijklmnopqrstuvwxyz0123456789";
  for( var i=0; i < hash_length; i++ ) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

module.exports = ModelHelpers;

我的解决方案的下一部分是将Waterlines的生命周期回调beforeValidateasync结合使用。这样我就可以将slug字段或hash id字段设置为unique,并在Waterline验证之前创建它。 Async是一个非常强大的工具,我只能建议调查它。我正在使用方法whilst

  

while(test,fn,callback)

     

反复调用fn,而test返回true。在何时调用回调   停止,或发生错误。

我创建了两个版本,一个是你需要一个随机字符串(hash_id),另一个是你想在你的slug的末尾添加一个数字,如果它不是唯一的。

对于哈希ID:

var Waterline = require('Waterline');
var orientAdapter = require('sails-orientdb');
var ModelHelpers = require('../modules/model-helpers');
var async = require('async');
var mh = new ModelHelpers();

var Post = Waterline.Collection.extend({
    identity: 'post',
    connection: 'myLocalOrient',

    attributes: {

        text: {
            type: 'text',
            required: true
        },

        slug: {
            type: 'string'
        },

        hash_id: {
            type: 'string',
            unique: true
        }
    },
    // Lifecycle Callbacks
    beforeValidate: function(values, next) {
        var model_self = this;
        var keep_running = true;
        // Create first slug
        values.hash_id = mh.makeHashID();
        values.slug = mh.createSlugString(values.text);

        async.whilst(
            function () { 
                // execute whilst while other post has not been retrieved or while it matches a hash_id
                // in the database
                return keep_running;
            },
            function (callback) {
                // search for post with this hash_id
                model_self.findOne().where({hash_id: values.hash_id}).then(function(op) {
                    if(op === undefined) {
                        // Nothing found, stop executing
                        keep_running = false;
                    } else {
                        // Create new hash_id
                        values.hash_id = mh.makeHashID();
                    }
                    callback();
                });
            },
            function (err) {
                // End the process
                // next(); is the callback of Waterlines' beforeValidate
                next();
            }
        ); // End whilst
    }
});

module.exports = Post;

对于独特的slu ::

var Waterline = require('Waterline');
var orientAdapter = require('sails-orientdb');
var ModelHelpers = require('../modules/model-helpers');
var async = require('async');
var mh = new ModelHelpers();

var Post = Waterline.Collection.extend({
    identity: 'post',
    connection: 'myLocalOrient',

    attributes: {

        text: {
            type: 'text',
            required: true
        },

        slug: {
            type: 'string',
            unique: true
        },

        hash_id: {
            type: 'string'
        }
    },
    // Lifecycle Callbacks
    beforeValidate: function(values, next) {
        var model_self = this;
        var keep_running = true;
        var counter = 0; // we use this to add a number
        // Create first slug
        values.hash_id = mh.makeHashID();
        values.slug = mh.createSlugString(values.text);

        async.whilst(
            function () { 
                // execute whilst while other post has not been retrieved or while it matches a slug
                // in the database
                return keep_running;
            },
            function (callback) {
                counter++;
                // search for post with this slug
                model_self.findOne().where({slug: values.slug}).then(function(op) {
                    if(op === undefined) {
                        // Nothing found, stop executing
                        keep_running = false;
                    } else {
                        // Create new slug
                        values.slug = mh.createSlugString(values.text, counter);
                    }
                    callback();
                });
            },
            function (err) {
                // End the test
                next();
            }
        ); // End whilst
    }
});

module.exports = Post;

这种方法的优点是,它只是一直运行,直到找到一个独特的slug / hash_id,并且它不关心数字之间的间隙(如果slug_2存在但不是slug_1)。它也不关心你使用的数据库类型。

如果偶然两个进程在同一时刻写入相同的slug,它仍然可能导致问题,但这必须在几毫秒内发生。而且我认为防止这种情况的唯一方法就是以某种方式锁定表格 - 如果我有幸遇到这个问题,我可以解决这个问题......