피터의 Deta 사용기 (1) Deta Base

Pt J·2021년 6월 26일
0

Story Of My Life

목록 보기
24/31
post-thumbnail

Deta Base (beta)
Instantly usable database with a feature-rich API.
― 기능이 풍부한 API를 이용하여 즉시 사용 가능한 데이터베이스

Deta라는 이름 자체가 Database를 노리고 지은 것 같ㄷ...

Deta Base

Deta Base is great for projects where configuring and maintaining a database is overkill.
― Deta Base는 데이터베이스를 구성 및 유지하기엔 지나친 프로젝트에 적합하다.

프로젝트를 기획하다보면 데이터베이스가 필요하긴 한데, 이를 위한 데이터베이스 관리 시스템을 구축하고 유지하는 건 프로젝트 규모 대비 너무 과한 것 같다고 판단되는 경우가 있을 수 있다.
Deta는 이런 경우에 Deta Base를 사용할 것을 추천하고 있다.

Deta Base의 데이터는 내부적으로는 암호화되어 AWS에 저장되며, NoSQL을 기반으로 한다.
어떤 데이터베이스 이름으로 아이템을 저장함으로써 새로운 데이터베이스를 생성할 수 있다.
한 프로젝트 내에 데이터베이스는 몇 개든지 만들 수 있다.

...프로젝트와 마찬가지로 데이터베이스 삭제 같은 건 보이지 않는다.

메서드

Deta Base는 데이터베이스를 관리하기 위해 다음과 같은 메서드를 지원한다.

  • put ― 데이터베이스에 아이템을 저장하되, 이미 존재한다면 이를 업데이트한다.
  • insert ― 데이터베이스에 아이템을 저장하되, 이미 존재한다면 에러를 일으킨다.
  • get ― key를 통해 데이터베이스로부터 아이템을 가져온다.
  • fetch ― 필터를 통해 데이터베이스로부터 아이템들을 가져온다.
  • delete ― 데이터베이스로부터 아이템을 삭제한다.
  • update ― 데이터베이스 내의 아이템을 업데이트한다.

insert 메서드는 put 메서드에 비해 두 배 정도 느리다는 것을 유의하자.

Deta Base를 사용하는 방법은 크게 세 가지가 있다.
그 방법에 따라 메서드를 호출하는 방법이 달라지니 하나씩 살펴 보도록 하자.
각각의 방법에 대한 예제 코드는 공식 문서에서 확인할 수 있다.

SDK로 사용하기

Deta Base는 Node.js, Python, Go 등의 언어에서 라이브러리를 사용할 수 있다.
각각 다음과 같은 명령어로 설치할 수 있는데

$ npm install deta
$ pip install deta
$ go get github.com/deta/deta-go

FastAPI를 다루다 왔으므로 Python을 기준으로 진행하겠다.

⚠️ 라이브러리 설치는 가상환경에서 하는 걸 잊지 말자.
주변에 Python 프로젝트 하다가 가상환경 꼬여서 운영체제 재설치 하는 친구를 봤다.

$ python3 -m venv venv
$ . venv/bin/activate
(venv)$ pip3 install deta

Project Key project_key 와 데이터베이스 base_name 에 대하여 다음과 같이 Deta Base의 데이터베이스를 사용할 수 있다.

from deta import Deta

deta = Deta("project_key")
db = deta.Base("base_name")

put 메서드

― 데이터베이스에 아이템을 저장하되, 이미 존재한다면 이를 업데이트한다.

put 메서드는 다음과 같다.

put(data: typing.Union[dict, list, str, int, float, bool], key:str = None)
parameterdescriptionremark
data아이템으로서 저장하고자 하는 데이터required
key아이템을 식별하기 위한 값; 미지정 시 자동 생성optional

Deta Base의 아이템은 dict, list, str, int, float, bool 자료형에 해당하는 값을 가질 수 있다.
put 메서드는 성공적으로 실행 시 저장한 아이템을, 오류 발생 시 그 오류를 반환한다.

put_many 메서드

만약 두 개 이상의 아이템을 동시에 저장하고자 한다면 put_many 메서드를 사용할 수 있다.
단, 한 번에 최대 25개의 아이템까지만 가능하다.

put_many 메서드는 다음과 같다.

put_many(items)
parameterdescriptionremark
items아이템을 담고 있는 listrequired

put_many 메서드는 성공적으로 실행 시 해당 아이템들을 성공적으로 집어넣겠다는 약속을, 25개를 초과하는 데이터를 전달했다면 오류를 반환한다.

insert 메서드

― 데이터베이스에 아이템을 저장하되, 이미 존재한다면 에러를 일으킨다.

insert 메서드는 다음과 같다.

insert(data: typing.Union[dict, list, str, int, float, bool], key:str = None)
parameterdescriptionremark
data아이템으로서 저장하고자 하는 데이터required
key아이템을 식별하기 위한 값required

insert 메서드는 성공적으로 실행 시 저장한 아이템을, 인자로 전달된 key 라는 key 값을 가진 아이템 존재 시 에러를 반환한다.

get 메서드

― key를 통해 데이터베이스로부터 아이템을 가져온다.

get 메서드는 다음과 같다.

get(key: str)
parameterdescriptionremark
key아이템을 식별하기 위한 값required

get 메서드는 인자로 전달된 key 라는 key 값을 가진 아이템이 있다면 그것을 dict 자료형 형태로 반환하며, 그렇지 않다면 None 을 반환한다.

fetch 메서드

― 필터를 통해 데이터베이스로부터 아이템들을 가져온다.

fetch 메서드는 다음과 같다.

fetch(query=None, buffer=None, pages=10)
parameterdescriptionremark
querydict 형태의 단일 쿼리 또는 그것의 리스트; 생략 시 최대 1MB의 전체 아이템들optional
buffer페이지 당 반환할 아이템 수optional
page반환할 페이지 수optional

dict 내의 쿼리는 AND 관계에 있으며 list 내의 각각의 쿼리는 OR 관계에 있다.

어떤 속성 attr 에 대하여 다음과 같은 쿼리를 query의 key 값으로 사용할 수 있다.

querykeydescription
equalattr해당 속성이 value 값과 동일한 아이템
not equalattr?ne해당 속성이 value 값과 동일하지 않은 아이템
less thanattr?lt해당 속성이 value 값보다 작은 아이템
greater thanattr?gt해당 속성이 value 값보다 큰 아이템
less than or equalattr?lte해당 속성이 value 값보다 작거나 같은 아이템
greater than or equalattr?gte해당 속성이 value 값보다 크거나 같은 아이템
prefixattr?pfx해당 속성이 value 값으로 시작하는 아이템
rangeattr?r해당 속성이 value 값에 해당하는 범위 내에 있는 아이템
containsattr?contains해당 속성이 value 값을 포함하는 아이템 (문자열 한정)
not containsattr?not_contains해당 속성이 value 값을 포함하지 않는 아이템 (문자열 한정)

왜 마지막 두 녀석만 안 줄여 쓰는거지?

fetch 메서드는 쿼리를 만족하는 아이템들의 제너레이터를 반환한다.

delete 메서드

― 데이터베이스로부터 아이템을 삭제한다.

delete 메서드는 다음과 같다.

delete(key: str)
parameterdescriptionremark
key아이템을 식별하기 위한 값required

delete 메서든는 인자로 전달된 key 라는 key 값을 가진 아이템의 존재 여부와 무관하게 None 을 반환한다.

update 메서드

― 데이터베이스 내의 아이템을 업데이트한다.

update 메서드는 다음과 같다.

update(updates:dict, key:str)
parameterdescriptionremark
updates업데이트할 내용을 담은 dictrequired
key아이템을 식별하기 위한 값required

업데이트에는 다음과 같은 연산이 사용된다.

parameterdescriptionremark
setkey 값으로 사용된 속성이 있을 경우 수정; 없을 경우 추가일반적인 key-value 쌍 사용
increment숫자 속성에 대하여 value 값으로 전달된 수만큼 증가 (default는 value==1)base.util.increment(value)
append리스트 속성에 대하여 value 값으로 전달된 값을 맨 뒤에 추가base.util.append(value)
prepend리스트 속성에 대하여 value 값으로 전달된 값을 맨 앞에 추가base.util.prepend(value)
trim이것을 value로 전달한 속성 제거base.util.trim()

update 메서드는 성공적으로 실행 시 None을, 그렇지 않다면 오류를 반환한다.

HTTP API 사용하기

Project ID project_id 와 데이터베이스 base_name 에 대하여 HTTP API를 통해 Deta Base를 사용할 땐 다음과 같은 URL을 사용한다.

https://database.deta.sh/v1/{project_id}/{base_name}

그리고 Project Key project_key 에 대하여 요청에 다음과 같은 헤더를 추가해야 한다.

'X-API-Key: project_key'
'Content-Type: application/json'

put 엔드포인트

― 데이터베이스에 아이템을 저장하되, 이미 존재한다면 이를 업데이트한다.

PUT /items

payloadtypedescriptionremark
itemsarray저장할 아이템의 리스트required

207 Multi Status

{
    "processed": {
        "items": [
            // items which were stored 
        ]
    },
    "failed": {
       "items": [
           // items failed because of internal processing
       ]
    }
}

insert 엔드포인트

― 데이터베이스에 아이템을 저장하되, 이미 존재한다면 에러를 일으킨다.

POST /items

payloadtypedescriptionremark
itemsarray저장할 아이템의 리스트required

201 Created

{
  "key": {key}, // auto generated key if key was not provided in the request
  "field1": "value1",
  // the rest of the item
}

get 엔드포인트

― key를 통해 데이터베이스로부터 아이템을 가져온다.

GET /items/{key}

UML parametertypedescriptionremark
keystring아이템을 식별하기 위한 값required

200 OK

{
  "key": {key},
  // the rest of the item
}

query 엔드포인트

― 필터를 통해 데이터베이스로부터 아이템들을 가져온다.

POST /query

payloadtypedescriptionremark
querylistdict 형태의 쿼리의 리스트optional
limitint반환할 값의 개수optional
laststring지난 응답 페이지의 마지막 keyoptional

200 OK

{
    "paging": {
        "size": 5, // size of items returned
        "last": "adfjie" // last key seen if paginated, provide this key in the following request
    },
    "items": [
       {
         "key": {key},
         // rest of the item
       },
       // rest of the items
   ]
}

delete 엔드포인트

― 데이터베이스로부터 아이템을 삭제한다.

DELETE /items/{key}

UML parametertypedescriptionremark
keystring아이템을 식별하기 위한 값required

200 OK

{
  "key": {key}
}

update 엔드포인트

― 데이터베이스 내의 아이템을 업데이트한다.

PATCH /items/{key}

payloadtypedescriptionremark
setobject업데이트 또는 생성할 속성optional
incrementobject값을 증감할 속성optional
appendobject맨 뒤에 값을 추가할 리스트 속성optional
prependobject맨 앞에 값을 추가할 리스트 속성optional
deletestring array제거할 속성optional

200 OK

{
   "key": {key},
   "set": {
     // identical to the request
   },
   "delete": ["field1", ..] // identical to the request 
}

Base UI 사용하기

마지막으로 웹 사이트 상의 UI를 사용하는 방법이 있는데, 이건 아직 정식 지원은 아니다.
이에 대한 건 관심 있다면 문서를 참고하도록 하자.

튜토리얼

공식 Python 튜토리얼을 보며 코드를 작성해보았다.
다만, 공식 문서는 Flask를 사용하였으나 나는 FastAPI로 작성하였다.
FastAPI로 인해 Deta를 찾아보기 시작한 만큼 나는 FastAPI와 함께 사용하게 될 확률이 높기에...

Project Key는 코드 안에 포함해서 좋을 게 없기에 dotenv 를 통해 .env 파일에서 읽어오기로 하였다.
따라서 다음과 같이 필요한 라이브러리를 설치했다.

$ pip3 install deta python-dotenv fastapi uvicorn

짧은 예제이므로 단일 파일 내에서 작업해도 되지만 보다 더 확장성을 고려해 작성하였다.
프로젝트 구조는 다음과 같다.

.
├── routers/
│   ├── __init__.py
│   └── users.py
├── .env
├── database.py
└── main.py

초기 설정

먼저 앱을 구성하기 위한 초기 설정을 한다.

main.py

from fastapi import FastAPI

app = FastAPI(
    title='DETA Base Test',
    description='Deta Base test with FastAPI')

database.py

from deta import Deta
import os
from dotenv import load_dotenv

load_dotenv(verbose=True)
PROJECT_KEY = os.getenv('PROJECT_KEY')
PROJECT_ID = os.getenv('PROJECT_ID')
BASE_NAME = os.getenv('BASE_NAME')

deta = Deta(PROJECT_KEY)
db = deta.Base(BASE_NAME)

Project ID나 Base Name까지 환경변수로 설정할 필요는 없지만 일반화를 위해 위와 같이 작성하였다.

생성

예제에서는 다음과 같은 속성을 가진 User 데이터를 저장하는 데이터베이스를 가정한다.

{
    "name": str,
    "age": int,
    "hometown": str
}

따라서 다음과 같은 Pydantic Class를 생성하였다.

routers/users.py

from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    name: str
    age: int
    hometown: str
    key: Optional[str] = None

그리고 POST 요청을 받아 put 메서드를 통해 User 를 생성하는 엔드포인트를 작성하였다.

routers/users.py

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import Optional
from ..database import db

class User(BaseModel):
    name: str
    age: int
    hometown: str
    key: Optional[str] = None

router = APIRouter(prefix='/users', tags=['users'])

@router.post('', status_code=status.HTTP_201_CREATED)
def create_user(user: User):
    if user.key is None:
        del(user.key)
    created = db.put(user.dict())
    return created

main.py 를 다음과 같이 수정하여 router를 적용한다.

main.py

from fastapi import FastAPI
from .routers import users

tags_metadata = [
    {
        'name': 'users',
        'description': 'Operations with users',
    }
]

app = FastAPI(
    title='DETA Base Test',
    description='Deta Base test with FastAPI',
    openapi_tags=tags_metadata)

app.include_router(users.router)

FastAPI의 docs를 통해 정상적으로 작동하는 것을 확인하면,

조회

다음으로 GET 요청을 받아 get 메서드를 통해 User 를 조회하는 엔드포인트를 작성하였다.

routers/users.py

# 생략
@router.get('', response_model=User)
def get_user(key: str):
    user = db.get(key)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail='User not found')
    return user

FastAPI의 docs를 통해 정상적으로 작동하는 것을 확인하면,

수정

PUT 요청을 받아 put 메서드를 통해 User 를 수정하는 엔드포인트를 작성하였다.

routers/users.py

# 생략
@router.put('/{key}', response_model=User)
def update_user(key: str, user: User):
    user.key = key
    updated = db.put(user.dict())
    return updated 

FastAPI의 docs를 통해 정상적으로 작동하는 것을 확인하면,

삭제

DELETE 요청을 받아 delete 메서드를 통해 User를 삭제하는 엔드포인트를 작성하였다.

routers/users.py

# 생략
@router.delete('/{key}')
def delete_user(key: str):
    db.delete(key)

FastAPI의 docs를 통해 정상적으로 작동하는 것을 확인하면,

최종

최종적으로 작성된 엔드포인트는 다음과 같다.

routers/users.py

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from typing import Optional
from ..database import db

class User(BaseModel):
    name: str
    age: int
    hometown: str
    key: Optional[str] = None

router = APIRouter(prefix='/users', tags=['users'])

@router.post('', status_code=status.HTTP_201_CREATED)
def create_user(user: User):
    if user.key is None:
        del(user.key)
    created = db.put(user.dict())
    return created

@router.get('', response_model=User)
def get_user(key: str):
    user = db.get(key)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail='User not found')
    return user

@router.put('/{key}', response_model=User)
def update_user(key: str, user: User):
    user.key = key
    updated = db.put(user.dict())
    return updated 

@router.delete('/{key}')
def delete_user(key: str):
    db.delete(key)

결론

확실히, 사람들이 PaaS를 왜 이용하는지 알 것 같다.
그리고 이거 데이터를 얼마나 쌓아놓든 무료인 거잖아?
정말 비용 걱정 없이 순수하게 프로젝트에 집중하기에 용이한 것 같다.

Deta의 다른 서비스도 빨리 훑어봐야지ㅎ


작성한 코드는 여기에서 확인할 수 있다.

profile
Peter J Online Space - since July 2020

0개의 댓글