Mongoose populate either ObjectId reference or String

别来无恙 提交于 2019-12-24 05:36:04

问题


Is there a way to specify a heterogeneous array as a schema property where it can contain both ObjectIds and strings? I'd like to have something like the following:

var GameSchema = new mongoose.schema({
    players: {
        type: [<UserModel reference|IP address/socket ID/what have you>]
    }

Is the only option a Mixed type that I manage myself? I've run across discriminators, which look somewhat promising, but it looks like it only works for subdocuments and not references to other schemas. Of course, I could just have a UserModel reference and create a UserModel that just stores the IP address or whatever I'm using to identify them, but that seems like it could quickly get hugely out of control in terms of space (having a model for every IP I come across sounds bad).

EDIT:

Example:

A game has one logged in user, three anonymous users, the document should look something like this:

{ players: [ ObjectId("5fd88ea85...."), "192.0.0.1", "192.1.1.1", "192.2.2.1"] }

Ideally this would be populated to:

{ players: [ UserModel(id: ..., name: ...),  "192.0.0.1", "192.1.1.1", "192.2.2.1"] }

EDIT:

I've decided to go a different route: instead of mixing types, I'm differentiating with different properties. Something like this:

players: [
    {
        user: <object reference>,
        sessionID: <string>,
        color: {
           type: String
        },
        ...other properties...
    }
]

I have a validator that ensures only one of user or sessionID are populated for a given entry. In some ways this is more complex, but it does obviate the need to do this kind of conditional populating and figuring out what type each entry is when iterating over them. I haven't tried any of the answers, but they look promising.


回答1:


If you are content to go with using Mixed or at least some scheme that will not work with .populate() then you can shift the "join" responsibility to the "server" instead using the $lookup functionality of MongoDB and a little fancy matching.

For me if I have a "games" collection document like this:

{
        "_id" : ObjectId("5933723c886d193061b99459"),
        "players" : [
                ObjectId("5933723c886d193061b99458"),
                "10.1.1.1",
                "10.1.1.2"
        ],
        "__v" : 0
}

Then I send the statement to the server to "join" with the "users" collection data where an ObjectId is present like this:

Game.aggregate([
  { "$addFields": {
    "users": {
      "$filter": {
        "input": "$players",
        "as": "p",
        "cond": { "$gt": [ "$$p", {} ] }
      }
    }
  }},
  { "$lookup": {
    "from": "users",
    "localField": "users",
    "foreignField": "_id",
    "as": "users"
  }},
  { "$project": {
    "players": {
      "$map": {
        "input": "$players",
        "as": "p",
        "in": {
          "$cond": {
            "if": { "$gt": [ "$$p", {} ] },
            "then": {
              "$arrayElemAt": [
                { "$filter": {
                  "input": "$users",
                  "as": "u",
                  "cond": { "$eq": [ "$$u._id", "$$p" ] }
                }},
                0
              ]
            },
            "else": "$$p"
          }
        }
      }
    }
  }}
])

Which gives the result when joined to the users object as:

{
        "_id" : ObjectId("5933723c886d193061b99459"),
        "players" : [
                {
                        "_id" : ObjectId("5933723c886d193061b99458"),
                        "name" : "Bill",
                        "__v" : 0
                },
                "10.1.1.1",
                "10.1.1.2"
        ]
}

So the "fancy" part really relies on this logical statement when considering the entries in the "players" array:

  "$filter": {
    "input": "$players",
    "as": "p",
    "cond": { "$gt": [ "$$p", {} ] }
  }

How this works is that to MongoDB, an ObjectId and actually all BSON types have a specific sort precedence. In this case where the data is "Mixed" between ObjectId and String then the "string" values are considered "less than" the value of a "BSON Object", and the ObjectId values are "greater than".

This allows you to separate the ObjectId values from the source array into their own list. Given that list, you $lookup to perform the "join" at get the objects from the other collection.

In order to put them back, I'm using $map to "transpose" each element of the original "players" where the matched ObjectId was found with the related object. An alternate approach would be to "split" the two types, do the $lookup and $concatArrays between the Users and the "strings". But that would not maintain the original array order, so $map may be a better fit.


I will add of note that the same basic process can be applied in a "client" operation by similarly filtering the content of the "players" array to contain just the ObjectId values and then calling the "model" form of .populate() from "inside" the response of the initial query. The documentation shows an example of that form of usage, as do some answers on this site before it was possible to do a "nested populate" with mongoose.

The other point of mind here is that .populate() itself existed as a mongoose method long before the $lookup aggregation pipeline operator came about, and was a solution for a time when MongoDB itself was incapable of performing a "join" of any sort. So the operations are indeed "client" side as an emulation and really only perform additional queries that you do not need to be aware of in issuing the statements yourself.

Therefore it should generally be desirable in a modern scenario to use the "server" features, and avoid the overhead involved with multiple queries in order to get the result.



来源:https://stackoverflow.com/questions/44349435/mongoose-populate-either-objectid-reference-or-string

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!