Skip to main content

One post tagged with "CRUD"

View All Tags

Hướng Dẫn Cloud Firestore CRUD Đầy Đủ

· 10 min read

Cloud Firestore là một NoSQL database mạnh mẽ của Firebase. Bài viết này sẽ hướng dẫn bạn cách thực hiện CRUD operations (Create, Read, Update, Delete) với Firestore trong Flutter một cách đầy đủ.


1️⃣ Cài Đặt và Setup

1.1 Thêm Dependencies

Thêm vào pubspec.yaml:

dependencies:
firebase_core: ^2.24.2
cloud_firestore: ^4.13.6

Sau đó chạy:

flutter pub get

1.2 Khởi Tạo Firestore

import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'firebase_options.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}

1.3 Cấu Hình Firestore Rules

Trong Firebase Console > Firestore Database > Rules:

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Cho phép đọc/ghi nếu đã đăng nhập
match /{document=**} {
allow read, write: if request.auth != null;
}

// Hoặc public read, authenticated write
match /posts/{postId} {
allow read: if true;
allow write: if request.auth != null;
}
}
}

2️⃣ Cấu Trúc Dữ Liệu Firestore

2.1 Collections và Documents

Firestore có cấu trúc:

  • Collection: Tương tự như table trong SQL
  • Document: Tương tự như row trong SQL
  • Field: Tương tự như column trong SQL
users (collection)
├── user1 (document)
│ ├── name: "Nguyễn Văn A"
│ ├── email: "a@example.com"
│ └── age: 25
└── user2 (document)
├── name: "Trần Thị B"
├── email: "b@example.com"
└── age: 30

2.2 Model Class

class User {
final String? id;
final String name;
final String email;
final int age;
final DateTime createdAt;

User({
this.id,
required this.name,
required this.email,
required this.age,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();

// Convert to Map for Firestore
Map<String, dynamic> toMap() {
return {
'name': name,
'email': email,
'age': age,
'createdAt': Timestamp.fromDate(createdAt),
};
}

// Create from Firestore document
factory User.fromMap(String id, Map<String, dynamic> map) {
return User(
id: id,
name: map['name'] as String,
email: map['email'] as String,
age: map['age'] as int,
createdAt: (map['createdAt'] as Timestamp).toDate(),
);
}

// Create from Firestore document snapshot
factory User.fromDocument(DocumentSnapshot doc) {
return User.fromMap(doc.id, doc.data() as Map<String, dynamic>);
}
}

3️⃣ CREATE - Tạo Dữ Liệu

3.1 Thêm Document với Auto ID

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;

// Thêm document với auto-generated ID
Future<String> addUser(User user) async {
try {
final docRef = await _firestore
.collection('users')
.add(user.toMap());

return docRef.id;
} catch (e) {
throw Exception('Error adding user: $e');
}
}
}

3.2 Thêm Document với Custom ID

// Thêm document với ID tùy chỉnh
Future<void> addUserWithId(String userId, User user) async {
try {
await _firestore
.collection('users')
.doc(userId)
.set(user.toMap());
} catch (e) {
throw Exception('Error adding user: $e');
}
}

3.3 Thêm Document với Merge

// Merge: Nếu document đã tồn tại, chỉ update các field được chỉ định
Future<void> addUserWithMerge(String userId, Map<String, dynamic> data) async {
try {
await _firestore
.collection('users')
.doc(userId)
.set(data, SetOptions(merge: true));
} catch (e) {
throw Exception('Error adding user: $e');
}
}

3.4 Thêm Nested Document

// Thêm document vào subcollection
Future<void> addPostToUser(String userId, Post post) async {
try {
await _firestore
.collection('users')
.doc(userId)
.collection('posts')
.add(post.toMap());
} catch (e) {
throw Exception('Error adding post: $e');
}
}

4️⃣ READ - Đọc Dữ Liệu

4.1 Đọc Single Document

// Đọc một document
Future<User?> getUser(String userId) async {
try {
final doc = await _firestore
.collection('users')
.doc(userId)
.get();

if (doc.exists) {
return User.fromDocument(doc);
}
return null;
} catch (e) {
throw Exception('Error getting user: $e');
}
}

4.2 Đọc Tất Cả Documents

// Đọc tất cả documents trong collection
Future<List<User>> getAllUsers() async {
try {
final snapshot = await _firestore
.collection('users')
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

4.3 Query với Điều Kiện

// Query với where
Future<List<User>> getUsersByAge(int minAge) async {
try {
final snapshot = await _firestore
.collection('users')
.where('age', isGreaterThanOrEqualTo: minAge)
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// Multiple conditions
Future<List<User>> getUsersByAgeAndName(int age, String name) async {
try {
final snapshot = await _firestore
.collection('users')
.where('age', isEqualTo: age)
.where('name', isEqualTo: name)
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

4.4 Sorting và Limiting

// Sort và limit
Future<List<User>> getUsersSortedByAge({int limit = 10}) async {
try {
final snapshot = await _firestore
.collection('users')
.orderBy('age', descending: false)
.limit(limit)
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// Pagination
Future<List<User>> getUsersPaginated({
required int limit,
DocumentSnapshot? startAfter,
}) async {
try {
Query query = _firestore
.collection('users')
.orderBy('createdAt', descending: true)
.limit(limit);

if (startAfter != null) {
query = query.startAfterDocument(startAfter);
}

final snapshot = await query.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

4.5 Real-time Updates

// Stream để lắng nghe thay đổi real-time
Stream<List<User>> getUsersStream() {
return _firestore
.collection('users')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList());
}

// Stream cho single document
Stream<User?> getUserStream(String userId) {
return _firestore
.collection('users')
.doc(userId)
.snapshots()
.map((doc) => doc.exists ? User.fromDocument(doc) : null);
}

5️⃣ UPDATE - Cập Nhật Dữ Liệu

5.1 Update Toàn Bộ Document

// Update toàn bộ document
Future<void> updateUser(String userId, User user) async {
try {
await _firestore
.collection('users')
.doc(userId)
.set(user.toMap());
} catch (e) {
throw Exception('Error updating user: $e');
}
}

5.2 Update Một Số Fields

// Update chỉ một số fields
Future<void> updateUserFields(
String userId,
Map<String, dynamic> fields,
) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update(fields);
} catch (e) {
throw Exception('Error updating user: $e');
}
}

// Ví dụ sử dụng
await updateUserFields('user123', {
'name': 'Tên mới',
'age': 26,
});

5.3 Increment/Decrement

// Tăng/giảm giá trị số
Future<void> incrementUserAge(String userId) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
'age': FieldValue.increment(1),
});
} catch (e) {
throw Exception('Error incrementing age: $e');
}
}

5.4 Array Operations

// Thêm vào array
Future<void> addToArray(String userId, String item) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
'tags': FieldValue.arrayUnion([item]),
});
} catch (e) {
throw Exception('Error adding to array: $e');
}
}

// Xóa khỏi array
Future<void> removeFromArray(String userId, String item) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
'tags': FieldValue.arrayRemove([item]),
});
} catch (e) {
throw Exception('Error removing from array: $e');
}
}

6️⃣ DELETE - Xóa Dữ Liệu

6.1 Xóa Document

// Xóa document
Future<void> deleteUser(String userId) async {
try {
await _firestore
.collection('users')
.doc(userId)
.delete();
} catch (e) {
throw Exception('Error deleting user: $e');
}
}

6.2 Xóa Field

// Xóa một field
Future<void> deleteUserField(String userId, String fieldName) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
fieldName: FieldValue.delete(),
});
} catch (e) {
throw Exception('Error deleting field: $e');
}
}

6.3 Xóa Subcollection

// Xóa tất cả documents trong subcollection
Future<void> deleteUserPosts(String userId) async {
try {
final snapshot = await _firestore
.collection('users')
.doc(userId)
.collection('posts')
.get();

final batch = _firestore.batch();
for (var doc in snapshot.docs) {
batch.delete(doc.reference);
}
await batch.commit();
} catch (e) {
throw Exception('Error deleting posts: $e');
}
}

7️⃣ Transactions và Batch Writes

7.1 Transactions

// Transaction: Đảm bảo atomic operations
Future<void> transferPoints(
String fromUserId,
String toUserId,
int points,
) async {
try {
await _firestore.runTransaction((transaction) async {
// Đọc documents
final fromDoc = await transaction.get(
_firestore.collection('users').doc(fromUserId),
);
final toDoc = await transaction.get(
_firestore.collection('users').doc(toUserId),
);

if (!fromDoc.exists || !toDoc.exists) {
throw Exception('User not found');
}

final fromPoints = fromDoc.data()!['points'] as int;
if (fromPoints < points) {
throw Exception('Insufficient points');
}

// Update
transaction.update(fromDoc.reference, {
'points': FieldValue.increment(-points),
});
transaction.update(toDoc.reference, {
'points': FieldValue.increment(points),
});
});
} catch (e) {
throw Exception('Error transferring points: $e');
}
}

7.2 Batch Writes

// Batch write: Thực hiện nhiều operations cùng lúc
Future<void> batchUpdateUsers(List<String> userIds, Map<String, dynamic> data) async {
try {
final batch = _firestore.batch();

for (var userId in userIds) {
final docRef = _firestore.collection('users').doc(userId);
batch.update(docRef, data);
}

await batch.commit();
} catch (e) {
throw Exception('Error batch updating users: $e');
}
}

8️⃣ Ví Dụ Hoàn Chỉnh: Firestore Service

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;

// CREATE
Future<String> createUser(User user) async {
try {
final docRef = await _firestore
.collection('users')
.add(user.toMap());
return docRef.id;
} catch (e) {
throw Exception('Error creating user: $e');
}
}

// READ - Single
Future<User?> getUser(String userId) async {
try {
final doc = await _firestore
.collection('users')
.doc(userId)
.get();
return doc.exists ? User.fromDocument(doc) : null;
} catch (e) {
throw Exception('Error getting user: $e');
}
}

// READ - All
Future<List<User>> getAllUsers() async {
try {
final snapshot = await _firestore
.collection('users')
.get();
return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// READ - Query
Future<List<User>> getUsersByAge(int minAge) async {
try {
final snapshot = await _firestore
.collection('users')
.where('age', isGreaterThanOrEqualTo: minAge)
.orderBy('age')
.get();
return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// READ - Stream
Stream<List<User>> getUsersStream() {
return _firestore
.collection('users')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList());
}

// UPDATE
Future<void> updateUser(String userId, Map<String, dynamic> data) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update(data);
} catch (e) {
throw Exception('Error updating user: $e');
}
}

// DELETE
Future<void> deleteUser(String userId) async {
try {
await _firestore
.collection('users')
.doc(userId)
.delete();
} catch (e) {
throw Exception('Error deleting user: $e');
}
}
}

9️⃣ Sử Dụng Với UI

9.1 StreamBuilder

class UsersListWidget extends StatelessWidget {
final FirestoreService _service = FirestoreService();

@override
Widget build(BuildContext context) {
return StreamBuilder<List<User>>(
stream: _service.getUsersStream(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}

if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}

if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No users found'));
}

final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('Age: ${user.age}'),
);
},
);
},
);
}
}

🔟 Best Practices

10.1 Indexes

  • ✅ Tạo composite indexes cho queries phức tạp
  • ✅ Firestore sẽ tự động suggest indexes

10.2 Security Rules

  • ✅ Luôn validate data trong security rules
  • ✅ Chỉ cho phép authenticated users
  • ✅ Validate data structure

10.3 Performance

  • ✅ Sử dụng pagination cho large collections
  • ✅ Limit số lượng documents trong queries
  • ✅ Sử dụng indexes cho queries

10.4 Error Handling

  • ✅ Luôn handle errors
  • ✅ Hiển thị thông báo cho người dùng
  • ✅ Log errors để debug

1️⃣1️⃣ Kết Luận

Cloud Firestore giúp bạn:

NoSQL database: Linh hoạt, dễ scale
Real-time updates: Stream data tự động
Offline support: Hoạt động offline
Security: Security rules mạnh mẽ
Scalable: Tự động scale

💡 Lời khuyên: Sử dụng StreamBuilder cho real-time data. Luôn validate data và sử dụng security rules. Tạo indexes cho queries phức tạp.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, Firestore, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, Firestore và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.