Как написать модель Sails.js, чтобы связанные категории не создавались заново для каждого нового документа?

Я чувствую, что должно быть простое решение для этого, но я не могу найти правильный способ найти его в Интернете. Я использую сервер Sails на Node.js и Express. Прямо сейчас я пытаюсь заставить базовый API CRUD работать, поэтому я отправляю сообщения и получаю соединения с Postman.

Я начинаю с пустой базы данных (размещенной в Mongolab) и пытаюсь создать два документа с разными именами, но одной категории. После этого я ожидаю, что в коллекции категорий будет только 1 документ и два в основной коллекции. Вместо этого второй POST вызывает ошибку, потому что он пытается создать документ второй категории, несмотря на атрибут {unique:true}.

Что я могу сделать, чтобы исправить эту ошибку и установить правильное отношение «один ко многим»?

\api\models\Beer.js:

module.exports = {
  attributes: {
    // Primitive attributes
    name: {
      type: 'string',
      required: true,
      unique: true
    },
    alcoholByVolume: {
      type: 'float',
      defaultsTo: null
    },
    beerAdvocateId: {
      type: 'integer',
      defaultsTo: null
    },

    // Associations (aka relational attributes)
    brewery: { model: 'Company' },
    style: { model: 'BeerStyle' }
  }
};

\api\models\Company.js:

module.exports = {
  attributes: {
    name: {
      type: 'string',
      required: true,
      unique: true
    },
    countryCode: {
      type: 'string',
      enum: ['ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', 'bm', 'bn', 'bo', 'bq', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cu', 'cv', 'cw', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh', 'er', 'es', 'et', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mf', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'ss', 'st', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'za', 'zm', 'zw'],
      defaultsTo: 'us'
    }
  }
};

\api\models\BeerStyle.js:

module.exports = {
  attributes: {
    name: {
      type: 'string',
      enum: ['American Amber / Red Ale', 'American Barleywine', 'American Black Ale', 'American Blonde Ale', 'American Brown Ale', 'American Dark Wheat Ale', 'American Double / Imperial IPA', 'American Double / Imperial Stout', 'American IPA', 'American Pale Ale (APA)', 'American Pale Wheat Ale', 'American Porter', 'American Stout', 'American Strong Ale', 'American Wild Ale', 'Black & Tan', 'Chile Beer', 'Cream Ale', 'Pumpkin Ale', 'Rye Beer', 'Wheatwine', 'Belgian Dark Ale', 'Belgian IPA', 'Belgian Pale Ale', 'Belgian Strong Dark Ale', 'Belgian Strong Pale Ale', 'Bière de Champagne / Bière Brut', 'Bière de Garde', 'Dubbel', 'Faro', 'Flanders Oud Bruin', 'Flanders Red Ale', 'Gueuze', 'Lambic - Fruit', 'Lambic - Unblended', 'Quadrupel (Quad)', 'Saison / Farmhouse Ale', 'Tripel', 'Witbier', 'Baltic Porter', 'Braggot', 'English Barleywine', 'English Bitter', 'English Brown Ale', 'English Dark Mild Ale', 'English India Pale Ale (IPA)', 'English Pale Ale', 'English Pale Mild Ale', 'English Porter', 'English Stout', 'English Strong Ale', 'Extra Special / Strong Bitter (ESB)', 'Foreign / Export Stout', 'Milk / Sweet Stout', 'Oatmeal Stout', 'Old Ale', 'Russian Imperial Stout', 'Winter Warmer', 'Sahti', 'Altbier', 'Berliner Weissbier', 'Dunkelweizen', 'Gose', 'Hefeweizen', 'Kölsch', 'Kristalweizen', 'Roggenbier', 'Weizenbock', 'Irish Dry Stout', 'Irish Red Ale', 'Kvass', 'Scotch Ale / Wee Heavy', 'Scottish Ale', 'Scottish Gruit / Ancient Herbed Ale', 'Lager Styles', 'American Adjunct Lager', 'American Amber / Red Lager', 'American Double / Imperial Pilsner', 'American Malt Liquor', 'American Pale Lager', 'California Common / Steam Beer', 'Light Lager', 'Low Alcohol Beer', 'Czech Pilsener', 'Euro Dark Lager', 'Euro Pale Lager', 'Euro Strong Lager', 'Bock', 'Doppelbock', 'Dortmunder / Export Lager', 'Eisbock', 'German Pilsener', 'Kellerbier / Zwickelbier', 'Maibock / Helles Bock', 'Märzen / Oktoberfest', 'Munich Dunkel Lager', 'Munich Helles Lager', 'Rauchbier', 'Schwarzbier', 'Vienna Lager', 'Happoshu', 'Japanese Rice Lager', 'Fruit / Vegetable Beer', 'Herbed / Spiced Beer', 'Smoked Beer'],
      required: true,
      unique: true
    },
    group: {
      type: 'string',
      enum: ['American Ales', 'Belgian / French Ales', 'English Ales', 'Finnish Ales', 'German Ales', 'Irish Ales', 'Russian Ales', 'Scottish Ales', 'American Lagers', 'Czech Lagers', 'European Lagers', 'German Lagers', 'Japanese Lagers', 'Hybrid Styles'],
      required: true
    }
  }
};

\api\controllers\BeerController.js:

module.exports = {

  create: function(req, res){
    console.log(req.body);
    Beer.create(req.body).exec(function createCB(err, beer){
      if (err) return res.send(err);
      res.status(201);
      res.json(beer);
    });
  },

  // a FIND action
  read: function (req, res, next) {
    var id = req.param('id');

    if (id) {
      Beer.findOne(id, function(err, beer) {
        if(beer === undefined) return res.notFound();
        if (err) return res.send(err);
        res.json(beer);
      });

    } else {
        var where = req.param('where');
        if (_.isString(where)) where = JSON.parse(where);
        var options = {
          limit: req.param('limit') || undefined,
          skip: req.param('skip')  || undefined,
          sort: req.param('sort') || undefined,
          where: where || undefined
        };

        Beer.find(options, function(err, beer) {
          if(beer === undefined) return res.notFound();
          if (err) return res.send(err);
          res.json(beer);
        });
    }

  },

};

В \config\routes.js:

'post /api/beers': 'BeerController.create',
'get /api/beers/:id?': 'BeerController.read',

Два запроса:

POST /api/beers HTTP/1.1
Host: localhost:1337
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 0fd81643-08ce-7e43-6ed7-c3abafb22700
{"name":"Bud Light Lime","style": {"name": "Light Lager","group": "American Lagers"}, "brewery": {"name": "Anheuser-Busch","countryCode":"us"}, "alcoholByVolume": 4.2, "beerAdvocateId": 41821}

POST /api/beers HTTP/1.1
Host: localhost:1337
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 6e9efbd9-bb10-9748-7e70-229e131fe166
{"name":"Bud Light","style": {"name": "Light Lager","group": "American Lagers"}, "brewery": {"name": "Anheuser-Busch","countryCode":"us"}, "alcoholByVolume": 4.2, "beerAdvocateId": 1320}

Ошибка при втором запросе:

{
  "error": "E_UNKNOWN",
  "status": 500,
  "summary": "Encountered an unexpected error",
  "raw": {
    "name": "MongoError",
    "code": 11000,
    "err": "insertDocument :: caused by :: 11000 E11000 duplicate key error index: hkbooze.beerstyle.$name_1  dup key: { : \"Light Lager\" }"
  }
}

person carpiediem    schedule 29.11.2014    source источник


Ответы (1)


Похоже, вы передаете JSON для style и brewery. Честно говоря, я никогда не пытался заполнить несколько моделей одним запросом, подобным этому. Это действительно особенность Sails? Если да, то он все еще, вероятно, пытается каждый раз создавать категорию, что вызывает проблему уникальности.

Два моих способа сделать это: 1) Сначала определите категории, если они останутся прежними. Передайте только идентификатор. или 2) Писать какой-нибудь кастомный скрипт наверное в beforeValidate() который бы вынимал JSON из запроса, пытался найти его в категориях по названию. Если он ничего не мог найти, он создавал новую категорию. Во всех случаях он вернет идентификатор.

this is some example code, it may contain flaws:]

beforeValidate: function(values, cb){
		if(typeof values.style !== 'string'){
			Style.find({where: {name: values.style.name}}.exec(function(err, foundStyle){
              if(!data){
                Style.create(values.style,function(err, newstyle){
                  if(!err){
                    values.style = newstyle.id;
                    cb();
                  }
                });
              } else {
                values.style  = foundStyle.id;
                cb();
              }
            });
		}
	}

person Martin Malinda    schedule 29.11.2014
comment
Эти варианты имеют смысл. Есть ли у вас мнение, какое из двух решений может быть лучше в долгосрочной перспективе (например, легче расширять, масштабировать и т. д.)? Я относительно новичок в серверном коде и ценю любые советы по передовому опыту. - person carpiediem; 29.11.2014
comment
Я бы определил категории в другой форме/запросе. А затем выберите из существующих категорий при создании новой записи продукта. Это очень легко реализовать, и код выглядит легко читаемым. Решение 2) имеет то преимущество, что используется на 1 форму/запрос меньше, и, следовательно, более удобно, но может быть подвержено ошибкам. - person Martin Malinda; 30.11.2014
comment
После более внимательного изучения документации Sails JS кажется, что чтобы соответствовать их рекомендации. Спасибо! Я также добавлю сорта пива: {коллекция: 'Пиво', via: 'пивоварня'} в модель компании. - person carpiediem; 30.11.2014