본문 바로가기

Flutter/00 Legacy

[Flutter] CRUD with FirebaseFirestore & FirebaseStorage

FirebaseFirestore 에 대한 기본 개념을 안다면 아래의 CRUD 개념을 이해하는데 도움이 될 것 입니다.

2022.08.12 - [Flutter/06 Basic] - [Flutter] Firestore 구조 알아보기

 

[Flutter] Firestore 구조 알아보기

Concept CollectionReference add() - 새로운 document 추가 doc() - return documentReference query 생성 - .get()을 통해서 querySnapshot 리턴 snapshot() - return stream get() - return querySnapshot D..

unsungit.tistory.com

 

 

 

firebase에 CRUD 하는 기본 샘플코드.

 

/* 플러그인 정보 */

firebase_core: ^0.7.0
firebase_storage: ^7.0.0
cloud_firestore: ^0.16.0

 

 

1. Create  -  (doc().set or collection().add)

2개의 차이는 doc의 ID를 설정가능 여부이다. 

doc().set 는 doc('abc').set 처럼 doc ID 를 설정가능하다, 공백으로하면 collection().add 과 동일하게 자동생성된다. 

 

/*  Create  */
var doc = FirebaseFirestore.instance.collection('post').doc(); 
doc.set({
  'id': doc.id,
  'datetime' : DateTime.now().toString(),
  'displayName': 'MrKim',
  'photoUrl': photoUrl,
});

 

/*  Create  */
var doc = FirebaseFirestore.instance.collection('post'); 
doc.add({
  'id': doc.id,
  'datetime' : DateTime.now().toString(),
  'displayName': 'MrKim',
  'photoUrl': photoUrl,
});

 

 

 

2. Read

 

/* Read */
// collection 하위의 모든 document 정보 읽고 리스트로 데이터 관리
Future _loadTestResult() async {

  var result = await FirebaseFirestore.instance
    .collection('post') //
    .doc(docId) // chapter_code
    .collection('post_sub')
    //.where('email', isEqualTo: 'aaa@gmail.com') // where 조건이 필요한 경우.
    .get()
    .then((QuerySnapshot querySnapshot) => {
    querySnapshot.docs.forEach((doc) {
      _testResult.add(doc.data()); //모든 document 정보를 리스트에 저장.
    })
  });

  if (result.toString() != null) {
    //리스트 관련 후처리 필요한 경우 여기서 처리함.
  }
}


//특정 collection에 포함된 documents 갯수 가져오기
void _getSubCnt() {
  FirebaseFirestore.instance
    .collection('post')
    .get()
    .then((snapShot) {
      qTotal = snapShot.docs.length;
  });
}

// tree 구조로 하위 docment 찾으면서 필요한 정보 읽기.
// collection(post) > all document > collection(post_sub) > 모든 document 관리.
Future multiCollection() async {

  var result = await FirebaseFirestore.instance
    .collection('post')
    .get()
    .then((QuerySnapshot querySnapshot) => {
      //1차 collection의 모든 document 관련 처리
      querySnapshot.docs.forEach((doc) async {

        var resultSub = await FirebaseFirestore.instance
          .collection('post')
          .doc(doc.data()['docId']) // forEach에서 가져옴
          .collection('post_sub')
          .get()
          .then((QuerySnapshot querySnapshot1) => {
            //2차 collection의 모든 document 관련 처리
          });
        if(resultSub.toString() != null) {
          // 후처리 작업이 필요한 경우.
        }
      }),
  });
  if(result.toString() != null) {
    첫번째 collection 관련 후처리 필요한 경우
  }
}


// doc 에서 필드를 읽고 후처리 하는 예시
  void _getUserInfo() {
    FirebaseFirestore.instance
      .collection('user_info')
      .doc(widget.user.uid)
      .get()
      .then((doc) async {
        if (doc.exists) { // 기 사용자 확인
          var _email = doc.data();
          this._userInfo['userType'] = _email['user_type'];
          this._userInfo['userLangType'] = _email['language'];

        } else { // 신규 사용자 등록시
          // doc.data() will be undefined in this case
          var result = await Navigator.push(context,
            MaterialPageRoute(builder: (context) => InformPage(widget.user))
          );
          try{
            if (result['complete'] == null) { // 정보입력이 완료되지 않음
              FirebaseAuth.instance.signOut();
              _googleSignIn.signOut();
            }
            else {// 정보입력이 완료
              this._userInfo['userType'] = result['user_type'];
              this._userInfo['userLangType'] = result['language'];
            }
          } catch (error) {// 정보입력이 완료되지 않음
            FirebaseAuth.instance.signOut();
            _googleSignIn.signOut();
          }
        }

      });
  }

 

추가 힌트, 알면 쉬운데, 모르면 개고생 ~~ factory 패턴 한번 쓸려고 하다가 밤새 코딩중 ~~

 

// 위에서 자세히 보면 collection 으로 마무리되면 리스트 타입으로 반환되고
// doc 으로 마무리하면 json/map 형식으로 반환된다.
// 그래서 factory 패턴을 사용하기 위해서는 리스트 형식이 필요하므로
// collection + where 형식으로 처리하면 원하는 결과를 얻을수 있다

  LoginUser get curUser => _curUser.value;

  Future getUserInfo(String uid) async {
    var result = await FirebaseFirestore.instance
        .collection('user_info')
        .get();

    final userList = result.docs.map((user) {
      return LoginUser.fromJson(user);
    }).where((user) {
      return (user.uid == uid);
    }).toList();

    if (userList.isNotEmpty) {
      _curUser(userList.first); // or _curUser(userList[0])
      print(userList.length);
      print(curUser.photoURL);  // _curUser.photoURL.value 이렇게 접근 불가(원인 아직 모름)
    }

    return userList.length.toInt();

  }

 

DocumentSnapshot, QuerySnapshot, QueryDocumentSnapshot 예시

 

Future<ItemModel2> getItem(String itemKey) async {
// .doc 의 리턴 타입은 DocumentReference
  DocumentReference<Map<String, dynamic>> docRef =
      FirebaseFirestore.instance.collection(COL_ITEMS).doc(itemKey);

// DocumentReference.get 의 리턴 타입은 DocumentSnapshot
  final DocumentSnapshot<Map<String, dynamic>> documentSnapshot = await docRef.get();

// documentSnapshot.data() 은 nullable 이므로 "!" 필요
  ItemModel2 itemModel = ItemModel2.fromJson(documentSnapshot.data()!);
  itemModel.reference = documentSnapshot.reference;

  return itemModel;
}

// .collection 의 리턴 타입은 CollectionReference
Future<List<ItemModel2>> getItems(String userKey) async {
  CollectionReference<Map<String, dynamic>> collectionReference =
      FirebaseFirestore.instance.collection(COL_ITEMS);

// collectionReference.get 의 리턴 타입은 QuerySnapshot
  QuerySnapshot<Map<String, dynamic>> snapshots = await collectionReference.get();

  List<ItemModel2> items = [];

// QuerySnapshot.docs 의 타입은 QueryDocumentSnapshot
  for (var snapshot in snapshots.docs) {
// documentSnapshot.data() 은 nullable 아니므로 "!" 필요 없음,
    ItemModel2 itemModel = ItemModel2.fromJson(snapshot.data());
    itemModel.reference = snapshot.reference;
    items.add(itemModel);
  }

  return items;
}

 

 

3. Update

 

/* Update */
// 특정 document 에 데이터 update
var doc = FirebaseFirestore.instance
  .collection('post')
  .doc(docId);

doc.update({
  'question_cnt': _questionCount,
});

 

 

 

4. Delete

 

/* Delete */
// collection에 속한 모든 documents 삭제(document에 연관된 사진들도 삭제)
FirebaseFirestore.instance
  .collection('post')
  .get()
  .then((snapshot) async{ //사진 삭제때문에 async 옵션 필요한 경우 있음
    for (DocumentSnapshot ds in snapshot.docs) { // 하위 documents 모두 삭제
      ds.reference.delete();
      //document가 포함한 사진을 찾아서 Storeage에서 삭제.
      final ref = FirebaseStorage.instance.refFromURL(ds['photoUrl']); //photoUrl 사진관련 필드 정보
      ref.delete();
      
    }
});

//특정 document만 삭제 및 해당 게시물에 포함된 사진 삭제  
FirebaseFirestore.instance
  .collection('post')
  .doc(docId) // 특정 document 아이디 정보
  .delete()
  .catchError((e) {
    print(e);
}).then((onValue) async{ //사진 삭제때문에 async 옵션 필요한 경우 있음
  final ref = FirebaseStorage.instance.refFromURL(photoUrl); //photoUrl 사진관련 필드 정보
  ref.delete();
});

 

 

 

5. field 가 존재하지 않을때 처리 - 새로운 필드를 추가했는데, 해당 필드를 먼저 읽고 후 처리 필요한 경우.

  • 좋아요 클릭시, 사용자 키를 이용하여 중복 방지처리함
  • 사용자 키가 중복일 경우 set 으로 중간처리하고 list 로 변환하면 더 좋음.
// ['favorite'] 필드(array 타입)가 없는데, 읽으면 오류가 발생한다.
// 그래서 try { if{} else{} } catch {} 구문으로 처리가 필요함.
// 경우에 따라 try 에서 오류나서 catch 로 넘어가는 경우도 있고,
// null 이 할당되어 try 내부의 else 에서 처리되는 경우 있음.

FirebaseFirestore.instance
    .collection(document['teacher_uid']) // post
    .doc(document['chapter_code'])
    .get()
    .then((doc) {
      var doc1 = FirebaseFirestore.instance
          .collection(document['teacher_uid']) // post
          .doc(document['chapter_code']);

      List<dynamic> _tmp = [];
      Set<dynamic> _set = Set();

      try{
        // doc.data()['favorite'] 필드가 존재하지 않아서 
        // try에서 오류나고 catch 로 넘어가는 경우를 예상했으나, 
        // 아래의 경우는 _tmp 에 null 이 할당되서 else 구문으로 넘어가는 경우임.
        // 그래서 else와 catch 내부 처리를 동일하게 하여 모든 경우를 대비함.
        
        _tmp = doc.data()['favorite'];
        if(_tmp != null){
          _set = _tmp.toSet();
          _tmp = _set.toList();
          if (_favorite[document['chapter_code']]){
            _tmp.add(document['student_uid'].toString());
          } else {
            _tmp.remove(document['student_uid'].toString());
          }
          _set = _tmp.toSet();
          _tmp = _set.toList();
          print('>>>' + _tmp.toString());
          doc1.update({
            'favorite' : _tmp,
          }).then((onValue) {
            setState(() {});
          });
        } else {
	      print('>>> else: 에서 처리함');
          doc1.update({
            'favorite' : [document['student_uid'],],
          }).then((onValue) {
            setState(() {});
          });
        }
      } catch (error) {// 정보입력이 완료되지 않음
        print('>>> error: ' + error.toString());
        doc1.update({
          'favorite' : [document['student_uid'],],
        }).then((onValue) {
          setState(() {});
        });
      }
});

 

 

 

runTransaction/transaction.set

- 여러군데에 동시에 create 를 진행가능, 둘중에 하나라도 오류가 발생하면 두군데 모두 롤백 처리됨

 

Future createNewItem(ItemModel2 itemModel, String itemKey, String userKey) async {
  // 신규 작성글을 업로드하기 위한 위치 설정
  DocumentReference<Map<String, dynamic>> itemDocRef =
      FirebaseFirestore.instance.collection(COL_ITEMS).doc(itemKey);
  final DocumentSnapshot documentSnapshot = await itemDocRef.get();

  // 신규 작성글을 사용자 정보의 하위 콜랙션에 추가, No SQL 에서는 역정규화 하는것이 좋을때가 있음,
  DocumentReference<Map<String, dynamic>> userItemDocRef = FirebaseFirestore
      .instance
      .collection(COL_USERS)
      .doc(userKey)
      .collection(COL_USER_ITEMS)
      .doc(itemKey);

  // 신규 작성글이 없다면 저장,
  if (!documentSnapshot.exists) {
    // await itemDocRef.set(itemModel.toJson());
    await FirebaseFirestore.instance.runTransaction((transaction) async {
      // 2개중에 하나라도 오류가 발생하면 모두 롤백한다,
      transaction.set(itemDocRef, itemModel.toJson());
      transaction.set(userItemDocRef, itemModel.toMinJson());
    });
  }
}