MDN 원문 참조: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB#adding_retrieving_and_removing_data
날짜 : 2022.11.10 (문서 내용은 계속 변경되는 부분이라 정리한 날짜를 함께 기록)
위 참조 링크 내용을 정리하였습니다.
[ 데이터 추가, 검색, 제거 (Adding, retrieving, and removing data) ]
하위 목차
- 데이터베이스에 데이터 추가 (Adding data to the database)
- 데이터베이스에서 데이터 제거 (Removing data from the database)
- 데이터베이스에서 데이터 가져오기 (Getting data from the database)
- 데이터베이스의 항목 업데이트 (Updating an entry in the database)
- cursor의 범위와 방향 지정 (Specifying the range and direction of cursors)
- 데이터베이스에 작업을 하려면 트랜잭션(transaction) 시작이 필요
트랜잭션(transaction)
-데이터베이스 객체에서 발생 (정리자 덧, indexedDB.open 의 결과로 받은 IDBDatabase 객체 : IDBDatabase.transaction )
- object store를 지정 (예 : db.transaction(["customers"])
- 트랜잭션에 들어가면, 데이터를 저장 및 요청하는 object store에 접근 가능
- 사용 가능 모드 3가지
[ IDBFactory.open ] (정리자 덧, 예: indexedDB.open("MyTestDatabase", 3); )
: versionchange - 데이터베이스의 "schema" 또는 구조 변경(object store 또는 index를 생성, 삭제)
[ IDBDatabase.transaction ] (정리자 덧, 예: db.transaction(["customers"], "readwrite"))
: readonly - object store의 레코드를 읽기
: readwrite - object store의 레코드를 읽기, 변경(추가, 삭제, 수정)
- IDBDatabase.transaction(storeNames, mode[optional])
: storeNames - 접근하려는 object store의 배열로 정의된 scope
(모든 object store를 포괄하려면, 빈 배열 전달하면 가능, 하지만 빈 배열은 InvalidAccessError를 발생시킨다고 사양에 명시 되어있음)
: mode - readonly 또는 readwrite (미지정 시 기본 readonly)
: return - IDBIndex.objectStore 메서드를 포함한 transaction object
: 정리자 덧, 예: db.transaction(["customers"], "readwrite")
- 올바른 범위(scope)와 모드를 사용하여 데이터 액세스의 속도 향상 팁 :
: 필요한 object store들만 지정 : 겹치지 않는 범위로 여러 트랜잭션을 동시에 실행 가능
: 필요한 경우에만 readwrite 트랜잭션을 지정 : object store에 대해 하나의 readwrite 트랜잭션만 가능
: 참고 - IndexedDB 주요 특성 및 기본 용어(key characteristics and basic terminology)(원문) 문서의 transaction
- 트랜잭션의 수명
: event loop에 밀접하게 연결
: 트랜잭션을 만들고 그것을 사용하지 않고 이벤트 루프로 돌아가면 트랜잭션은 비활성화
: 트랜잭션 활성을 유지하는 유일한 방법은 트랜잭션에 요청을 하는 것
: 요청이 완료되면 DOM event를 받게 되고, 요청이 성공했다고 가정하면, 해당 callback 중에 트랜잭션을 확장할 수 있는 또 다른 기회
: 확장하지 않고 이벤트 루프로 돌아가면 비활성 됨
: 보류중인 요청이 있는 한 트랜잭션은 활성 상태를 유지
: TRANSACTION_INACTIVE_ERR 에러 코드가 표시되기 시작하면 뭔가 잘 못 된 것
- 3가지 타입의 DOM event를 받음: error, abort, complete
: error
- 에러 이벤트는 버블링되며, 생성된 모든 요청에서 에러 이벤트를 받는다.
- 에러의 기본 동작은 에러가 발생한 트랜잭션을 중단하는 것
- 에러 이벤트에서 stopPropagation()을 먼저 호출하여 다른 작업을 해서 에러를 처리하지 않는 한, 전체 트랜잭션이 롤백
- 세분화된 오류 처리가 번거로운 경우, 데이터베이스에 모든 오류 처리기를 추가
: abort
- 에러 이벤트를 처리하지 않거나 트랜잭션에서 abort()를 호출하면, 트랜잭션은 롤백되고 트랜잭션에 abort 이벤트가 발생
: complete
- 보류중인 요청이 완료되어야 complete 이벤트를 받게 됨
* 많은 데이터베이스 작업을 하는 경우, 개별 요청보다 트랜잭션을 추적하는 것이 더 확실한 판단
// 작성자 덧, 위 "많은 데이터베이스 작업을 하는 경우, 개별 요청보다 트랜잭션을 추적하는 것이 더 확실한 판단"에 대한 추가 설명
const transaction = db.transaction(["customers"], "readwrite");
// 트랜잭션을 추적하는 것이 더 확실한 판단
transaction.oncomplete = (event) => { };
transaction.onerror = (event) => { };
const objectStore = transaction.objectStore("customers");
// 개별 요청(추가,제거, 읽기 등)보다 위 트랜잭션을 추적하는 것이 더 확실
const request = objectStore.add(customer); // 추가
const request = objectStore.delete("444-44-4444"); // 제거
const request = objectStore.get("444-44-4444"); // 읽기
데이터베이스에 데이터 추가 (Adding data to the database)
const transaction = db.transaction(["customers"], "readwrite");
// 주의 :이전 실험적 구현에서는 "readwrite" 대신 deprecate된 상수 IDBTransaction.READ_WRITE를 사용
// 이러한 구현을 지원하려는 경우, 다음과 같이 작성 :
// const transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);
- 트랜잭션에서 object store 가져오기. (트랜잭션은 생성할 때 지정한 object store만 가질 수 있음)
- 그런 다음, 원하는 모든 데이터를 추가 가능 (objectStore.add(customer))
// 모든 데이터가 데이터베이스에 추가되면 할 것
transaction.oncomplete = (event) => { console.log("All done!"); };
transaction.onerror = (event) => { console.log("에러 처리를 잊지 말 것!"); };
const objectStore = transaction.objectStore("customers");
customerData.forEach((customer) => {
const request = objectStore.add(customer); //<--- 추가!!!!
request.onsuccess = (event) => {
// event.target.result === customer.ssn;
};
});
- add() 의 result : 추가된 값(value)의 key (event.target.result === customer.ssn; )
(위 예의 경우, 추가된 객체(cusomer)의 ssn, object store(customers)의 key path가 ssn 이므로)
- add() 함수는 데이터베이스에 같은 key를 가진 object가 없어야 함
- 기존 항목을 수정하거나 데이터 존재 여부를 신경쓰지 않으려면, 아래 데이터베이스에서 항목 업데이트(Updating an entry in the database) 부분에서 볼 수 있는 것 처럼 put() 함수를 사용
데이터베이스에서 데이터 제거 (Removing data from the database)
데이터 삭제는 매우 비슷합니다 :
const request = db
.transaction(["customers"], "readwrite")
.objectStore("customers")
.delete("444-44-4444");
request.onsuccess = (event) => {
// 없어졌음!
};
데이터베이스에서 데이터 가져오기 (Getting data from the database)
- 여러 방법으로 조회 가능
: get() - key를 사용하여 단일 값 조회
: openCursor() / getAll() / getAllKeys() - 모든 값 조회 (자세한 설명은 cursor 사용 항목에서 확인)
: index() - 설정된 index key로 get(), openCursor() 조회 (자세한 설명은 인덱스 사용 항목에서 확인)
- get() : key 제공 필수 (objectStore.get("444-44-4444"))
const transaction = db.transaction(["customers"]);
const objectStore = transaction.objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
// 에러 처리!
};
request.onsuccess = (event) => {
// request.result로 처리
console.log(`Name for SSN 444-44-4444 is ${request.result.name}`);
};
위 코드 축약 :
db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = (event) => {
console.log(`Name for SSN 444-44-4444 is ${event.target.result.name}`);
};
위 코드 설명 :
- 하나의 object store만 있으므로, 트랜잭션에 필요한 object store 목록이 아닌 string으로 이름을 전달
- 읽기만 하므로, "readwrite" 트랜잭션이 필요음. mode를 지정하지 않았기 때문에 기본 "readonly" 트랜잭션이 제공
- 여기에 또 다른 미묘한 점은 실제로 request object를 변수에 저장하지 않는 다는 것입니다.
- DOM event는 target으로 request를 갖기 때문에, result 속성을 얻기 위해 event를 사용
(정리자 덧, event.target = request 할당됨, 그렇기 때문에 request.result.name 와 같이 event.target.result.name을 사용 가능)
데이터베이스의 항목 업데이트 (Updating an entry in the database)
- 데이터 가져오기 및 업데이트 후 IndexedDB에 다시 넣기(put)
- 이전 예제 업데이트 :
// 1. objectStore를 생성, 쓰기가 필요하므로 readwrite 트랜잭션을 지정
const objectStore = db.transaction(["customers"], "readwrite").objectStore("customers");
// 2. ssn 값(444-44-4444)으로 식별되는 customer 레코드 요청
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
// 에러 처리!
};
request.onsuccess = (event) => {
// 3. 변수(data)에 요청의 결과(업데이트할 이전 값)를 넣고
const data = event.target.result;
// 4. object의 age 속성을 업데이트
data.age = 42;
// 5. objectStore에 업데이트된 data를 넣는 두 번째 요청(requestUpdate)
const requestUpdate = objectStore.put(data);
requestUpdate.onerror = (event) => {
// 에러 처리
};
requestUpdate.onsuccess = (event) => {
// 성공 - 6. 데이터가 업데이트 됨! (이전 값 덮어씀)
};
};
cursor 사용 (Using a cursor)
- get() 사용 : 조회하려는 key를 알아야 함
- store의 모든 값을 단계별로 원하면 cursor를 사용 :
const objectStore = db.transaction("customers").objectStore("customers");
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(`Name for SSN ${cursor.key} is ${cursor.value.name}`);
cursor.continue();
} else {
console.log("No more entries!");
}
};
- openCursor(query[optional], direction[optional]) :
- query[optional] : key range object 사용으로, 조회되는 항목의 범위(range)를 제한하여 1분 안에 가져올 수 있음
- direction[optional] : 반복하려는 방향(direction)을 지정 (기본: 오름차순)
- success callback(onsucess(event)) : cursor 객체 자체는 요청의 결과입니다(위에서는, event.target.result)
- 실제 kev와 value는 cursor 객체의 key와 value 속성 (cursor.key, cursor.value.name)
- 계속 하려면, cursor에서 continue()를 호출
- 데이터의 끝(또는 openCursor() 요청에 일치 항목이 없으면) success callback을 받지만, result 속성은 undefined
- cursor의 일반적인 패턴, object store의 모든 object를 조회하고 배열에 추가하는 것 :
const customers = [];
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
customers.push(cursor.value);
cursor.continue();
} else {
console.log(`Got all customers: ${customers}`);
}
};
- 참고 : 또는, 이런 것을 처리하기 위해 getAll()을 사용(그리고 getAllKeys()). 다음 코드는 정확히 동일한 작업 :
objectStore.getAll().onsuccess = (event) => {
console.log(`Got all customers: ${event.target.result}`);
};
- object는 느리게 생성되므로, cursor의 value 속성을 보는 것과 관련된 성능 비용이 있음,
- 예를 들어 getAll()을 사용할 때, 브라우저는 모든 object를 한번에 생성해야 합니다.
- 예로, 각각의 key를 보려고 하면, getAll()을 사용하는 것 보다 cursor을 사용하는 것이 더 효율적
- object store의 모든 object의 배열을 얻으려면 getAll()을 사용
인덱스 사용 (Using an index)
- 지금까지의 예제에서, name으로 customer 데이터를 찾으려면, 찾을 때 까지 데이터베이스에 있는 모든 SSN을 반복해야 함
- 이 방식은 매우 느릴 수 있으므로, index를 사용
// 먼저 request.onupgradeneeded에 index를 생성했는지 확인:
// objectStore.createIndex("name", "name");
// 없으면 DOMException 발생.
const index = objectStore.index("name");
index.get("Donna").onsuccess = (event) => {
console.log(`Donna's SSN is ${event.target.result.ssn}`);
};
- index("인덱스명").get("이름")
: "name" 인덱스는 고유하지 않기 때문에, "Donna"라는 이름의 항목은 하나 이상일 수 있음. 이런 경우 항상 가장 낮은 key값의 항목을 반환
- index("인덱스명").openCursor()
: 주어진 인덱스명("name")으로 모든 항목에 접근하려면 cursor를 사용
: 두 가지 유형의 cursor
- 일반(normal) cursor는 object store의 object에 index 속성을 매핑
- key cursor는 index 속성을 object store의 object를 저장하는데에 사용하는 key에 매핑.
: 차이점은 다음 예제 확인
// normal cursor : 모든 customer 레코드 객체 얻기
index.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// cursor.key는 "Bill"과 같은 이름, cursor.value는 전체 객체
console.log(`Name: ${cursor.key}, SSN: ${cursor.value.ssn}, email: ${cursor.value.email}`);
cursor.continue();
}
};
// key cursor : customer 레코드 객체의 key 얻기
index.openKeyCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// cursor.key는 "Bill"과 같은 이름, cursor.value는 SSN
// 저장된 객체의 나머지를 직접 가져올 방법은 없음.
console.log(`Name: ${cursor.key}, SSN: ${cursor.primaryKey}`);
cursor.continue();
}
};
cursor의 범위와 방향 지정 (Specifying the range and direction of cursors)
- 표시되는 값의 범위를 제한하려면 IDBKeyRange 객체를 openCursor() 또는 openKeyCursor()에 첫 번째 인수로 전달
(const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true); index.openCursor(boundKeyRange).onsuccess...)
- 키 범위 (key range)
: 단일 key
: lower(이하,미만) / upper(이상,초과)
: lower(이하,미만)and upper(이상,초과) (정리자 덧, between)
- 경계는 "closed"(예. 주어진 값을 포함한 key 범위) 또는 "open"(예. 주어진 값이 포함되지 않은 key 범위)
- 다음 예를 확인 :
// "Donna"만 일치
const singleKeyRange = IDBKeyRange.only("Donna");
// "Bill"을 포함한, "Bill" 이전(past) 모든 것과 일치
const lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");
// "Bill"을 포함하지 않는, "Bill" 이전(past) 모든 것과 일치
const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);
// "Donna"를 포함하지 않는, 최대 모든 것과 일치
const upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);
// "Donna"를 포함하지 않는, "Bill"과 "Donna" 사이의 모든 것과 일치
const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);
// key 범위 중 하나를 사용하려면, openCursor()/openKeyCursor()의 첫 번째 인수로 전달
index.openCursor(boundKeyRange).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// 일치한 것에 작업
cursor.continue();
}
};
- 오름차순(cursor의 기본 방향)이 아닌 내림차순으로 반복 : 방향 전환은 openCursor()의 두번째 인수로 prev를 전달
objectStore.openCursor(boundKeyRange, "prev").onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// Do something with the entries.
cursor.continue();
}
};
- 방향(direction)만 지정하고 결과 제한은 하지 않으려면, 첫 번째 인수에 null을 전달 :
objectStore.openCursor(null, "prev").onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// Do something with the entries.
cursor.continue();
}
};
- "name" 인덱스는 unique가 아니기 때문에, 같은 이름의 여러 항목이 있을 수 있음
- 인덱스에 대한 cursor 반복 중, 중복을 필터링하려면, direction 파라미터로 nextunique (또는 뒤로 이동하는 경우 prevunique)를 전달
- nextunique 또는 prevunique가 사용될 때, 가장 낮은 key를 갖는 항목이 항상 반환
index.openKeyCursor(null, "nextunique").onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// Do something with the entries.
cursor.continue();
}
};
- 유효한 direction 인수(argument)는 "IDBCursor Constants(원문)"를 참고
IndexedDB 정리 목록
IndexedDB API: IndexedDB 사용 - 1 (개요)
IndexedDB API: IndexedDB 사용 - 2 (저장소(store) 생성 및 구조화)
IndexedDB API: IndexedDB 사용 - 3 (데이터 추가, 검색, 제거)
IndexedDB API: IndexedDB 사용 - 4 (버전 변경 시 다른 탭, 보안, 브라우저 종료 시 경고 등 나머지 내용)
'냐냐한 IT > 냐냐한 실습 기록' 카테고리의 다른 글
IndexedDB API: IndexedDB 주요 특징 및 기본 용어 (0) | 2022.11.16 |
---|---|
IndexedDB API: IndexedDB 사용 - 4 (버전 변경 시 다른 탭, 보안, 브라우저 종료 시 경고 등 나머지 내용) (0) | 2022.11.14 |
IndexedDB API: IndexedDB 사용 - 2 (저장소(store) 생성 및 구조화) (0) | 2022.11.09 |
IndexedDB API: IndexedDB 사용 - 1 (개요) (0) | 2022.11.09 |
IndexedDB API: 프로그래머 친화적인 indexedDB 라이브러리 (0) | 2022.11.08 |