flask를 기반으로 Mongodb를 사용할 때, mongoengine을 사용하면 NoSQL DB임에도 모델링을 하고(데이터 타입이나 관계를 정의), view를 작성할 때 Django ORM 비스무리하게 쿼리할 수 있다.
사용할 stack, package manager는 다음과 같다.
간단하게 순서를 보면 다음과 같다. (참고 링크)
poetry로 파이썬 프로젝트를 만들었고, root directory에 run.py를 넣었다.
디자인 패턴은 크게 models / views / serializers로 나누었고 각 디렉토리 안에 기능에 따라 파일을 추가할 예정이다.
작동하는 방식은 다음과 같이 이해했다.
❯ tree
.
├── app
│ ├── __init__.py
│ ├── config.py
│ ├── database.py
│ ├── models
│ │ └── user.py
│ ├── serializers
│ │ └── user.py
│ └── views
│ ├── __init__.py
│ └── user.py
├── README.rst
├── poetry.lock
├── pyproject.toml
├── run.py
└── tests
└── __init__.py
아래 내용들은 MongoEngine 공식 문서를 참고했다.
관계형 데이터베이스의 row와 비슷한 개념. 관계형 데이터베이스에서 row들은 테이블에 저장되지만, MongoDB는 document를 collections에 저장한다.
여기서 원칙적으로 다는 점은 데이터베이스 레벨에서 스키마가 필요하지 않다는 점이다.
스키마를 정의하기 위해서, Document를 상속하는 class를 만든다. Field들은 Field object를 document class에 속성으로서 추가해서 지정된다. BSON은 순서에 의존적이기 때문에, document들은 field의 순서에 따라 serialize된다.
from mongoengine import *
import datetime
class Page(Document):
title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.utcnow)
document class로 만들어진 collection의 이름은 소문자로 생성된다.
이걸 왜 알게됐냐면, get test method를 만드는데 분명 User collection에 내용이 있는데도 Nonetype을 읽어올 수 없다는 오류가 떴다. 그래서 post method를 만들어보니까 User가 아니라 user collection이 생성되면서 데이터가 들어갔다.
MongoDB의 장점 중 하나가 dynamic document다. 데이터가 계획되고 정리되는 것, 즉 명확한게 좋긴 하지만 동적이고 확장 가능한 document 스타일이 가능한 시나리오가 있다.
동작하는 것은 Document와 똑같지만 어떤 데이터나 속성도 저장될 수 있다는 특징이 있다. 주의할 점이 하나 있는데, field는 언더바(_)로 시작할 수 없다.
from mongoengine import *
class Page(DynamicDocument):
title = StringField(max_length=200, required=True)
# Create a new page and add tags
>>> page = Page(title='Using MongoEngine')
>>> page.tags = ['mongodb', 'mongoengine']
>>> page.save()
>>> Page.objects(tags='mongoengine').count()
>>> 1
예시: BinaryField, BooleanField, DateTimeField, DictField, ListField, EmailField...etc.
다른 Document 안에 document를 끼워넣는 것이 가능하다.
class Comment(EmbeddedDocument):
content = StringField()
class Page(Document):
comments = ListField(EmbeddedDocumentField(Comment))
comment1 = Comment(content='Good work!')
comment2 = Comment(content='Nice article!')
page = Page(comments=[comment1, comment2])
embedded coument는 dictionary 대신 쓰인다. 일반적으로 embedded document들은 validation을 지원하지 않는 딕셔너리나 custom type으로 쓰일 때 권장된다. 하지만 때때로 저장하고자 하는 구조가 무엇인지 모르기 때문에, 이럴 때 DictField를 사용하게 된다.
Dictionary는 복잡한 데이터, 다른 dictionary들, 리스트, 다른 객체를 참조할 수 있기 때문에 field type 중에 가장 유연하다.
특정 document에서 ReferenceField를 사용하면 다른 document에 저장될 수 있다. constructor에서 첫 번째 인자로 다른 document class를 넣으면 field에 Document 객체들을 할당한다.
class User(Document):
name = StringField()
class Page(Document):
content = StringField()
author = ReferenceField(User)
john = User(name="John Smith")
john.save()
post = Page(content="Test Page")
post.author = john
post.save()
이 때 User 객체는 자동적으로 참조가 되고, Page 객체를 검색했을 때는 역참조된다. 정의 중인 document를 참조하는 ReferenceField를 추가하기 위해서는 'self'
를 사용한다.
class Employee(Document):
name = StringField()
boss = ReferenceField('self')
profile_page = ReferenceField('ProfilePage')
class ProfilePage(Document):
content = StringField()
일대다 관계를 list를 통해 실행한다면, 참조들은 DBRefs에 저장되고 객체의 instance들을 통과시켜야 한다.
class User(Document):
name = StringField()
class Page(Document):
content = StringField()
authors = ListField(ReferenceField(User))
bob = User(name="Bob Jones").save()
john = User(name="John Smith").save()
Page(content="Test Page", authors=[bob, john]).save()
Page(content="Another Page", authors=[john]).save()
참조 필드 중 두 번째 종류는 GenericReferenceField인데, 어떤 종류의 document도 다 참조할 수 있게끔 해준다. 그래서 constructor argument로서 document subclass가 필요하지 않다.
class Link(Document):
url = StringField()
class Post(Document):
title = StringField()
class Bookmark(Document):
bookmark_object = GenericReferenceField()
link = Link(url='http://hmarr.com/mongoengine/')
link.save()
post = Post(title='Using MongoEngine')
post.save()
Bookmark(bookmark_object=link).save()
Bookmark(bookmark_object=post).save()