1

I'm trying to perform an 'inner join' like with two MongoDB collections using Flask.

I think I should be using $lookup to achieve this, and my route currently is:

@app.route("/perfumes")
def perfumes():
    perfumes = mongo.db.perfumes.find()
    users = mongo.db.perfumes.aggregate(
        [
            {
                "$lookup": {
                    "from": "mongo.db.users",
                    "localField": "author",
                    "foreignField": "username",
                    "as": "creator",
            }
        }
    ]
)
return render_template("perfumes.html", title="Perfumes", perfumes=perfumes, users=users)

In my template I'm iterating through the perfumes in one collection and would like to access all the data in the users collection, as each perfume item has been created by one user. The perfumes collection has an author field which should be used to link the username field in the users collection. A very simplified (stripped down) version of my template looks like this:

% for perfume in perfumes %}
    {{ perfume.name }}
    Created by {{ <HERE I WANT TO HAVE MY ITEM CREATOR> }} on {{ perfume.date_updated.strftime('%d-%m-%Y') }}
{% endfor %}

What am I missing in order to have this 'join' working and be able to access to all data for that item from both collections?

Thank you!!

NOTE: I want to access more than just the creator's username, like profile picture, etc, that's why I need the join.

UPDATE: These are the structures I'm using for both databases:

{
    "perfumes": {
        "author": "<string>",
        "brand": "<string>",
        "name": "<string>",
        "descritpion": "<text field>",
        "date_updated": "<date>",
        "public": "<boolean>",
        "picture": "<string>"
    },
    "users": {
        "username": "<string>",
        "first_name": "<string>",
        "last_name": "<string>",
        "email": "<string>",
        "is_admin": "<boolean>",
        "avatar": "<string>"
    }
}

So the author in perfumes would be linkable with the username in users.

ngShravil.py
  • 4,742
  • 3
  • 18
  • 30
Guillermo Brachetta
  • 3,857
  • 3
  • 18
  • 36
  • Could you post how you collection looks, few documents and your expected output? – ngShravil.py May 11 '20 at 15:22
  • I just updated my question. So basically I'd like to access each item in my template, and access all the data from its author, which links to the `username` in `users`. `Username` is unique. – Guillermo Brachetta May 11 '20 at 15:38

2 Answers2

2

You can do it with a simple aggregation. Try the below code:

cur = mongo.db.perfumes.aggregate(
    [
        {
            '$lookup': {
                'from': 'users', # You can directly specify on which collection you want to lookup
                'localField': 'author',
                'foreignField': 'username',
                'as': 'creator',
            }
        },
        {
            '$unwind' : '$creator'
        },
        {
            '$project': {
                'username' : '$creator.username',
                'perfumeName' : '$name',
                'date_updated' : '$date_updated',
                'profilePicture' : '$creator.avatar',
                ..., # Rest of the items that you want to display
                '_id': 0
            }
        }
    ]
)

Return the results like below:

return render_template("perfumes.html", title="Perfumes", perfumes=cur) # You don't need to pass users

Display in the template like below:

{% for perfume in perfumes %}
    {{ perfume['perfumeName'] }}
    Created by {{ perfume['username'] }} on {{ perfume['date_updated'].strftime('%d-%m-%Y') }}
{% endfor %}

I hope it helps.

ngShravil.py
  • 4,742
  • 3
  • 18
  • 30
  • This is wonderful! It just works beautifully! Exactly what I needed. I'm so grateful! – Guillermo Brachetta May 11 '20 at 17:07
  • Just an additional question: Say I have a 3rd collection (types) for the types of perfume. How can I also 'join' that one on perfumes.type field so I have all 3 available in the template? – Guillermo Brachetta May 11 '20 at 17:44
  • How can I pass (to another template) just ONE record of that cursor? What's the method to find one in the aggregate? I'm looking to display again all crossed information of a single record (no iteration involved) – Guillermo Brachetta May 12 '20 at 19:48
  • 1
    For joining a 3rd collection, you should use a `$lookup` operator again. You can understand it from this link: https://stackoverflow.com/questions/35813854/how-to-join-multiple-collections-with-lookup-in-mongodb. If you need any further help, you can post a separate question and let me know. – ngShravil.py May 13 '20 at 02:49
  • To find one in aggregation, you should use the `$match` operator on your unique field/primary key. But this will also result in a list of documents, but with length of one. – ngShravil.py May 13 '20 at 02:56
  • You can have a look this this link: https://stackoverflow.com/questions/26375301/is-it-possible-to-get-single-result-in-aggregate – ngShravil.py May 13 '20 at 03:02
  • Dear helper! I posted a new question explaining what I need to achieve with aggregating a third collection. Could you please help me? I'd be extremely grateful!. Here's the new post: https://stackoverflow.com/questions/61801980/aggregating-3-mongodb-collections-python-flask-jinja – Guillermo Brachetta May 14 '20 at 16:05
1

You can create a jinja filter in order to join the information from the two cursors, and then iterate over it on jinja as if it was a list o dicts.

Start creating a custom jinja filter, where aggregated cursor is the output from your mongo.db.perfumes.aggregate line: e.g. page_filters.py file

from .. import app

@app.template_filter()
def get_user_information(perfume_cursor):
    """ Creates a generator to get extra info of user """
    # Transform mongo cursor into a list of dicts
    perfume_cursor = list(perfume_cursor)

    for item in perfume_cursor:
        extra_info = mongo.db.users.find_one({"username": item['author']}, {"first_name": 1, "profile_picture": 1})
        # Declare below your desired user info
        item['user_info'] = dict(first_name=extra_info.first_name,profile_picture=extra_info.profile_picture)
        yield item

To make our filter available in the templates, we just need to import it in our top-level init.py

# Make sure app has been initialized first to prevent circular imports.
from .util import page_filters

Now on jinja, just use the filter

{% for perfume in (perfumes | get_user_information) %}
    {{ perfume['name'] }}
    Created by {{ perfume['user_info']['first_name']}} on {{ perfume['date_updated'].strftime('%d-%m-%Y') }}
{% endfor %}

You should make the query more specific getting only the necassary user fields though, otherwise there might be too much info on your memory. The reason we use Generator, is to provide a means of processing data without loading the entire data-source into memory first.

refs: https://uniwebsidad.com/libros/explore-flask/chapter-8/custom-filters https://www.lachlaneagling.com/reducing-memory-consumption-python/

Kenny Aires
  • 1,338
  • 12
  • 16