Jak zaktualizować / wstawić dokument w Mongoose?


367

Być może nadszedł czas, może to ja tonę w rzadkiej dokumentacji i nie jestem w stanie owinąć głowy koncepcją aktualizacji w Mongoose :)

Oto oferta:

Mam schemat i model kontaktu (skrócone właściwości):

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var mongooseTypes = require("mongoose-types"),
    useTimestamps = mongooseTypes.useTimestamps;


var ContactSchema = new Schema({
    phone: {
        type: String,
        index: {
            unique: true,
            dropDups: true
        }
    },
    status: {
        type: String,
        lowercase: true,
        trim: true,
        default: 'on'
    }
});
ContactSchema.plugin(useTimestamps);
var Contact = mongoose.model('Contact', ContactSchema);

Otrzymuję żądanie od klienta, zawierające pola, których potrzebuję i w ten sposób używam mojego modelu:

mongoose.connect(connectionString);
var contact = new Contact({
    phone: request.phone,
    status: request.status
});

A teraz dochodzimy do problemu:

  1. Jeśli zadzwonię contact.save(function(err){...}), otrzymam błąd, jeśli kontakt z tym samym numerem telefonu już istnieje (zgodnie z oczekiwaniami - niepowtarzalny)
  2. Nie mogę zadzwonić update()na kontakt, ponieważ ta metoda nie istnieje w dokumencie
  3. Jeśli wywołam aktualizację na modelu:
    Contact.update({phone:request.phone}, contact, {upsert: true}, function(err{...})
    dostanę się do nieskończonej pętli, ponieważ implementacja aktualizacji Mongoose wyraźnie nie chce, aby obiekt był drugim parametrem.
  4. Jeśli zrobię to samo, ale w drugim parametrze przekażę tablicę asocjacyjną właściwości żądania, {status: request.status, phone: request.phone ...}to działa - ale wtedy nie mam odniesienia do konkretnego kontaktu i nie mogę znaleźć jego createdAti updatedAtwłaściwości.

Więc w końcu, po wszystkim, co próbowałem: biorąc pod uwagę dokument contact, jak go zaktualizować, jeśli istnieje, lub dodać, jeśli nie istnieje?

Dziękuję za Twój czas.


Co o zaczepiając w preza save?
Shamoon,

Odpowiedzi:


427

Mongoose obsługuje to teraz natywnie z findOneAndUpdate (wywołuje MongoDB findAndModify ).

Opcja upsert = true tworzy obiekt, jeśli nie istnieje. domyślnie false .

var query = {'username': req.user.username};
req.newData.username = req.user.username;

MyModel.findOneAndUpdate(query, req.newData, {upsert: true}, function(err, doc) {
    if (err) return res.send(500, {error: err});
    return res.send('Succesfully saved.');
});

W starszych wersjach Mongoose nie obsługuje tych haków przy użyciu tej metody:

  • domyślne
  • setery
  • walidatory
  • oprogramowanie pośrednie

17
To powinna być aktualna odpowiedź. Większość innych używa dwóch połączeń lub (jak sądzę) wraca do natywnego sterownika mongodb.
huggie

10
Problem z findOneAndUpdate polega na tym, że wstępne zapisywanie nie zostanie wykonane.
a77icu5

2
Brzmi jak błąd w Mongoose lub MongoDB?
Pascalius

8
Od docs: "... przy użyciu pomocników findAndModify dodaje nie są stosowane: domyślne, ustawiaczy, zatwierdzające, middleware" mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
Kellen

2
@JamieHutber To nie jest ustawione domyślnie, jest to właściwość niestandardowa
Pascalius

194

Właśnie spaliłem solidne 3 godziny, próbując rozwiązać ten sam problem. W szczególności chciałem „zastąpić” cały dokument, jeśli istnieje, lub wstawić go w inny sposób. Oto rozwiązanie:

var contact = new Contact({
  phone: request.phone,
  status: request.status
});

// Convert the Model instance to a simple object using Model's 'toObject' function
// to prevent weirdness like infinite looping...
var upsertData = contact.toObject();

// Delete the _id property, otherwise Mongo will return a "Mod on _id not allowed" error
delete upsertData._id;

// Do the upsert, which works like this: If no Contact document exists with 
// _id = contact.id, then create a new doc using upsertData.
// Otherwise, update the existing doc with upsertData
Contact.update({_id: contact.id}, upsertData, {upsert: true}, function(err{...});

Utworzyłem problem na stronie projektu Mongoose z prośbą o dodanie informacji o tym do dokumentacji.


1
Dokumentacja wydaje się w tej chwili słaba. MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
Dokumenty

ponieważ nie znaleziono dokumentu sprawy, który identyfikator jest używany? Mongoose to generuje czy ten, przeciwko któremu był pytany?
Haider

91

Byliście blisko

Contact.update({phone:request.phone}, contact, {upsert: true}, function(err){...})

ale twoim drugim parametrem powinien być na przykład obiekt z operatorem modyfikacji

Contact.update({phone:request.phone}, {$set: { phone: request.phone }}, {upsert: true}, function(err){...})

15
Nie sądzę, żebyś potrzebował tej {$set: ... }części, ponieważ jest to automatyczna forma mojego czytania
CpILL

5
Tak, mangusta mówi, że zamienia wszystko w $ set
grantwparks

1
Było to ważne w momencie pisania tego tekstu, nie używam już MongoDB, więc nie mogę mówić o zmianach w ostatnich miesiącach: D
chrixian

5
Jednak niestosowanie $ set może być złym nawykiem, jeśli od czasu do czasu będziesz używać natywnego sterownika.
UpTheCreek

Możesz użyć $ set i $ setOnInsert, aby ustawić tylko niektóre pola w przypadku wstawki
justin.m.chase

73

Cóż, czekałem wystarczająco długo i bez odpowiedzi. W końcu zrezygnowałem z całego podejścia do aktualizacji / aktualizacji i wybrałem:

ContactSchema.findOne({phone: request.phone}, function(err, contact) {
    if(!err) {
        if(!contact) {
            contact = new ContactSchema();
            contact.phone = request.phone;
        }
        contact.status = request.status;
        contact.save(function(err) {
            if(!err) {
                console.log("contact " + contact.phone + " created at " + contact.createdAt + " updated at " + contact.updatedAt);
            }
            else {
                console.log("Error: could not save contact " + contact.phone);
            }
        });
    }
});

Czy to działa? Tak. Czy jestem z tego zadowolony? Prawdopodobnie nie. 2 połączenia DB zamiast jednego.
Mamy nadzieję, że przyszłe wdrożenie Mongoose przyniesie jakąś Model.upsertfunkcję.


2
W tym przykładzie zastosowano interfejs dodany w MongoDB 2.2 do określenia opcji wielu i wstawiania w formie dokumentu. .. include :: /includes/fact-upsert-multi-options.rst Dokumentacja to stwierdza, nie wiem, dokąd się udać.
Donald Derek

1
Chociaż powinno to działać, wykonujesz teraz 2 operacje (znajdź, aktualizuj), gdy potrzebna jest tylko 1 (aktualizacja). @chrixian pokazuje właściwy sposób, aby to zrobić.
szacunek TheCode

12
Warto zauważyć, że jest to jedyna odpowiedź, która pozwala walidatorom Mongoose na uruchomienie . Zgodnie z dokumentami sprawdzanie poprawności nie występuje, jeśli wywołasz aktualizację.
Tom Spencer

@fiznool wygląda na to, że możesz ręcznie przekazać tę opcję runValidators: truepodczas aktualizacji: aktualizuj dokumenty (jednak aktualizatory działają tylko $seti $unsetdziałają)
Danny

Zobacz moją odpowiedź na podstawie tego, jeśli chcesz .upsert()być dostępny we wszystkich modelach. stackoverflow.com/a/50208331/1586406
spondbob

24

Bardzo eleganckie rozwiązanie, które można osiągnąć za pomocą łańcucha obietnic:

app.put('url', (req, res) => {

    const modelId = req.body.model_id;
    const newName = req.body.name;

    MyModel.findById(modelId).then((model) => {
        return Object.assign(model, {name: newName});
    }).then((model) => {
        return model.save();
    }).then((updatedModel) => {
        res.json({
            msg: 'model updated',
            updatedModel
        });
    }).catch((err) => {
        res.send(err);
    });
});

Dlaczego to nie zostało ocenione? Wydaje się być świetnym rozwiązaniem i bardzo eleganckim
MadOgre

Świetne rozwiązanie, naprawdę sprawiło, że przemyślałem swoje podejście do obietnic.
lux,

4
Jeszcze bardziej elegancki byłby przepisywanie (model) => { return model.save(); }jako model => model.save(), a także (err) => { res.send(err); }jako err => res.send(err);)
Jeremy Thille

1
Gdzie mogę uzyskać więcej informacji?
V0LT3RR4

17

Jestem opiekunem Mongoose. Bardziej nowoczesnym sposobem na zachowanie dokumentu jest użycie tej Model.updateOne()funkcji .

await Contact.updateOne({
    phone: request.phone
}, { status: request.status }, { upsert: true });

Jeśli potrzebujesz zaktualizowanego dokumentu, możesz użyć Model.findOneAndUpdate()

const doc = await Contact.findOneAndUpdate({
    phone: request.phone
}, { status: request.status }, { upsert: true });

Kluczową kwestią jest to, że musisz umieścić unikalne właściwości filterparametru w parametrze updateOne()lub findOneAndUpdate(), a pozostałe właściwości updateparametru.

Oto samouczek dotyczący wstawiania dokumentów w Mongoose .


15

Utworzyłem konto StackOverflow, aby odpowiedzieć na to pytanie. Po bezowocnych poszukiwaniach między sieciami sam coś napisałem. Tak to zrobiłem, aby można go było zastosować do dowolnego modelu mangusty. Zaimportuj tę funkcję lub dodaj ją bezpośrednio do kodu, w którym przeprowadzasz aktualizację.

function upsertObject (src, dest) {

  function recursiveFunc (src, dest) {
    _.forOwn(src, function (value, key) {
      if(_.isObject(value) && _.keys(value).length !== 0) {
        dest[key] = dest[key] || {};
        recursiveFunc(src[key], dest[key])
      } else if (_.isArray(src) && !_.isObject(src[key])) {
          dest.set(key, value);
      } else {
        dest[key] = value;
      }
    });
  }

  recursiveFunc(src, dest);

  return dest;
}

Następnie, aby wstawić dokument mangusty, wykonaj następujące czynności:

YourModel.upsert = function (id, newData, callBack) {
  this.findById(id, function (err, oldData) {
    if(err) {
      callBack(err);
    } else {
      upsertObject(newData, oldData).save(callBack);
    }
  });
};

To rozwiązanie może wymagać 2 połączeń DB, jednak zyskujesz,

  • Sprawdzanie poprawności schematu względem modelu, ponieważ używasz funkcji .save ()
  • Możesz wstawić głęboko zagnieżdżone obiekty bez ręcznego wyliczania w wywołaniu aktualizacji, więc jeśli zmienisz model, nie musisz się martwić o aktualizację kodu

Pamiętaj tylko, że obiekt docelowy zawsze nadpisuje źródło, nawet jeśli źródło ma istniejącą wartość

Ponadto w przypadku tablic, jeśli istniejący obiekt ma dłuższą tablicę niż ta, która ją zastępuje, wówczas wartości na końcu starej tablicy pozostaną. Prostym sposobem na upsertowanie całej tablicy jest ustawienie starej tablicy na pustą tablicę przed upsertem, jeśli to właśnie zamierzasz zrobić.

AKTUALIZACJA - 16.01.2016 Dodałem dodatkowy warunek, jeśli istnieje tablica prymitywnych wartości, Mongoose nie zdaje sobie sprawy, że tablica zostanie zaktualizowana bez użycia funkcji „set”.


2
+1 za utworzenie acc tylko w tym celu: P Chciałbym dać kolejny +1 za użycie .save (), ponieważ findOneAndUpate () uniemożliwia nam użycie walidatorów i pre, post itp. Dzięki, też to
sprawdzę

Przepraszamy, ale tutaj nie działało :( Przekroczono rozmiar stosu wywołań
użytkownik1576978

Jakiej wersji lodash używasz? Korzystam z wersji Lodash 2.4.1 Dzięki!
Aaron Mast

A także, jak skomplikowane są obiekty? Jeśli są zbyt duże, proces węzła może nie być w stanie obsłużyć liczby wywołań rekurencyjnych wymaganych do scalenia obiektów.
Aaron Mast

Użyłem tego, ale musiałem dodać if(_.isObject(value) && _.keys(value).length !== 0) {warunek ochronny, aby zatrzymać przepełnienie stosu. Tutaj Lodash 4+ wydaje się przekształcać wartości nieobiektywne w obiekty w keyswywołaniu, więc ochrona rekurencyjna była zawsze prawdziwa. Może jest lepszy sposób, ale teraz prawie dla mnie działa ...
Richard G

12

Musiałem zaktualizować / wstawić dokument do jednej kolekcji, a ja stworzyłem nowy literał obiektu, taki jak ten:

notificationObject = {
    user_id: user.user_id,
    feed: {
        feed_id: feed.feed_id,
        channel_id: feed.channel_id,
        feed_title: ''
    }
};

składa się z danych, które otrzymuję z innej bazy danych, a następnie wywołuje aktualizację na modelu

Notification.update(notificationObject, notificationObject, {upsert: true}, function(err, num, n){
    if(err){
        throw err;
    }
    console.log(num, n);
});

to jest wyjście, które otrzymuję po pierwszym uruchomieniu skryptu:

1 { updatedExisting: false,
    upserted: 5289267a861b659b6a00c638,
    n: 1,
    connectionId: 11,
    err: null,
    ok: 1 }

Oto wynik, gdy uruchamiam skrypt po raz drugi:

1 { updatedExisting: true, n: 1, connectionId: 18, err: null, ok: 1 }

Używam wersji mangusty 3.6.16


10
app.put('url', function(req, res) {

        // use our bear model to find the bear we want
        Bear.findById(req.params.bear_id, function(err, bear) {

            if (err)
                res.send(err);

            bear.name = req.body.name;  // update the bears info

            // save the bear
            bear.save(function(err) {
                if (err)
                    res.send(err);

                res.json({ message: 'Bear updated!' });
            });

        });
    });

Oto lepsze podejście do rozwiązania metody aktualizacji w wersji mangoose , możesz sprawdzić Scotch.io, aby uzyskać więcej informacji. To zdecydowanie działało dla mnie !!!


5
Błędem jest myśleć, że robi to to samo, co aktualizacja MongoDB. To nie jest atomowe.
Valentin Waeselynck

1
Chcę wykonać kopię zapasową odpowiedzi @ValentinWaeselynck. Kod szkocki jest czysty - ale pobierasz dokument, a następnie aktualizujesz. W trakcie tego procesu dokument mógł zostać zmieniony.
Nick Pineda

8

W wersji 2.6 wprowadzono błąd, który dotyczy również wersji 2.7

Upsert działał poprawnie na 2.4

https://groups.google.com/forum/#!topic/mongodb-user/UcKvx4p4hnY https://jira.mongodb.org/browse/SERVER-13843

Spójrz, zawiera kilka ważnych informacji

AKTUALIZACJA:

To nie znaczy, że upsert nie działa. Oto dobry przykład tego, jak z niego korzystać:

User.findByIdAndUpdate(userId, {online: true, $setOnInsert: {username: username, friends: []}}, {upsert: true})
    .populate('friends')
    .exec(function (err, user) {
        if (err) throw err;
        console.log(user);

        // Emit load event

        socket.emit('load', user);
    });

7

Możesz po prostu zaktualizować ten rekord i uzyskać zaktualizowane dane w odpowiedzi

router.patch('/:id', (req, res, next) => {
    const id = req.params.id;
    Product.findByIdAndUpdate(id, req.body, {
            new: true
        },
        function(err, model) {
            if (!err) {
                res.status(201).json({
                    data: model
                });
            } else {
                res.status(500).json({
                    message: "not found any relative data"
                })
            }
        });
});

6

to zadziałało dla mnie.

app.put('/student/:id', (req, res) => {
    Student.findByIdAndUpdate(req.params.id, req.body, (err, user) => {
        if (err) {
            return res
                .status(500)
                .send({error: "unsuccessful"})
        };
        res.send({success: "success"});
    });

});


Dzięki. To właśnie w końcu dla mnie zadziałało!
Luis Febro

4

Oto najprostszy sposób na utworzenie / aktualizację przy jednoczesnym wywołaniu oprogramowania pośredniego i walidatorów.

Contact.findOne({ phone: request.phone }, (err, doc) => {
    const contact = (doc) ? doc.set(request) : new Contact(request);

    contact.save((saveErr, savedContact) => {
        if (saveErr) throw saveErr;
        console.log(savedContact);
    });
})

3

Dla każdego, kto tu przyjeżdża, wciąż szukając dobrego rozwiązania dla „upsertingu” z obsługą haków, właśnie to przetestowałem i działałem. Nadal wymaga 2 połączeń DB, ale jest znacznie bardziej stabilny niż cokolwiek, co próbowałem podczas jednego połączenia.

// Create or update a Person by unique email.
// @param person - a new or existing Person
function savePerson(person, done) {
  var fieldsToUpdate = ['name', 'phone', 'address'];

  Person.findOne({
    email: person.email
  }, function(err, toUpdate) {
    if (err) {
      done(err);
    }

    if (toUpdate) {
      // Mongoose object have extra properties, we can either omit those props
      // or specify which ones we want to update.  I chose to update the ones I know exist
      // to avoid breaking things if Mongoose objects change in the future.
      _.merge(toUpdate, _.pick(person, fieldsToUpdate));
    } else {      
      toUpdate = person;
    }

    toUpdate.save(function(err, updated, numberAffected) {
      if (err) {
        done(err);
      }

      done(null, updated, numberAffected);
    });
  });
}

3

Jeśli dostępne są generatory, staje się to jeszcze łatwiejsze:

var query = {'username':this.req.user.username};
this.req.newData.username = this.req.user.username;
this.body = yield MyModel.findOneAndUpdate(query, this.req.newData).exec();

3

Żadne inne rozwiązanie nie działało dla mnie. Korzystam z żądania postu i aktualizuję dane, jeśli znalazłem, wstaw je, a także _id jest wysyłany z treścią żądania, które należy usunąć.

router.post('/user/createOrUpdate', function(req,res){
    var request_data = req.body;
    var userModel = new User(request_data);
    var upsertData = userModel.toObject();
    delete upsertData._id;

    var currentUserId;
    if (request_data._id || request_data._id !== '') {
        currentUserId = new mongoose.mongo.ObjectId(request_data._id);
    } else {
        currentUserId = new mongoose.mongo.ObjectId();
    }

    User.update({_id: currentUserId}, upsertData, {upsert: true},
        function (err) {
            if (err) throw err;
        }
    );
    res.redirect('/home');

});

2
//Here is my code to it... work like ninj

router.param('contractor', function(req, res, next, id) {
  var query = Contractors.findById(id);

  query.exec(function (err, contractor){
    if (err) { return next(err); }
    if (!contractor) { return next(new Error("can't find contractor")); }

    req.contractor = contractor;
    return next();
  });
});

router.get('/contractors/:contractor/save', function(req, res, next) {

    contractor = req.contractor ;
    contractor.update({'_id':contractor._id},{upsert: true},function(err,contractor){
       if(err){ 
            res.json(err);
            return next(); 
            }
    return res.json(contractor); 
  });
});


--

2
User.findByIdAndUpdate(req.param('userId'), req.body, (err, user) => {
    if(err) return res.json(err);

    res.json({ success: true });
});

Ten fragment kodu może rozwiązać problem, ale nie wyjaśnia, dlaczego ani w jaki sposób odpowiada na pytanie. Dołącz wyjaśnienie swojego kodu , ponieważ to naprawdę pomaga poprawić jakość Twojego postu. Pamiętaj, że w przyszłości odpowiadasz na pytanie czytelników, a ci ludzie mogą nie znać przyczyn Twojej sugestii kodu. Osoby zgłaszające / recenzenci: w przypadku odpowiedzi zawierających tylko kod, takich jak ta, głosuj, nie usuwaj!
Patrick

2

Zgodnie z odpowiedzią Traveling Tech Guy , która już jest niesamowita, możemy stworzyć wtyczkę i dołączyć ją do mangusty po jej zainicjowaniu, aby .upsert() była dostępna we wszystkich modelach.

plugins.js

export default (schema, options) => {
  schema.statics.upsert = async function(query, data) {
    let record = await this.findOne(query)
    if (!record) {
      record = new this(data)
    } else {
      Object.keys(data).forEach(k => {
        record[k] = data[k]
      })
    }
    return await record.save()
  }
}

db.js

import mongoose from 'mongoose'

import Plugins from './plugins'

mongoose.connect({ ... })
mongoose.plugin(Plugins)

export default mongoose

Następnie możesz zrobić coś takiego User.upsert({ _id: 1 }, { foo: 'bar' })lub YouModel.upsert({ bar: 'foo' }, { value: 1 })kiedy chcesz.



0

Ten skrypt do kawy działa dla mnie z Węzłem - sztuczka polega na tym, że _id pobiera z opakowania ObjectID po wysłaniu i zwróceniu od klienta, więc należy go wymienić na aktualizacje (gdy nie podano _id, save powróci do wstawiania i dodawania jeden).

app.post '/new', (req, res) ->
    # post data becomes .query
    data = req.query
    coll = db.collection 'restos'
    data._id = ObjectID(data._id) if data._id

    coll.save data, {safe:true}, (err, result) ->
        console.log("error: "+err) if err
        return res.send 500, err if err

        console.log(result)
        return res.send 200, JSON.stringify result

0

na podstawie tego, co napisał Martin Kuzdowicz powyżej. Korzystam z poniższych, aby wykonać aktualizację za pomocą mangusty i głębokiego scalenia obiektów json. Wraz z funkcją model.save () w mongoose pozwala to mangusty wykonać pełną walidację, nawet taką, która opiera się na innych wartościach w json. wymaga pakietu Deepmerge https://www.npmjs.com/package/deepmerge . Ale to bardzo lekki pakiet.

var merge = require('deepmerge');

app.put('url', (req, res) => {

    const modelId = req.body.model_id;

    MyModel.findById(modelId).then((model) => {
        return Object.assign(model, merge(model.toObject(), req.body));
    }).then((model) => {
        return model.save();
    }).then((updatedModel) => {
        res.json({
            msg: 'model updated',
            updatedModel
        });
    }).catch((err) => {
        res.send(err);
    });
});

1
Przestrzegałbym przed użyciem req.bodyw obecnym stanie przed przetestowaniem pod kątem wstrzykiwania NoSQL (patrz owasp.org/index.php/Testing_for_NoSQL_injection ).
Podróżujący facet techniczny

1
@TravelingTechGuy Dzięki za ostrożność Nadal jestem nowy w Node i Mongoose. Czy mój model mangusty z walidatorami nie wystarczyłby, aby złapać próbę wstrzyknięcia? podczas model.save ()
Chris Deleo

-5

Po przeczytaniu powyższych postów postanowiłem użyć tego kodu:

    itemModel.findOne({'pid':obj.pid},function(e,r){
        if(r!=null)
        {
             itemModel.update({'pid':obj.pid},obj,{upsert:true},cb);
        }
        else
        {
            var item=new itemModel(obj);
            item.save(cb);
        }
    });

jeśli r ma wartość null, tworzymy nowy element. W przeciwnym razie użyj aktualizacji w aktualizacji, ponieważ aktualizacja nie tworzy nowego elementu.


Jeśli to dwa połączenia z Mongo, to nie jest tak naprawdę słuszne, prawda?
huggie
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.