bxd

BoxDB는 IndexedDB를 위한 Promise 기반의 브라우저 ORM 입니다.

개념

IndexedDB는 비동기 방식으로 동작하고, 빠르고 강력합니다. 하지만 Promise 를 지원하지 않으며, IndexedDB를 사용하기 위해선 콜백 패턴을 사용해야만 합니다. 이는 레거시 코드처럼 느껴집니다.

이러한 이유로, BoxDB 프로젝트가 시작되었습니다.

그런데 왜 이름이 BoxDB 인가요?

  • 박스는 가볍습니다.
  • 박스에는 모양이 있습니다.
  • 박스에는 무언가를 담을 수 있습니다.
  • 박스는 쉽게 가져갈 수 있습니다.

이것이 BoxDB의 시작점이자, 핵심 개념입니다.

Box는 BoxDB의 핵심입니다. Box는 IndexedDB 내의 객체 저장소를 나타대는 추상화된 객체입니다.

Box는 다른 ORM에서 사용되는 모델과 동일한 개념입니다.

주요 기능

  • Promise 기반의 ORM
  • 사용자 친화적이고 사용하기 쉽습니다
  • 가벼운(< 10kb) IndexedDB 래퍼
  • 의존성 없음
  • 데이터베이스와 객체 저장소 버전 관리
  • 모델(Box)을 통한 데이터 검증과 트랜잭션 제어
  • 트랜잭션으로 ACID(원자성, 일관성, 고립성, 지속성) 보장
  • TypeScript 지원
  • 웹 워커에서 동작

브라우저 지원

ie IE edge Edge firefox Firefox chrome Chrome safari Safari ios-safari iOS Safari samsung Samsung opera Opera
11 12~ 10~ 23~ 10~ 10~ 4~ 15~
  • 여러분의 브라우저에서 기능을 테스트 해보세요. link.
  • IE11 테스트 확인해보기 here.

설치

BoxDB는 npm (혹은 yarn)에서 설치 가능합니다.

1
2
3
yarn add bxd
# 또는
npm install bxd

기본

데이터베이스 준비

만약 데이터베이스가 존재하지 않다면, 새로운 데이터베이스를 생성합니다.

1
2
3
import BoxDB from 'bxd';

const db = new BoxDB('database-name', 1); // 데이터베이스 이름, 버전

Box 생성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// User Box (객체 저장소 이름: user)
const User = db.create('user', {
  _id: {
    type: BoxDB.Types.STRING,
    key: true,
  },
  name: {
    type: BoxDB.Types.STRING,
    index: true,
  },
  age: BoxDB.Types.NUMBER,
});

// Item Box (객체 저장소 이름: item)
const Item = db.create('item', {
  uid: {
    type: BoxDB.Types.STRING,
    index: true,
  },
  name: {
    type: BoxDB.Types.STRING,
    index: true,
  },
  memo: BoxDB.Types.STRING,
}, {
  autoIncrement: true,
});

데이터베이스 버전 업데이트

만약 데이터베이스 버전을 변경하고 싶다면(예: Box 스키마 변경, 새로운 Box 추가 등), 이전 버전보다 큰 숫자로 변경하면 됩니다.

1
2
3
4
// 이전 버전
// const db = new BoxDB('database-name', 1);

const db = new BoxDB('database-name', 2); // 버전 업데이트

데이터베이스 열기

1
2
3
4
5
6
// 박스 정의
// ...

await db.open();

// 여기부터 Box를 사용할 수 있습니다! (데이터 접근 및 제어)

데이터베이스 닫기

주로 특별한 상황에서만 닫습니다. (예: 동일한 데이터베이스를 다시 열어야 할 경우)

1
db.close();

트랜잭션

단일 레코드 추가

1
2
await User.add({ _id: '0', name: 'EMPTY', age: 0 });
await User.add({ _id: '1', name: 'leegeunhyeok', age: 20 });

다중 레코드 추가

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const items = [
  { uid: '0', name: 'unknown 1', memo: '' },
  { uid: '0', name: 'unknown 2', memo: '' },
  { uid: '1', name: 'mac', memo: 'so expensive' },
  { uid: '1', name: 'phone', memo: '' },
  { uid: '1', name: 'desktop', memo: '' },
];

// 비동기 함수 내에서
for (let i = 0; i < items.length; i++) {
  await Item.add(items[i]);
}

레코드 가져오기

1
2
await User.get('1'); // { _id: '1', name: 'leegeunhyeok', age: 20 }
await User.get('999'); // null

레코드 갱신하기

1
await User.put({ _id: '0', name: 'temp' }); // `user._id = '0'` 인 레코드의 `name` 이 'temp' 로 변경됩니다.

레코드 삭제하기

1
await User.delete('0'); // `user._id = '0'` 레코드 삭제

커서로 모든 레코드 가져오기

1
2
3
4
5
6
7
8
9
await Item.find().get();

// [
//   { uid: '0', name: 'unknown 1', memo: '' },
//   { uid: '0', name: 'unknown 2', memo: '' },
//   { uid: '1', name: 'mac', memo: 'so expensive' },
//   { uid: '1', name: 'phone', memo: '' },
//   { uid: '1', name: 'desktop', memo: '' }
// ]

커서로 레코드 가져오기 (인덱스 사용)

1
2
3
await Item.find({ index: 'name', value: 'phone' }).get();

// [{ uid: '1', name: 'phone', memo: '' }]

커서로 레코드 가져오기 (필터 함수 사용)

1
2
3
4
5
6
7
await Item.find(
  null,
  (item) => parseInt(item.uid) % 2 === 1,
  (item) => item.memo.includes('expensive'),
).get();

// [{ uid: '1', name: 'mac', memo: 'so expensive' }]

커서로 레코드 가져오기 (둘 다 사용)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
await Item.find(
  {
    index: 'uid',
    value: BoxDB.Range.equal('1'),
  },
  (item) => !!item.memo,
  (item) => item.memo.includes('exp'),
).get();

// [{ uid: '1', name: 'mac', memo: 'so expensive' }]

커서로 다중 레코드 갱신하기

1
2
3
4
await Item.find(null, (item) => !!item.memo).update({ memo: 'new memo' });

// 전: { uid: '1', name: 'mac', memo: 'so expensive' }
// 후: { uid: '1', name: 'mac', memo: 'new memo' }

커서로 다중 레코드 삭제하기

1
2
3
await Item.find(null, (item) => item.uid === '0').delete();

// `Item.uid = '0'` 인 레코드 모두 삭제

한 트랜잭션에서 다중 작업 처리하기

만약 트랜잭션 작업 중 에러가 발생할 경우, 트랜잭션 이전으로 롤백됩니다.

Transactionable 한 메소드 이름에는 $ 접두사가 붙어있습니다. ($add, $put, $delete, $find)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
await db.transaction(
  User.$add({ _id: '3', name: 'Aiden', age: 16 }),
  Item.$add({ uid: '3', name: 'pencil', memo: 'super sharp' }),
  Item.$add({ uid: '3', name: 'eraser', memo: '' }),
  Item.$add({ uid: '3', name: 'book', memo: '200 pages' }),
  Item.$add({ uid: '3', name: 'juice', memo: '' }),
);

// 새로운 user 추가: { _id: '3', name: 'Aiden', age: 16 }
// 새로운 item 추가: { uid: '3', name: 'pencil', memo: 'super sharp' }
// 새로운 item 추가: { uid: '3', name: 'eraser', memo: '' }
// 새로운 item 추가: { uid: '3', name: 'book', memo: '200 pages' }
// 새로운 item 추가: { uid: '3', name: 'juice', memo: '' }

레코드 수 가져오기

1
await Item.count(); // 7

모든 레코드 지우기

1
2
3
4
5
// A. 커서로 지우기
await User.find().delete();

// B. clear()로 지우기
await User.clear();

웹 워커

1
2
3
4
importScripts('https://cdn.jsdelivr.net/npm/bxd@latest/dist/bxd.min.js');

// 모든 기능 사용 가능!
self.BoxDB;