Trang chủ

SwiftData sau tám năm dùng Core Data

SwiftData là Core Data với một API mang hình dáng Swift đặt lên trên. Vẫn kho SQLite đó, vẫn persistent container đó, vẫn cách quản lý đồ thị đối tượng đó. Nghĩa là nó thừa hưởng những phần hay và phần lớn các cạnh sắc, với khoảng một phần mười lượng thủ tục rườm rà.

Sau một năm dùng nó, đây là những gì thật sự đã đổi.

Nó chữa được gì

Mô hình là code. Không trình soạn .xcdatamodeld, không các lớp con sinh tự động lệch pha với trình soạn trực quan, không xung đột merge trong một file XML mà chẳng ai đọc nổi.

@Model
final class Book {
    var title: String
    var author: String
    var addedAt: Date

    @Relationship(deleteRule: .cascade)
    var notes: [Note] = []

    init(title: String, author: String) {
        self.title = title
        self.author = author
        self.addedAt = .now
    }
}

Đó là toàn bộ mô hình. Trong Core Data nó là một file XML, một class được sinh ra, và một loạt thuộc tính @NSManaged mà tất cả đều ngầm là optional bất kể trình soạn nói gì.

Tính optional là lương thiện. Một String không optional trong SwiftData thì đúng là không optional. Các thuộc tính sinh ra của Core Data là optional trong Swift kể cả khi được đánh dấu không optional trong mô hình, nghĩa là phải mở ép hoặc ?? ở mọi chỗ dùng.

Truy vấn được kiểm tra kiểu.

@Query(filter: #Predicate<Book> { $0.author == "Ursula K. Le Guin" },
       sort: \.addedAt, order: .reverse)
private var books: [Book]

#Predicate được biên dịch và kiểm tra kiểu. NSPredicate(format: "author == %@") là một chuỗi hỏng lúc chạy nếu bạn gõ sai một key path.

Phần tích hợp với SwiftUI thật sự tốt. @Query quan sát kho dữ liệu và cập nhật view; chẳng có fetched results controller nào phải cấu hình.

Nó giấu đi cái gì

Bên dưới vẫn là Core Data, và điều đó lộ ra vào đúng những lúc tệ nhất. Một cú sập bên trong SwiftData cho bạn một stack trace đầy NSManagedObjectContext và NSPersistentStoreCoordinator, và gỡ nó đòi hỏi phải biết đúng cái tầng mà người ta bảo bạn không cần học.

Luồng thực thi vẫn gắn với context. ModelContext không phải Sendable, và một đối tượng mô hình thuộc về cái context đã lấy nó về. Các quy tắc y hệt Core Data và ít nhìn thấy được hơn nhiều:

@ModelActor
actor ImportActor {
    func importBooks(_ items: [BookData]) throws {
        for item in items {
            modelContext.insert(Book(title: item.title, author: item.author))
        }
        try modelContext.save()
    }
}

@ModelActor là công cụ đúng cho công việc chạy nền, và truyền một Book lấy về từ context này sang context khác vẫn là đúng cái sai lầm cũ — bạn truyền PersistentIdentifier rồi lấy lại từ đầu.

Việc migration ít kiểm soát được hơn. VersionedSchema và SchemaMigrationPlan lo được các trường hợp phổ biến, còn với bất cứ thứ gì liên quan đến biến đổi dữ liệu thì mapping model của Core Data cho bạn nhiều quyền kiểm soát hơn. Đây là mảng tôi cho là thật sự kém chín muồi hơn.

Cảnh báo

Migration nhẹ chạy được cho việc thêm và bớt thuộc tính. Mọi thứ khác — chẻ một entity, biến đổi giá trị, gộp hai thuộc tính — đều cần một giai đoạn migration tùy biến, và kiểm thử nó với dữ liệu người dùng thật là bắt buộc. Một lần migration hỏng lúc khởi động là không cứu được cho người dùng đó nếu không xóa ứng dụng đi.

Chỗ tôi vẫn phải tụt xuống

Các thao tác hàng loạt. Xóa mười nghìn đối tượng qua context sẽ nạp mười nghìn đối tượng vào bộ nhớ. NSBatchDeleteRequest làm việc ở tầng SQL và thì không.

Các truy vấn tổng hợp phức tạp. Đếm, gom nhóm, tính tổng. @Query lấy về các đối tượng; nếu bạn muốn một con số đếm thì lấy hết về rồi gọi .count là lãng phí. fetchCount(_:) lo được trường hợp đơn giản, và ngoài đó ra thì tôi với tới NSFetchRequest bên dưới.

Gỡ lỗi hiệu năng. -com.apple.CoreData.SQLDebug 1 đặt làm tham số khởi chạy sẽ in ra mọi câu lệnh SQL, và nó vẫn là cách nhanh nhất để phát hiện ra view của bạn đang chạy một truy vấn cho mỗi dòng.

Tôi có chuyển đổi một ứng dụng có sẵn không

Chắc là không, trừ khi có lý do khác để động vào tầng lưu trữ.

SwiftData và Core Data cùng tồn tại được trên một kho dữ liệu, nên chuyển đổi dần dần là khả thi. Nhưng công việc là thật, cái lợi chủ yếu nằm ở trải nghiệm viết code chứ không phải ở năng lực, và một stack Core Data đang chạy tốt và đã đúng suốt nhiều năm thì không nợ bạn một cuộc viết lại nào.

Với một ứng dụng mới, tôi sẽ dùng SwiftData không do dự — riêng chuyện mô-hình-là-code và #Predicate đã xứng đáng rồi, và phần tích hợp SwiftUI dẹp đi cả một tầng nối ống.

Điều tôi sẽ nói với người mới bắt đầu

Hãy học ModelContext là gì và nó liên hệ với một ModelContainer ra sao trước khi viết nhiều. API đủ thân thiện để bạn dựng được một ứng dụng chạy được mà không hiểu đồ thị đối tượng bên dưới, và rồi mọi lỗi luồng, mọi cú sập “object was deleted”, và mọi xung đột merge không giải thích được đều trở nên không lập luận nổi.

SwiftData khiến API của Core Data trở nên dễ chịu. Nó không làm mô hình của Core Data biến mất, và cái mô hình đó vẫn là thứ bạn đang thật sự lập trình cùng.