3. MongoDB for Python
1. MongoDB for Python 설치
- PyMongo에 대한 자세한 내용은 공식 도큐먼트에서 확인할 수 있다.
- MongoDB의 데이터 구조를 이해한 후 진행하면 좋다.
1) pymongo
설치
$(base) pip install pymongo
2) 포트 오픈 확인
- 다음과 같이
27017
번 포트가 열려있는지 확인한다.
$(base) netstat -nlpt | grep 27017
tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN 5402/mongod
2. Making A Connection with MongoClient
- PyMongo로 작업할 때의 첫 번째 단계는 실행 중인
mongod
인스턴스에 대해 MongoClient
를 만드는 것이다.
from pymongo import MongoClient
client = MongoClient()
- 위의 코드는 기본 호스트와 기본 포트에 연결한다.
- 다음과 같이 호스트와 포트를 명시적으로 지정할 수 있다.
client = MongoClient("localhost", 27017)
- 또는 MongoDB URI 형식으로 지정할 수 있다.
client = MongoClient("mongodb://localhost:27017")
3. Getting a Database
- MongoDB의 단일 인스턴스는 여러 독립 Database를 지원할 수 있다.
- PyMongo로 작업할 때
MongoClient
인스턴스에서 Attribute 스타일 액세스를 사용하여 Database에 액세스한다.
db = client.test_database
- Attribute 스타일 액세스가 작동하지 않는 경우 Dictionary 스타일 액세스를 대신 사용할 수 있다.
db = client["test-database"]
4. Getting a Collection
- Collection은 MongoDB에 저장된 Documents의 그룹이며 RDBMS의 테이블과 거의 동일하게 볼 수 있다.
- PyMongo에서 Collection을 가져오는 것은 Database를 가져오는 것과 동일하게 작동한다.
collection = db.test_collection
- Dictionary 스타일 액세스의 경우는 다음과 같다.
collection = db["test_collection"]
- MongoDB의 Database 및 Collection은 Lazy 생성이다.
- 즉, 위의 명령 중 어느 것도 MongoDB 서버에서 실제로 어떤 작업도 수행하지 않았다.
- Database 및 Collection은 첫 번째 Document가 삽입될 때 생성된다.
5. Documents
- MongoDB의 데이터는 JSON 스타일의 Documents를 사용하여 표현 및 저장된다.
- PyMongo에서는
dict
타입을 사용하여 Documents를 나타낸다.
- 예를 들어 다음
dict
타입을 사용하여 블로그 게시물을 나타낼 수 있다.
import datetime
post = {"author": "Mike",
"text": "My first blog post!",
"tags": ["mongodb", "python", "pymongo"],
"date": datetime.datetime.utcnow()}
- Documents에는 적절하게 BSON 타입으로 자동 변환되는 기본 Python 타입(
datetime.datetime
인스턴스 등)이 들어갈 수 있다.
6. Inserting a Document
- Document 하나를 Collection에 삽입하려면
insert_one()
메서드를 사용할 수 있다.
posts = db.posts
post_id = posts.insert_one(post).inserted_id
print(post_id)
# ObjectId('...')
- Document가 삽입될 때
_id
가 포함되어 있지 않은 경우 _id
는 자동으로 추가된다.
_id
값은 Collection 전체에서 고유한 값이어야 한다.
insert_one()
메서드는 InsertOneResult
의 인스턴스를 반환한다.
- 첫 번째 Document를 삽입한 후 실제로 서버에는 게시물 Collection이 생성되었다.
- Database의 모든 Collection을 나열하여 이를 확인할 수 있다.
print(db.list_collection_names())
# [u'posts']
7. Getting a Single Document With find_one()
- MongoDB에서 수행할 수 있는 가장 기본적인 쿼리 유형은
find_one()
이다.
- 이 메서드는 쿼리와 일치하는 단일 Document를 반환하거나 일치하는 항목이 없는 경우
None
을 반환한다.
- 이 메서드는 일치하는 Document 하나를 찾거나 저장된 데이터들의 모양을 볼 때 간단하게 사용할 수 있다.
import pprint
pprint.pprint(posts.find_one())
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'mongodb', u'python', u'pymongo'],
# u'text': u'My first blog post!'}
- 반환된 Document에는 삽입 시 자동으로 추가된
_id
가 포함되어 있다.
- 또한
find_one()
은 결과 Document가 일치해야 하는 특정 요소에 대한 쿼리를 지원한다.
- 다음과 같이
author
가 "Mike"
인 Document로 결과를 제한할 수 있다.
pprint.pprint(posts.find_one({"author": "Mike"})
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'mongodb', u'python', u'pymongo'],
# u'text': u'My first blog post!'}
author
가 "Eliot"
인 Document를 찾아볼 수도 있다.
posts.find_one({"author": "Eliot"})
- 하지만 일치하는 특정 요소가 없기 때문에 아무것도 반환되지 않는다.
8. Querying By ObjectId
print(post_id)
# ObjectId('...')
pprint.pprint(posts.find_one({"_id": post_id}))
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'mongodb', u'python', u'pymongo'],
# u'text': u'My first blog post!'}
- 이때
ObjectId
는 문자열 표현과는 다른 인스턴스이다.
post_id_as_str = str(post_id)
print(posts.find_one({"_id": post_id_as_str}))
# None
- 일반적으로 웹 애플리케이션의 작업은 Requests URL에서
ObjectId
를 가져오고 일치하는 Document를 찾는 것일 것이다.
- 이런 경우
find_one()
에 전달하기 전에 문자열에서 ObjectId
로 변환해야 한다.
from bson.objectid import ObjectId
def get(post_id):
document = client.db.collection.find_one({"_id": ObjectId(post_id)})
- Flask와 같은 웹 프레임워크를 사용할 때는 다음과 같이 항상
ObjectId
로 변환해야 한다.
from pymongo import MongoClient
from bson.objectid import ObjectId
from flask import Flask, render_template
client = MongoClient()
app = Flask(__name__)
@app.route("/posts/<_id>")
def show_post(_id):
post = client.db.posts.find_one({"_id": ObjectId(_id)})
return render_template("post.html", post=post)
if __name__ == "__main__":
app.run()
9. A Note On Unicode Strings
- 일반적인 Python
str
타입이 서버에서 검색될 때는 u'Mike'
처럼 다르게 보인다.
- 그 이유는 MongoDB는 데이터를 BSON 형식으로 저장한다.
- BSON 문자열은 UTF-8로 인코딩되므로 PyMongo는 저장하는 모든 문자열이 유효한 UTF-8 데이터만 포함하도록 해야 한다.
- 유니코드 문자열(
<type 'unicode'>
)은 먼저 UTF-8로 인코딩되어 서버로 입력되고, 검색될 때 다시 디코딩되기 때문이다.
10. Bulk Inserts
- 단일 Document를 삽입하는 것 외에도
insert_many()
의 첫 번째 인수로 list
타입을 전달하여 대량으로 삽입 작업을 수행할 수도 있다.
new_posts = [{"author": "Mike",
"text": "Another post!",
"tags": ["bulk", "insert"],
"date": datetime.datetime(2009, 11, 12, 11, 14)},
{"author": "Eliot",
"title": "MongoDB is fun",
"text": "and pretty easy too!",
"date": datetime.datetime(2009, 11, 10, 10, 45)}]
result = posts.insert_many(new_posts)
print(result.inserted_ids)
# [ObjectId('...'), ObjectId('...')]
insert_many()
의 결과는 이제 삽입된 각 Document에 대해 하나씩 총 두 개의 ObjectId
인스턴스를 반환한다.
new_posts[1]
이 new_posts[0]
과 서로 다른 점은 tags
필드가 없고 title
필드가 추가되었다는 것이다.
- 이는 MongoDB가 Schema-free하다는 것을 의미한다.
11. Querying for More Than One Document
- 쿼리 결과로 하나 이상의 Documents를 얻으려면
find()
메서드를 사용한다.
find()
는 Cursor
인스턴스를 반환하고 이를 사용하면 Iteration을 이용할 수 있다.
- 예를 들어 다음과 같이
posts
Collection의 모든 Documents를 반복할 수 있다.
for post in posts.find():
pprint.pprint(post)
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'mongodb', u'python', u'pymongo'],
# u'text': u'My first blog post!'}
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'bulk', u'insert'],
# u'text': u'Another post!'}
# {u'_id': ObjectId('...'),
# u'author': u'Eliot',
# u'date': datetime.datetime(...),
# u'text': u'and pretty easy too!',
# u'title': u'MongoDB is fun'}
find_one()
과 마찬가지로 Document를 find()
에 전달하여 반환된 결과를 제한할 수 있다.
- 예를 들어
author
가 "Mike"
인 Documents만 가져올 수 있다.
for post in posts.find({"author": "Mike"}):
pprint.pprint(post)
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'mongodb', u'python', u'pymongo'],
# u'text': u'My first blog post!'}
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'bulk', u'insert'],
# u'text': u'Another post!'}
12. Counting
- 얼마나 많은 Documents가 쿼리와 일치하는지 알고 실다면 전체 쿼리 대신
count_documents()
작업을 수행할 수 있다.
- Collection의 모든 Documents 수를 확인할 수 있다.
print(posts.count_documents({})
# 3
- 또는 특정 쿼리와 일치하는 Documents 수를 확인할 수 있다.
print(posts.count_documents({"author": "Mike"})
# 2
13. Range Queries
- MongoDB는 다양한 타입의 고급 쿼리를 지원한다.
- 예를 들어 특정 날짜보다 오래된 게시물로 결과를 제한하고,
author
를 기준으로 결과를 정렬하는 쿼리를 수행할 수 있다.
d = datetime.datetime(2009, 11, 12, 12)
for post in posts.find({"date": {"$lt": d}}).sort("author"):
pprint.pprint(post)
# {u'_id': ObjectId('...'),
# u'author': u'Eliot',
# u'date': datetime.datetime(...),
# u'text': u'and pretty easy too!',
# u'title': u'MongoDB is fun'}
# {u'_id': ObjectId('...'),
# u'author': u'Mike',
# u'date': datetime.datetime(...),
# u'tags': [u'bulk', u'insert'],
# u'text': u'Another post!'}
14. Indexing
- 인덱스를 추가하면 특정 쿼리를 가속화하는 데 도움이 될 수 있으며, Documents를 쿼리하고 저장하는 기능을 추가할 수 있다.
- 예를 들어 특정 키에 대한 값이 이미 인덱스에 존재하는 경우 추가적으로 삽입되는 Documents를 방지할 수 있다.
- 먼저 인덱스를 생성한다.
result = db.profiles.create_index([("user_id", pymongo.ASCENDING)], unique=True)
print(sorted(list(db.profiles.index_information()))
# [u'_id_', u'user_id_1']
- 이제 두 개의 인덱스가 생성되었다.
- 하나는 MongoDB가 자동으로 생성하는
_id
에 대한 인덱스이고, 다른 하나는 방금 생성한 user_id
에 대한 인덱스이다.
user_profiles = [{"user_id": 211, "name": "Luke"},
{"user_id": 212, "name": "Ziltoid"}]
result = db.profiles.insert_many(user_profiles)
- 인덱스는
user_id
가 이미 Collection에 있는 Document를 삽입하는 것을 방지한다.
new_profile = {"user_id": 123, "name": "Drew"}
duplicate_profile = {"user_id": 212, "name": "Tommy"}
result = db.profiles.insert_one(new_profile)
result = db.profiles.insert_one(duplicate_profile)
# Traceback (most recent call last):
# DuplicateKeyError: E11000 duplicate key error index: test_database.profiles.$user_id_1 dup key: { : 212 }
References