Как использовать транзакцию MongoDB, используя Mongoose? - PullRequest
0 голосов
/ 22 ноября 2018

Я использую облако MongoDB Atlas (https://cloud.mongodb.com/) и библиотеку Mongoose.

Я пытался создать несколько документов, используя концепцию транзакций, но это не работает. Я не получаю никакой ошибки., Нокажется, что откат не работает должным образом.

app.js

//*** more code here

var app = express();

require('./models/db');

//*** more code here

models / db.js

var mongoose = require( 'mongoose' );

// Build the connection string
var dbURI = 'mongodb+srv://mydb:pass@cluster0-****.mongodb.net/mydb?retryWrites=true';

// Create the database connection
mongoose.connect(dbURI, {
  useCreateIndex: true,
  useNewUrlParser: true,
});

// Get Mongoose to use the global promise library
mongoose.Promise = global.Promise;

models / user.js

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  userName: {
    type: String,
    required: true
  },
  pass: {
    type: String,
    select: false
  }
});

module.exports = mongoose.model("User", UserSchema, "user");

myroute.js

const db = require("mongoose");
const User = require("./models/user");

router.post("/addusers", async (req, res, next) => {

    const SESSION = await db.startSession();

    await SESSION.startTransaction();

    try {

          const newUser = new User({
            //*** data for user ***
          });
          await newUser.save();

          //*** for test purpose, trigger some error ***
          throw new Error("some error");

          await SESSION.commitTransaction();

          //*** return data 

    } catch (error) {
            await SESSION.abortTransaction();
    } finally {
            SESSION.endSession();
    }    

 });

Приведенный выше код работает без ошибок, но все равносоздает пользователя в БД. Предполагается, что откат созданного пользователя и коллекция должна быть пустой.

Я не знаю, что я здесь упустил. Может кто-нибудь, пожалуйста, дайте мне знать, что здесь не так?

приложение, модели, схема и маршрутизатор находятся в разных файлах.

1 Ответ

0 голосов
/ 23 ноября 2018

Вам необходимо включить session в опции для всех операций чтения / записи, которые активны во время транзакции.Только тогда они фактически применяются к области транзакции, где вы можете откатить их.

В качестве немного более полного списка, и просто с использованием более классического Order/OrderItems моделирования, которое должно быть довольно знакомо большинству людей.с некоторым опытом работы с реляционными транзакциями:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/trandemo';
const opts = { useNewUrlParser: true };

// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);

// schema defs

const orderSchema = new Schema({
  name: String
});

const orderItemsSchema = new Schema({
  order: { type: Schema.Types.ObjectId, ref: 'Order' },
  itemName: String,
  price: Number
});

const Order = mongoose.model('Order', orderSchema);
const OrderItems = mongoose.model('OrderItems', orderItemsSchema);

// log helper

const log = data => console.log(JSON.stringify(data, undefined, 2));

// main

(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    // clean models
    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.deleteMany())
    )

    let session = await conn.startSession();
    session.startTransaction();

    // Collections must exist in transactions
    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.createCollection())
    );

    let [order, other] = await Order.insertMany([
      { name: 'Bill' },
      { name: 'Ted' }
    ], { session });

    let fred = new Order({ name: 'Fred' });
    await fred.save({ session });

    let items = await OrderItems.insertMany(
      [
        { order: order._id, itemName: 'Cheese', price: 1 },
        { order: order._id, itemName: 'Bread', price: 2 },
        { order: order._id, itemName: 'Milk', price: 3 }
      ],
      { session }
    );

    // update an item
    let result1 = await OrderItems.updateOne(
      { order: order._id, itemName: 'Milk' },
      { $inc: { price: 1 } },
      { session }
    );
    log(result1);

    // commit
    await session.commitTransaction();

    // start another
    session.startTransaction();

    // Update and abort
    let result2 = await OrderItems.findOneAndUpdate(
      { order: order._id, itemName: 'Milk' },
      { $inc: { price: 1 } },
      { 'new': true, session }
    );
    log(result2);

    await session.abortTransaction();

    /*
     * $lookup join - expect Milk to be price: 4
     *
     */

    let joined = await Order.aggregate([
      { '$match': { _id: order._id } },
      { '$lookup': {
        'from': OrderItems.collection.name,
        'foreignField': 'order',
        'localField': '_id',
        'as': 'orderitems'
      }}
    ]);
    log(joined);


  } catch(e) {
    console.error(e)
  } finally {
    mongoose.disconnect()
  }

})()

Поэтому я, как правило, рекомендую вызывать переменную session в нижнем регистре, поскольку это имя ключа для объекта «options», который требуется для всех операций.,Сохранение этого в соглашении о нижнем регистре позволяет использовать такие вещи, как назначение объекта ES6:

const conn = await mongoose.connect(uri, opts);

...

let session = await conn.startSession();
session.startTransaction();

Также документация mongoose по транзакциям немного вводит в заблуждение, или, по крайней мере, это может бытьболее описательный.В примерах это db - это на самом деле экземпляр Mongoose Connection , а не базовый Db или даже глобальный импорт mongoose, поскольку некоторые могут неверно истолковать это.Обратите внимание, что в листинге и выше приведен отрывок, полученный из mongoose.connect(), и он должен храниться в вашем коде как нечто, к чему вы можете получить доступ из общего импорта.

В качестве альтернативы вы можете даже получить это в модульном коде через mongoose.connection свойство, в любое время после соединение было установлено.Обычно это безопасно в таких вещах, как обработчики маршрутов сервера и т. П., Поскольку к моменту вызова кода будет установлено соединение с базой данных.

Код также демонстрирует использование session в различных методах модели:

let [order, other] = await Order.insertMany([
  { name: 'Bill' },
  { name: 'Ted' }
], { session });

let fred = new Order({ name: 'Fred' });
await fred.save({ session });

Все методы, основанные на find(), и методы, основанные на update() или insert() и delete(), имеют окончательный «блок опций», в котором ожидаются ключ и значение сеанса.Единственный аргумент метода save() - это блок параметров.Это то, что сообщает MongoDB о необходимости применить эти действия к текущей транзакции в указанном сеансе.

Во многом таким же образом, прежде чем транзакция будет зафиксирована, запросы на find() или аналогичные, которые не указывают это *Опция 1041 * не отображает состояние данных во время выполнения транзакции.Измененное состояние данных доступно для других операций только после завершения транзакции.Обратите внимание, что это влияет на запись, как описано в документации .

Когда выдается «abort»:

// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
  { order: order._id, itemName: 'Milk' },
  { $inc: { price: 1 } },
  { 'new': true, session }
);
log(result2);

await session.abortTransaction();

Все операции с активной транзакцией удаляются изсостояние и не применяются.Как таковые они не видны для последующих операций впоследствии.В приведенном здесь примере значение в документе увеличивается, и в текущем сеансе будет показано извлеченное значение 5.Однако после session.abortTransaction() предыдущее состояние документа возвращается.Обратите внимание, что любой глобальный контекст, который не считывал данные в одном сеансе, не видит это изменение состояния, если не зафиксировано.

Это должно дать общий обзор.Существует еще больше сложностей, которые можно добавить для обработки разного уровня ошибок записи и повторных попыток, но это уже подробно описано в документации и во многих примерах, или на них можно ответить на более конкретный вопрос.


Вывод

Для справки, вывод включенного списка показан здесь:

Mongoose: orders.deleteMany({}, {})
Mongoose: orderitems.deleteMany({}, {})
Mongoose: orders.insertMany([ { _id: 5bf775986c7c1a61d12137dd, name: 'Bill', __v: 0 }, { _id: 5bf775986c7c1a61d12137de, name: 'Ted', __v: 0 } ], { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orders.insertOne({ _id: ObjectId("5bf775986c7c1a61d12137df"), name: 'Fred', __v: 0 }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orderitems.insertMany([ { _id: 5bf775986c7c1a61d12137e0, order: 5bf775986c7c1a61d12137dd, itemName: 'Cheese', price: 1, __v: 0 }, { _id: 5bf775986c7c1a61d12137e1, order: 5bf775986c7c1a61d12137dd, itemName: 'Bread', price: 2, __v: 0 }, { _id: 5bf775986c7c1a61d12137e2, order: 5bf775986c7c1a61d12137dd, itemName: 'Milk', price: 3, __v: 0 } ], { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orderitems.updateOne({ order: ObjectId("5bf775986c7c1a61d12137dd"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
{
  "n": 1,
  "nModified": 1,
  "opTime": {
    "ts": "6626894672394452998",
    "t": 139
  },
  "electionId": "7fffffff000000000000008b",
  "ok": 1,
  "operationTime": "6626894672394452998",
  "$clusterTime": {
    "clusterTime": "6626894672394452998",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}
Mongoose: orderitems.findOneAndUpdate({ order: ObjectId("5bf775986c7c1a61d12137dd"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2"), upsert: false, remove: false, projection: {}, returnOriginal: false })
{
  "_id": "5bf775986c7c1a61d12137e2",
  "order": "5bf775986c7c1a61d12137dd",
  "itemName": "Milk",
  "price": 5,
  "__v": 0
}
Mongoose: orders.aggregate([ { '$match': { _id: 5bf775986c7c1a61d12137dd } }, { '$lookup': { from: 'orderitems', foreignField: 'order', localField: '_id', as: 'orderitems' } } ], {})
[
  {
    "_id": "5bf775986c7c1a61d12137dd",
    "name": "Bill",
    "__v": 0,
    "orderitems": [
      {
        "_id": "5bf775986c7c1a61d12137e0",
        "order": "5bf775986c7c1a61d12137dd",
        "itemName": "Cheese",
        "price": 1,
        "__v": 0
      },
      {
        "_id": "5bf775986c7c1a61d12137e1",
        "order": "5bf775986c7c1a61d12137dd",
        "itemName": "Bread",
        "price": 2,
        "__v": 0
      },
      {
        "_id": "5bf775986c7c1a61d12137e2",
        "order": "5bf775986c7c1a61d12137dd",
        "itemName": "Milk",
        "price": 4,
        "__v": 0
      }
    ]
  }
]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...