ChatGPT解决这个技术问题 Extra ChatGPT

如何在 MongoDB 中执行等效的 SQL Join?

如何在 MongoDB 中执行等效的 SQL Join?

例如,假设您有两个集合(用户和评论),我想提取 pid=444 的所有评论以及每个集合的用户信息。

comments
  { uid:12345, pid:444, comment="blah" }
  { uid:12345, pid:888, comment="asdf" }
  { uid:99999, pid:444, comment="qwer" }

users
  { uid:12345, name:"john" }
  { uid:99999, name:"mia"  }

有没有办法一次性提取具有某个字段的所有评论(例如 ...find({pid:444}) )以及与每个评论关联的用户信息?

目前,我首先得到符合我标准的评论,然后找出该结果集中的所有 uid,获取用户对象,并将它们与评论的结果合并。好像我做错了。

这个问题的最后一个答案可能是最相关的,因为 MongoDB 3.2+ 实现了一个名为 $lookup 的连接解决方案。以为我会把它推到这里,因为也许不是每个人都会读到底。 stackoverflow.com/a/33511166/2593330
正确,$lookup 是在 MongoDB 3.2 中引入的。详情可在 docs.mongodb.org/master/reference/operator/aggregation/lookup/… 找到
将任何查询转换为 mongo,检查答案:stackoverflow.com/questions/68155715/…
我相信 NoSQL 的初衷是你存储你想要检索的数据。所以,以这种方式存储它并以这种方式检索它

A
Ahmad Baktash Hayeri

从 Mongo 3.2 开始,这个问题的答案大多不再正确。添加到聚合管道的新 $lookup 运算符本质上与左外连接相同:

https://docs.mongodb.org/master/reference/operator/aggregation/lookup/#pipe._S_lookup

从文档:

{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

当然,Mongo 不是关系数据库,开发人员正在谨慎地推荐 $lookup 的特定用例,但至少从 3.2 开始,现在可以使用 MongoDB 进行连接。


@clayton:多于两个系列怎么样?
@DipenDedania 只需向聚合管道添加额外的 $lookup 阶段。
我无法将左集合中的数组中的任何字段与右集合中的相应 id 一起加入。有人可以帮助我吗?
我对此有点困惑 - 有什么方法可以指定您只需要“来自”集合中的某些文档,还是它会一次自动加入数据库中的所有文档?
只是想知道最新的 Spring Data MongoDB 是否支持 3.2?
O
Orlando Becerra

我们可以使用 mongodb 客户端控制台在几行中通过简单的功能合并/连接一个集合中的所有数据,现在我们可以执行所需的查询。下面是一个完整的例子,

.- 作者:

db.authors.insert([
    {
        _id: 'a1',
        name: { first: 'orlando', last: 'becerra' },
        age: 27
    },
    {
        _id: 'a2',
        name: { first: 'mayra', last: 'sanchez' },
        age: 21
    }
]);

.- 类别:

db.categories.insert([
    {
        _id: 'c1',
        name: 'sci-fi'
    },
    {
        _id: 'c2',
        name: 'romance'
    }
]);

.- 书籍

db.books.insert([
    {
        _id: 'b1',
        name: 'Groovy Book',
        category: 'c1',
        authors: ['a1']
    },
    {
        _id: 'b2',
        name: 'Java Book',
        category: 'c2',
        authors: ['a1','a2']
    },
]);

.- 图书借阅

db.lendings.insert([
    {
        _id: 'l1',
        book: 'b1',
        date: new Date('01/01/11'),
        lendingBy: 'jose'
    },
    {
        _id: 'l2',
        book: 'b1',
        date: new Date('02/02/12'),
        lendingBy: 'maria'
    }
]);

。- 魔法:

db.books.find().forEach(
    function (newBook) {
        newBook.category = db.categories.findOne( { "_id": newBook.category } );
        newBook.lendings = db.lendings.find( { "book": newBook._id  } ).toArray();
        newBook.authors = db.authors.find( { "_id": { $in: newBook.authors }  } ).toArray();
        db.booksReloaded.insert(newBook);
    }
);

.- 获取新的集合数据:

db.booksReloaded.find().pretty()

。- 回复 :)

{
    "_id" : "b1",
    "name" : "Groovy Book",
    "category" : {
        "_id" : "c1",
        "name" : "sci-fi"
    },
    "authors" : [
        {
            "_id" : "a1",
            "name" : {
                "first" : "orlando",
                "last" : "becerra"
            },
            "age" : 27
        }
    ],
    "lendings" : [
        {
            "_id" : "l1",
            "book" : "b1",
            "date" : ISODate("2011-01-01T00:00:00Z"),
            "lendingBy" : "jose"
        },
        {
            "_id" : "l2",
            "book" : "b1",
            "date" : ISODate("2012-02-02T00:00:00Z"),
            "lendingBy" : "maria"
        }
    ]
}
{
    "_id" : "b2",
    "name" : "Java Book",
    "category" : {
        "_id" : "c2",
        "name" : "romance"
    },
    "authors" : [
        {
            "_id" : "a1",
            "name" : {
                "first" : "orlando",
                "last" : "becerra"
            },
            "age" : 27
        },
        {
            "_id" : "a2",
            "name" : {
                "first" : "mayra",
                "last" : "sanchez"
            },
            "age" : 21
        }
    ],
    "lendings" : [ ]
}

我希望这些线可以帮助你。


我想知道是否可以使用学说 mongodb 运行相同的代码?
当引用对象之一获得更新时会发生什么?该更新是否会自动反映在 book 对象中?还是该循环需要再次运行?
只要您的数据很小,这很好。它将把每本书的内容带给你的客户,然后逐一获取每个类别、借阅和作者。当你的书有成千上万的时候,这会真的很慢。更好的技术可能是使用聚合管道并将合并的数据输出到单独的集合中。让我再说一遍。我将添加一个答案。
你能把你的算法调整到另一个例子吗? stackoverflow.com/q/32718079/287948
@SandeepGiri 我怎么能做聚合管道,因为我在分离集合中有非常密集的数据需要加入?
C
Community

官方 mongodb 网站上的这个页面正好解决了这个问题:

https://mongodb-documentation.readthedocs.io/en/latest/ecosystem/tutorial/model-data-for-ruby-on-rails.html

当我们显示我们的故事列表时,我们需要显示发布故事的用户的姓名。如果我们使用关系数据库,我们可以对用户和存储执行连接,并在单个查询中获取所有对象。但是 MongoDB 不支持连接,因此有时需要一些非规范化。在这里,这意味着缓存“用户名”属性。关系纯粹主义者可能已经感到不安,就好像我们违反了一些普遍规律一样。但请记住,MongoDB 集合并不等同于关系表;每个都有一个独特的设计目标。规范化表提供了一个原子的、隔离的数据块。然而,一个文档更接近地代表了一个整体的对象。在社交新闻网站的情况下,可以说用户名是发布的故事所固有的。


@dudelgrincen 这是从规范化和关系数据库的范式转变。 NoSQL 的目标是非常快速地读取和写入数据库。使用 BigData,您将拥有大量应用程序和前端服务器,而 DB 上的数量较少。您需要每秒进行数百万笔交易。从数据库中卸载繁重的工作并将其放到应用程序级别。如果您需要深入分析,您可以运行将数据放入 OLAP 数据库的集成作业。无论如何,您不应该从您的 OLTP 数据库中获得很多深度查询。
@dudelgrincen 我还应该说它并不适用于每个项目或设计。如果您有在 SQL 类型数据库中工作的东西,为什么要更改它?如果你不能按摩你的模式来使用 noSQL,那就不要。
迁移和不断发展的模式在 NoSQL 系统上也更容易管理。
如果用户在网站上有 3.540 个帖子,并且他确实更改了个人资料中的用户名怎么办?每个帖子都应该用新用户名更新吗?
@IvoPereira 是的,这正是人们应该避免以这种方式建模数据的原因。有一篇文章解释了相同的场景及其后果:Why You Should Never Use MongoDB
O
Otto Allmendinger

您必须按照您描述的方式进行操作。 MongoDB 是一个非关系型数据库,不支持连接。


似乎来自 sql server 背景的性能错误,但对于文档数据库来说可能还不错?
同样从 sql 服务器背景来看,我希望 MongoDB 一次性将“结果集”(带有选定的返回字段)作为新查询的输入,就像 SQL 中的嵌套查询一样
@terjetyl您必须真正计划一下。您将在前端显示哪些字段,如果在单个视图中数量有限,那么您将这些字段作为嵌入式文档。关键是不需要做连接。如果您想进行深入分析,请事后在另一个数据库中进行。运行将数据转换为 OLAP 多维数据集的作业以获得最佳性能。
从 mongo 3.2 版本开始支持左连接。
s
sbharti

通过 $lookup、$project 和 $match 的正确组合,您可以在多个参数上连接多个表。这是因为它们可以被链接多次。

假设我们要执行以下操作 (reference)

SELECT S.* FROM LeftTable S
LEFT JOIN RightTable R ON S.ID = R.ID AND S.MID = R.MID  
WHERE R.TIM > 0 AND S.MOB IS NOT NULL

第 1 步:链接所有表

您可以根据需要 $lookup 任意数量的表。

$lookup - 查询中的每个表一个

$unwind - 正确反规范化 data ,否则它会被包裹在数组中

Python代码..

db.LeftTable.aggregate([
                        # connect all tables

                        {"$lookup": {
                          "from": "RightTable",
                          "localField": "ID",
                          "foreignField": "ID",
                          "as": "R"
                        }},
                        {"$unwind": "R"}
                   
                        ])

第 2 步:定义所有条件

$project :在此处定义所有条件语句,以及您要选择的所有变量。

Python代码..

db.LeftTable.aggregate([
                        # connect all tables

                        {"$lookup": {
                          "from": "RightTable",
                          "localField": "ID",
                          "foreignField": "ID",
                          "as": "R"
                        }},
                        {"$unwind": "R"},

                        # define conditionals + variables

                        {"$project": {
                          "midEq": {"$eq": ["$MID", "$R.MID"]},
                          "ID": 1, "MOB": 1, "MID": 1
                        }}
                        ])

第 3 步:加入所有条件

$match - 使用 OR 或 AND 等连接所有条件。这些条件可以有多个。

$project:取消定义所有条件

完整的 Python 代码..

db.LeftTable.aggregate([
                        # connect all tables

                        {"$lookup": {
                          "from": "RightTable",
                          "localField": "ID",
                          "foreignField": "ID",
                          "as": "R"
                        }},
                        {"$unwind": "$R"},

                        # define conditionals + variables

                        {"$project": {
                          "midEq": {"$eq": ["$MID", "$R.MID"]},
                          "ID": 1, "MOB": 1, "MID": 1
                        }},

                        # join all conditionals

                        {"$match": {
                          "$and": [
                            {"R.TIM": {"$gt": 0}}, 
                            {"MOB": {"$exists": True}},
                            {"midEq": {"$eq": True}}
                        ]}},

                        # undefine conditionals

                        {"$project": {
                          "midEq": 0
                        }}

                        ])

几乎任何表、条件和连接的组合都可以以这种方式完成。


谢谢!,喜欢你答案的格式。
完美的答案,对我来说,它给 {"$unwind ":"R"} 一个错误,如果它改为 {"$unwind":"$R"},它就完美了!
V
VishAl

正如其他人指出的那样,您正在尝试从您真的不想做的非关系数据库创建一个关系数据库,但无论如何,如果您有一个必须这样做的案例,那么您可以使用一个解决方案。我们首先对集合 A(或在您的情况下为用户)进行 foreach 查找,然后我们将每个项目作为一个对象,然后我们使用对象属性(在您的情况下为 uid)在我们的第二个集合中查找(在您的情况下为评论),如果我们可以找到它然后我们有一个匹配,我们可以打印或用它做一些事情。希望这对你有帮助,祝你好运:)

db.users.find().forEach(
function (object) {
    var commonInBoth=db.comments.findOne({ "uid": object.uid} );
    if (commonInBoth != null) {
        printjson(commonInBoth) ;
        printjson(object) ;
    }else {
        // did not match so we don't care in this case
    }
});

这不会找到我们当前正在循环的项目吗?
A
Alex M

您可以使用 3.2 版本中提供的查找来加入 Mongo 中的两个集合。在您的情况下,查询将是

db.comments.aggregate({
    $lookup:{
        from:"users",
        localField:"uid",
        foreignField:"uid",
        as:"users_comments"
    }
})

或者您也可以加入用户,然后会有一些变化,如下所示。

db.users.aggregate({
    $lookup:{
        from:"comments",
        localField:"uid",
        foreignField:"uid",
        as:"users_comments"
    }
})

它将与 SQL 中的左右连接一样工作。


J
JosephSlote

这是“加入” * Actors 和 Movies 集合的示例:

https://github.com/mongodb/cookbook/blob/master/content/patterns/pivot.txt

它利用 .mapReduce() 方法

join - 加入面向文档的数据库的替代方法


-1,这不是连接来自两个集合的数据。它使用来自单个集合(参与者)的数据来旋转数据。因此,以前是键的东西现在是值,而值现在是键……与 JOIN 非常不同。
这正是您必须做的,MongoDB 不是关系型的,而是面向文档的。 MapReduce 允许处理具有高性能的数据(您可以使用集群等....),但即使对于简单的情况,它也非常有用!
C
Community

$lookup(聚合)

对同一数据库中的未分片集合执行左外连接,以过滤来自“已连接”集合的文档以进行处理。对于每个输入文档,$lookup 阶段添加一个新的数组字段,其元素是“加入”集合中的匹配文档。 $lookup 阶段将这些重新调整的文档传递到下一个阶段。 $lookup 阶段具有以下语法:

平等匹配

要在输入文档中的字段与“已加入”集合的文档中的字段之间执行相等匹配,$lookup 阶段具有以下语法:

{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

该操作将对应于以下伪 SQL 语句:

SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (SELECT <documents as determined from the pipeline>
                               FROM <collection to join>
                               WHERE <pipeline> );

Mongo URL


子查询与连接完全不同,如果您的左侧表很大,子查询意味着每一行都必须自己进行查询。它会变得很慢。 join 在 sql 中非常快。
S
Snowburnt

这取决于你想要做什么。

您当前已将其设置为规范化数据库,这很好,并且您执行此操作的方式是合适的。

但是,还有其他方法可以做到这一点。

您可以拥有一个帖子集合,其中嵌入了每个帖子的评论,其中包含对您可以迭代查询以获取的用户的引用。您可以将用户名与评论一起存储,您可以将它们全部存储在一个文档中。

NoSQL 的特点是它专为灵活的模式和非常快速的读写而设计。在典型的大数据场中,数据库是最大的瓶颈,数据库引擎比应用程序和前端服务器少……它们更昂贵但更强大,硬盘空间也相对便宜。规范化来自试图节省空间的概念,但它带来了使您的数据库执行复杂的联接和验证关系的完整性、执行级联操作的成本。如果他们正确设计数据库,所有这些都可以为开发人员节省一些麻烦。

使用 NoSQL,如果您接受冗余和存储空间不是问题,因为它们的成本(更新所需的处理器时间和存储额外数据的硬盘成本),非规范化不是问题(对于成为数十万个项目可能是性能问题,但大多数时候这不是问题)。此外,每个数据库集群都有多个应用程序和前端服务器。让他们完成连接的繁重工作,让数据库服务器坚持读写。

TL;DR:你正在做的很好,还有其他方法可以做到。查看 mongodb 文档的数据模型模式以获取一些很好的示例。 http://docs.mongodb.org/manual/data-modeling/


“规范化来自试图节省空间的概念”我对此表示质疑。恕我直言规范化来自避免冗余的概念。假设您将用户名与博文一起存储。如果她结婚了呢?在未标准化的模型中,您将不得不浏览所有帖子并更改名称。在标准化模型中,您通常会更改 ONE 记录。
@DanielKhan 防止冗余和节省空间是相似的概念,但经过重新分析,我同意,冗余是这种设计的根本原因。我会改写的。感谢您的注意。
N
NDB

许多驱动程序都支持一种称为 DBRef 的规范。

DBRef 是用于在文档之间创建引用的更正式的规范。 DBRefs(通常)包括一个集合名称和一个对象 ID。大多数开发人员仅在集合可以从一个文档更改为下一个文档时才使用 DBRefs。如果您引用的集合始终相同,则上述手动引用会更有效。

取自 MongoDB 文档:Data Models >数据模型参考 > Database References


A
Anish Agarwal

3.2.6之前,mongodb不像mysql那样支持join查询。以下解决方案适合您。

 db.getCollection('comments').aggregate([
        {$match : {pid : 444}},
        {$lookup: {from: "users",localField: "uid",foreignField: "uid",as: "userData"}},
   ])

m
metdos

您可以运行 SQL 查询,包括使用 Postgres 中的 mongo_fdw 在 MongoDB 上连接。


M
Michael Mior

MongoDB 不允许连接,但您可以使用插件来处理它。检查 mongo-join 插件。这是最好的,我已经用过了。您可以像 npm install mongo-join 一样直接使用 npm 安装它。您可以查看 full documentation with examples

(++) 当我们需要加入 (N) 个集合时非常有用的工具

(--) 我们可以在查询的顶层应用条件

例子

var Join = require('mongo-join').Join, mongodb = require('mongodb'), Db = mongodb.Db, Server = mongodb.Server;
db.open(function (err, Database) {
    Database.collection('Appoint', function (err, Appoints) {

        /* we can put conditions just on the top level */
        Appoints.find({_id_Doctor: id_doctor ,full_date :{ $gte: start_date },
            full_date :{ $lte: end_date }}, function (err, cursor) {
            var join = new Join(Database).on({
                field: '_id_Doctor', // <- field in Appoints document
                to: '_id',         // <- field in User doc. treated as ObjectID automatically.
                from: 'User'  // <- collection name for User doc
            }).on({
                field: '_id_Patient', // <- field in Appoints doc
                to: '_id',         // <- field in User doc. treated as ObjectID automatically.
                from: 'User'  // <- collection name for User doc
            })
            join.toArray(cursor, function (err, joinedDocs) {

                /* do what ever you want here */
                /* you can fetch the table and apply your own conditions */
                .....
                .....
                .....


                resp.status(200);
                resp.json({
                    "status": 200,
                    "message": "success",
                    "Appoints_Range": joinedDocs,


                });
                return resp;


            });

    });

M
Marcelo Lazaroni

您可以使用聚合管道来完成它,但是自己编写它很痛苦。

您可以使用 mongo-join-query 从您的查询中自动创建聚合管道。

这就是您的查询的样子:

const mongoose = require("mongoose");
const joinQuery = require("mongo-join-query");

joinQuery(
    mongoose.models.Comment,
    {
        find: { pid:444 },
        populate: ["uid"]
    },
    (err, res) => (err ? console.log("Error:", err) : console.log("Success:", res.results))
);

您的结果将在 uid 字段中包含用户对象,您可以根据需要链接任意多个级别。您可以填充对用户的引用,它引用了一个团队,它引用了其他东西,等等。

免责声明:我写了 mongo-join-query 来解决这个确切的问题。


D
Dean Hiller

playORM 可以使用 S-SQL(可扩展 SQL)为您完成此操作,它只是添加了分区,以便您可以在分区内进行连接。


M
Michael Cole

不,看起来你做错了。 MongoDB 连接是“客户端”。和你说的差不多:

目前,我首先得到符合我标准的评论,然后找出该结果集中的所有 uid,获取用户对象,并将它们与评论的结果合并。好像我做错了。

1) Select from the collection you're interested in.
2) From that collection pull out ID's you need
3) Select from other collections
4) Decorate your original results.

这不是一个“真正的”连接,但它实际上比 SQL 连接更有用,因为您不必处理“多”边连接的重复行,而是装饰最初选择的集合。

此页面上有很多废话和 FUD。事实证明 5 年后 MongoDB 仍然是一件事。


'你不必处理“多”边连接的重复行' - 不知道你的意思。你能澄清一下吗?
@MarkAmery,当然。在 SQL 中,一个 nn 关系将返回重复的行。例如朋友。如果 Bob 是 Mary 和 Jane 的朋友,您将得到 Bob 的 2 行:Bob,Mary 和 Bob,Jane。 2 Bobs 是个谎言,只有一个 Bob。通过客户端连接,您可以从 Bob 开始并按照您的喜好进行装饰:Bob,“Mary and Jane”。 SQL 让你用子查询来做这件事,但那是在数据库服务器上做的工作,可以在客户端上做。
M
Max Sherbakov

我认为,如果您需要规范化的数据表 - 您需要尝试其他一些数据库解决方案。

但我在 Git 上找到了 MOngo 的解决方案顺便说一句,在插入代码中 - 它有电影的名称,但 noi 电影的 ID

问题

您有一组 Actors 以及他们制作的一系列电影。

您想要生成一个包含一组 Actors 的 Movies 集合。

一些样本数据

 db.actors.insert( { actor: "Richard Gere", movies: ['Pretty Woman', 'Runaway Bride', 'Chicago'] });
 db.actors.insert( { actor: "Julia Roberts", movies: ['Pretty Woman', 'Runaway Bride', 'Erin Brockovich'] });

解决方案

我们需要遍历 Actor 文档中的每部电影并单独发出每部电影。

这里的问题是在减少阶段。我们不能从 reduce 阶段发出一个数组,所以我们必须在返回的“值”文档中构建一个 Actors 数组。

map = function() {
  for(var i in this.movies){
    key = { movie: this.movies[i] };
    value = { actors: [ this.actor ] };
    emit(key, value);
  }
}

reduce = function(key, values) {
  actor_list = { actors: [] };
  for(var i in values) {
    actor_list.actors = values[i].actors.concat(actor_list.actors);
  }
  return actor_list;
}

注意actor_list 实际上是一个包含数组的javascript 对象。还要注意 map 发出相同的结构。

运行以下命令执行 map/reduce,将其输出到“pivot”集合并打印结果:

printjson(db.actors.mapReduce(map, reduce, "pivot")); db.pivot.find().forEach(printjson);

这是示例输出,请注意“Pretty Woman”和“Runaway Bride”都有“Richard Gere”和“Julia Roberts”。

{ "_id" : { "movie" : "Chicago" }, "value" : { "actors" : [ "Richard Gere" ] } }
{ "_id" : { "movie" : "Erin Brockovich" }, "value" : { "actors" : [ "Julia Roberts" ] } }
{ "_id" : { "movie" : "Pretty Woman" }, "value" : { "actors" : [ "Richard Gere", "Julia Roberts" ] } }
{ "_id" : { "movie" : "Runaway Bride" }, "value" : { "actors" : [ "Richard Gere", "Julia Roberts" ] } }


请注意,此答案的大部分内容(即可以理解的英语部分)是从 MongoDB 食谱中复制的,位于 GitHub 链接上,答案是回答者提供的。
K
Krishna

我们可以使用 mongoDB 子查询合并两个集合。这是示例,评论--

`db.commentss.insert([
  { uid:12345, pid:444, comment:"blah" },
  { uid:12345, pid:888, comment:"asdf" },
  { uid:99999, pid:444, comment:"qwer" }])`

用户——

db.userss.insert([
  { uid:12345, name:"john" },
  { uid:99999, name:"mia"  }])

用于 JOIN 的 MongoDB 子查询——

`db.commentss.find().forEach(
    function (newComments) {
        newComments.userss = db.userss.find( { "uid": newComments.uid } ).toArray();
        db.newCommentUsers.insert(newComments);
    }
);`

从新生成的 Collection 中获取结果——

db.newCommentUsers.find().pretty()

结果 -

`{
    "_id" : ObjectId("5511236e29709afa03f226ef"),
    "uid" : 12345,
    "pid" : 444,
    "comment" : "blah",
    "userss" : [
        {
            "_id" : ObjectId("5511238129709afa03f226f2"),
            "uid" : 12345,
            "name" : "john"
        }
    ]
}
{
    "_id" : ObjectId("5511236e29709afa03f226f0"),
    "uid" : 12345,
    "pid" : 888,
    "comment" : "asdf",
    "userss" : [
        {
            "_id" : ObjectId("5511238129709afa03f226f2"),
            "uid" : 12345,
            "name" : "john"
        }
    ]
}
{
    "_id" : ObjectId("5511236e29709afa03f226f1"),
    "uid" : 99999,
    "pid" : 444,
    "comment" : "qwer",
    "userss" : [
        {
            "_id" : ObjectId("5511238129709afa03f226f3"),
            "uid" : 99999,
            "name" : "mia"
        }
    ]
}`

希望这会有所帮助。


为什么您基本上复制了这个几乎相同的一年前的答案? stackoverflow.com/a/22739813/4186945