Blog

  • Dynamic Member Lookup in Swift

    Dynamic Member Lookup in Swift

    Chắc hẳn chúng ta đã từng phải viết những đoạn code dài dòng kiểu như thế này chỉ để lấy được 1 giá trị cuối cùng ở cuối 1 chuỗi chaining:

    apiManager.configuration.networkConfiguration.baseHeaders
    apiManager.configuration.networkConfiguration.baseURL

    Việc này có 1 vài hạn chế:

    • Quá dài chỉ để lấy ra 1 vài giá trị, chưa kể nó bị lặp lại ở nhiều nơi
    • Không set private cho configuration được vì như vậy consumer của API sẽ k lấy được giá trị của baseHeaders/baseURL.
    • Leak detail implementation cho consumer của API.

    Để giải quyết vấn đề này, ta có thể sử dụng Dynamic Member Lookup

    Table of contents

    • Giới thiệu
    • Dynamic Member Lookup với ExpressibleByStringLiteral
    • Dynamic Member Lookup với KeyPath
    • Kết luận

    Giới thiệu

    Apply this attribute to a class, structure, enumeration, or protocol to enable members to be looked up by name at runtime.

    By Apple

    Sử dụng Dynamic Member Lookup bằng cách:

    • Sử dụng @dynamicMemberLookup attribute cho class/struct/enum/protocol bạn muốn support
    • Khai báo subscript subscript(dynamicMember:). Kiểu dữ liệu của dynamicMember có thể là KeyPath, hoặc 1 kiểu dữ liệu conform ExpressibleByStringLiteral

    Dynamic Member Lookup với ExpressibleByStringLiteral

    1. Dòng @dynamicMemberLookup đánh dấu struct User để cho biết rằng nó hỗ trợ tính năng dynamicMemberLookup.
    2. Phương thức subscript được sử dụng để xử lý truy cập vào thành viên của đối tượng. Tham số dynamicMember đại diện cho tên thành viên được truy cập, và phương thức sẽ trả về một chuỗi đơn giản là "Tuan" để minh họa cho việc truy cập thành viên.
    3. user.name được truy cập, sử dụng cú pháp giống như truy cập vào một thuộc tính của đối tượng. Khi đó, phương thức subscript của struct User được gọi để xử lý việc truy cập thành viên và trả về chuỗi "Tuan".

    Chúng ta cũng có thể implement nhiều subscript(dynamicMember) khác nhau cho cùng 1 đối tượng support dynamicMemberLookup

    Tuy nhiên, đừng quá lạm dụng sử dụng subscript(dynamicMember:) với ExpressibleByStringLiteral. Nó có 1 vài nhược điểm:

    • Swift tất nhiên sẽ không gợi í cho consumer của class là phải gọi name/age để lấy thông tin. Chúng ta sẽ phải bảo với consumer là phải dùng dynamicMemberLookup để lấy thông tin => Bad API design.
    • Làm mất tính safety của Swift.

    Having to be aware of internal implementation details is usually a bad sign when it comes to API design

    Dynamic Member Lookup với KeyPath

    Sử dụng Dynamic Member Lookup với KeyPath là 1 sự lựa chọn tuyệt vời khi nó vẫn giữ được tính safety của Swift, đồng thời vẫn có recommend cho user.

    @dynamicMemberLookup
    struct APIManager {
        private var configuration: Configuration
    
        init(configuration: Configuration) {
            self.configuration = configuration
        }
    
        subscript<T>(dynamicMember keyPath: KeyPath<NetworkConfiguration, T>) -> T {
            return configuration.networkConfiguration[keyPath: keyPath]
        }
    
        subscript<T>(dynamicMember keyPath: WritableKeyPath<NetworkConfiguration, T>) -> T {
            get {
                return configuration.networkConfiguration[keyPath: keyPath]
            }
    
            set {
                configuration.networkConfiguration[keyPath: keyPath] = newValue
            }
        }
    }
    
    var apiManager = APIManager(
        configuration: Configuration(networkConfiguration: .init(
            baseURL: URL(string: "https://www.google.com.vn/")!,
            baseHeaders: ["key":"someValue"])
        )
    )
    
    apiManager.baseHeaders
    apiManager.baseHeaders["anotherKey"] = "anotherValue"
    apiManager.baseHeaders
    Bạn có thể thấy, Swift vẫn recommend sử dụng baseHeadersbaseURL cho consumer mà k leak detail implementation của API -> Good API design

    Kết luận

    • DynamicMemberLookup được sử dụng rộng rãi trong các thư viện phần mềm như Alamofire, Combine, SwiftUI và nhiều thư viện khác để giảm thiểu việc viết mã boilerplate và tăng tính linh hoạt của ứng dụng.
    • Với DynamicMemberLookup, bạn có thể viết mã Swift hiệu quả hơn và tránh việc phải sao chép và dán nhiều mã lặp đi lặp lại.
    • DynamicMemberLookup với ExpressibleByStringLiteral giải quyết bài toán khi app cần tương tác với WebView khá tốt, vì ta phải handle javascript code. Đối với các case còn lại, đừng quá lạm dụng DynamicMemberLookup :3

    Thanks for reading, guys ?

  • Value type và Reference type – ARC in Swift

    Value type và Reference type – ARC in Swift

    Value type và Reference type – ARC in Swift

    1. Value type và Reference type:

    Là một lập trình viên iOS chúng ta biết rằng, Apple khuyên chúng ta sử dụng struct và enum nhiều hơn. Lí do là chúng là kiểu Value type, dẫn tới việc bộ nhớ của chúng không ảnh hưởng tới nhau. An toàn hơn khi truy cập và sử dụng

    Vậy với Class, Closure có escaping thì sao, chúng là kiểu reference type -> Câu hỏi đặt là ra là sao để quản lý vùng nhớ của chúng, để tránh tạo instance lẫn lộn, gây ra leak memory. Câu trả lời của Apple đó là ARC

    Noted:
    Class sẽ được khởi tạo trong heap -> nên có thể sử dụng cho data có size lớn
    Struct sẽ được khởi trong stack -> nên không thể dùng cho data có size lớn, nó sẽ dẫn tới phình stack nhanh. Vậy nên chúng ta chỉ nên dùng cho những data có size nhỏ.


    Còn Heap với Stack là gì, hẹn các bạn ở một bài khác nhé.

    2. Vậy ARC là gì?

    Automatic Reference Counting: Dịch theo nghĩa đen tiếng Việt là tự động đếm số lượng reference. 

    Theo Apple định nghĩa về ARC: là cơ chế Swift sử dụng để theo dõi và quản lý bộ nhớ mà ứng dụng sử dụng. Từ đó giúp bạn phát triển ứng dụng mà không cần quan tâm tới việc quản lý bộ nhớ như một số ngôn ngữ khác, hay là Objective – C chưa implement ARC. ARC sẽ tự động frees up( hủy bỏ vùng nhớ của các strong reference = 0)

    ARC chỉ áp dụng cho instance của classes. Structures và Enumerations là kiểu value types, không phải kiểu references types và không được lưu trữ dưới dạng tham chiếu.

    3. ARC hoạt động như thế nào?

    Khi mà chúng ta tạo một instance của một class, ARC sẽ phân bổ một vùng nhớ của bộ nhớ để lưu trữ thông tin của instance đó

    Khi mà instance không được sử dụng trong một thời gian đủ dài, ARC sẽ tự động frees up memory được sử dụng bởi instance đó để giải phóng vùng nhớ để sự dụng vào các mục đích khác,

    Để xác định một biến không còn được sử dụng, ARC sẽ đếm số lượng strong reference của các object khác đang trỏ vào chính nó. Chỉ khi số lượng strong reference = 0 thì biến đó mới được giải phóng khỏi bộ nhớ.

    4. Strong reference


    Ở đây chúng ta có pen1, pen2, pen3 đều là kiểu strong reference chỉ về vùng nhớ của class Pen.

    Khi ta gán:

    pen1 = nil

    pen2 = nil

    => ARC sẽ đếm strong reference trong trường họp này sẽ là = 1. vì còn 1 intance được giữ lại bởi pen3. Nên trong trường hơp này ARC không giải phóng vùng nhớ cho Pen.

    5. Reference giữa các Class

    Khi chúng ta gán person và car = nil thì ta thấy person có car đang chỉ tới vùng nhớ của của Car kiểu strong reference, khi person = nil, thì strong reference này = 1 ( instance chưa bị hủy đi), tương tự với car, tồn tại instance trỏ tới vùng nhớ của Peson chưa được giải phóng:

    -> Trong trường hợp này strong reference = 2

    -> Tạo retain cycle, dẫn tới leak memory ( Khi bộ nhớ bị đầy dẫn tới app bị chậm, hoặc dừng lại, hoặc thoát ra đột ngột)

    Vậy để giải quyết bài toán trên Apple đã đưa ra hai kiểu định nghĩa reference đó là weak và unowned.

    6. Weak reference

    Khi chúng ta tạo một biến là kiểu weak reference, thì ARC sẽ hiểu reference counting của nó = 0 ( vì nó không phải là strong reference nên không làm tăng retain count. Từ bộ đếm counting không tăng lên không dẫn tới việc leak memory.

    Ở đây khi chúng ta set cong = nil, Car sẽ không còn reference nào chỉ đến. Từ đó vùng nhớ được giải phóng, và khi đó strong reference của Person trỏ đến Car cũng bị hủy.

    Set honda = nil, ở thời điểm này không còn strong nào trỏ đến -> instance đuợc giải phóng khỏi vùng nhớ. Không làm leak memory

    7. Unowned reference

    Tương tự như weak thì khi tạo một biến kiểu unowned, Counting Reference không tăng lên. ARC không giữ instance  và không gây ra leak memory.

    Note: “ Unowned không sử dụng cho kiểu dữ liệu optional, một instance A unowned reference ( trỏ ) đến một instance B mà instance B đó có vòng đời bằng hoặc lớn hơn instance A, nên khi bạn truy cập một biến unowned sau khi nó đã giải phóng khỏi vùng nhớ sẽ dẫn tới crash. Vì vậy khi dùng unowned cần cẩn thận!”

    Tương tự như weak khi chúng ta set cong = nil, Car sẽ không còn reference nào chỉ đến. Từ đó vùng nhớ được giải phóng, và khi đó strong reference của Person trỏ đến Car cũng bị hủy.

    set honda = nil, ở thời điểm này không còn strong nào trỏ đến -> instance đuợc giải phóng khỏi vùng nhớ. Không làm leak memory

    8. Strong Reference Cycles for Closures:

    Closure cũng giống như class là reference types.

    Trong một class, nếu một property là closure và trong closure đó lại dùng lại property / method của class nữa ( xài self.property ) thì sẽ xảy ra hiện tượng retain cycle như các ví dụ ở trên.

    Ta có ví dụ sau:

    9. Resolving Strong Reference Cycles for Closures

    Để giải quyết vấn đề retain cycle trong closure, swift đã cung cấp cho chúng ta một giải pháp đó là: closure capture list.

    Capture list sẽ quy định luật để lấy giá trị của property trong closure. Tức là lấy self.property / self.method như thế nào. 

    Ta dùng syntax sau ở phần body của closure:

    [ weak self ] in

    hoặc:

    [ unowned self ] in

    hoặc lấy nhiều property / method cũng được: 

    [ weak self, unowned self.property, … ] in

    carbon-8

    Thanks for reading!

  • iOS/Swift In-App Purchase (P2)

    iOS/Swift In-App Purchase (P2)

    Xin chào mọi người. Phần 1 mình đã hướng dẫn các bạn setup môi trường và setup payments rồi. Bài này mình sẽ tiếp tục với implement code và thực hiện test sau khi implement.

    Implement

    Đầu tiên chúng ta enable In-App Purchase trong Capability

    Bắt đầu implememt code

    Ở bài trước mình đã tạo một item tên là Apple Book và đặt productID là Demo.TechoverInAppPurchase.co.Apple.Book

    Cũng trong bài trước mình có nhấn mạnh productID rất quan trọng và phải matching trong code, bên dưới đây mình sẽ thực hiện điều đó.

    Đầu tiên mình tạo ra một file plist mình để tên là IAPProductIDs, mục đích của file này là để quản lý tất cả các purchase producIDs của ứng dụng, thường thì một ứng dụng sử dụng In-App Purchase sẽ có nhiều item thanh toán, cũng như nhiều giá trị khác nhau, mỗi giá item đó tương ứng với một productID, Hiện tại demo của mình chỉ có 1 item thanh toán vì thế trong file plist của mình chỉ có một item và value của item đó là productID.


    Tiếp theo mình tạo ra một class IAPManager như bên dưới:

    import Foundation
    import StoreKit
    
    class IAPManager: NSObject {
        
        // MARK: - Custom Types
        
        enum IAPManagerError: Error {
            case noProductIDsFound
            case noProductsFound
            case paymentWasCancelled
            case productRequestFailed
        }
        
        // MARK: - Properties
        
        static let shared = IAPManager()
        
        private var onReceiveProductsHandler: ((Result<[SKProduct], IAPManagerError>) -> Void)?
        private var onBuyProductHandler: ((Result<Bool, Error>) -> Void)?
        
        var totalRestoredPurchases = 0
        
        // MARK: - Init
        
        private override init() {
            super.init()
        }
        
        // MARK: - General Methods
        
        fileprivate func getProductIDs() -> [String]? {
            guard let url = Bundle.main.url(forResource: "IAPProductIDs", withExtension: "plist") else { return nil }
            do {
                let data = try Data(contentsOf: url)
                let productIDs = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) as? [String] ?? []
                return productIDs
            } catch {
                print(error.localizedDescription)
                return nil
            }
        }
        
        func getPriceFormatted(for product: SKProduct) -> String? {
            let formatter = NumberFormatter()
            formatter.numberStyle = .currency
            formatter.locale = product.priceLocale
            return formatter.string(from: product.price)
        }
        
        func startObserving() {
            SKPaymentQueue.default().add(self)
        }
        
        func stopObserving() {
            SKPaymentQueue.default().remove(self)
        }
        
        func canMakePayments() -> Bool {
            return SKPaymentQueue.canMakePayments()
        }
        
        // MARK: - Get IAP Products
        
        func getProducts(withHandler productsReceiveHandler: @escaping (_ result: Result<[SKProduct], IAPManagerError>) -> Void) {
            onReceiveProductsHandler = productsReceiveHandler
            
            guard let productIDs = getProductIDs() else {
                productsReceiveHandler(.failure(.noProductIDsFound))
                return
            }
            
            let request = SKProductsRequest(productIdentifiers: Set(productIDs))
            request.delegate = self
            request.start()
        }
    
        // MARK: - Purchase Products
        
        func buy(product: SKProduct, withHandler handler: @escaping ((_ result: Result<Bool, Error>) -> Void)) {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
            
            onBuyProductHandler = handler
        }
        
        func restorePurchases(withHandler handler: @escaping ((_ result: Result<Bool, Error>) -> Void)) {
            onBuyProductHandler = handler
            totalRestoredPurchases = 0
            SKPaymentQueue.default().restoreCompletedTransactions()
        }
    }
    
    // MARK: - SKPaymentTransactionObserver
    
    extension IAPManager: SKPaymentTransactionObserver {
        func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
            transactions.forEach { (transaction) in
                switch transaction.transactionState {
                case .purchased:
                    onBuyProductHandler?(.success(true))
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .restored:
                    totalRestoredPurchases += 1
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .failed:
                    if let error = transaction.error as? SKError {
                        if error.code != .paymentCancelled {
                            onBuyProductHandler?(.failure(error))
                        } else {
                            onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
                        }
                        print("IAP Error:", error.localizedDescription)
                    }
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .deferred, .purchasing: break
                @unknown default: break
                }
            }
        }
        
        func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
            if totalRestoredPurchases != 0 {
                onBuyProductHandler?(.success(true))
            } else {
                print("IAP: No purchases to restore!")
                onBuyProductHandler?(.success(false))
            }
        }
        
        func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
            if let error = error as? SKError {
                if error.code == .paymentCancelled {
                    onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
                } else {
                    print("IAP Restore Error:", error.localizedDescription)
                    onBuyProductHandler?(.failure(error))
                }
            }
        }
    }
    
    // MARK: - SKProductsRequestDelegate
    
    extension IAPManager: SKProductsRequestDelegate {
        func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
            let products = response.products
            
            if !products.isEmpty {
                onReceiveProductsHandler?(.success(products))
            } else {
                onReceiveProductsHandler?(.failure(.noProductsFound))
            }
        }
        
        func request(_ request: SKRequest, didFailWithError error: Error) {
            onReceiveProductsHandler?(.failure(.productRequestFailed))
        }
        
        func requestDidFinish(_ request: SKRequest) {
            // Handle finish
        }
    }
    
    // MARK: - IAPManagerError Localized Error Descriptions
    
    extension IAPManager.IAPManagerError: LocalizedError {
        var errorDescription: String? {
            switch self {
            case .noProductIDsFound:
                return "No In-App Purchase product identifiers were found."
            case .noProductsFound:
                return "No In-App Purchases were found."
            case .productRequestFailed:
                return "Unable to fetch available In-App Purchase products at the moment."
            case .paymentWasCancelled:
                return "In-App Purchase process was cancelled."
            }
        }
    }
    

    Ở class trên mình đã viết đầy đủ các methods như: buy(), restorePurchases()… Cũng như xử lý các error.

    Bây giờ chúng ta sử dụng IAPManager để thực hiện purchase nhé.

    Đầu tiên mình thực hiện layout một để một button có tên là buy ở giữa màn hình:

    Tiếp theo mình thực hiện implement việc purchase trong class ViewController

    import UIKit
    import StoreKit
    
    class ViewController: UIViewController {
        
        private var products: [SKProduct] = []
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        override func viewWillAppear(_ animated: Bool) {
            IAPManager.shared.getProducts { (result) in
                DispatchQueue.main.async {
                    switch result {
                    case .success(let products):
                        self.products = products
                    case .failure(let error):
                        self.showAlert(message: error.localizedDescription)
                    }
                }
            }
        }
        
        // MARK: - Actions
    
        @IBAction func didBuy(_ sender: UIButton) {
            DispatchQueue.main.async {
                self.verifyBeforeBuy()
            }
        }
        
        // MARK: - Private Methods
        
        private func verifyBeforeBuy() {
            guard let product = product(with: "Demo.TechoverInAppPurchase.co.Apple.Book"),
                  let price = IAPManager.shared.getPriceFormatted(for: product) else {
                return
            }
            
            let alertController = UIAlertController(title: product.localizedTitle,
                                                    message: product.localizedDescription,
                                                    preferredStyle: .alert)
            
            alertController.addAction(UIAlertAction(title: "Buy now for \(price)", style: .default, handler: { (_) in
                self.buy(product: product)
            }))
            
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
            self.present(alertController, animated: true, completion: nil)
        }
        
        private func product(with id: String) -> SKProduct? {
            return products.first(where: { $0.productIdentifier == id })
        }
            
        private func buy(product: SKProduct) {
            if !IAPManager.shared.canMakePayments() {
                self.showAlert(message: "In-App Purchases are not allowed in this device.")
            } else {
                IAPManager.shared.buy(product: product) { result in
                    switch result {
                    case .success(let success):
                        if success {
                            self.showAlert(message: "Buy success")
                        } else {
                            self.showAlert(message: "Buy error")
                        }
                    case .failure(let failure):
                        self.showAlert(message: failure.localizedDescription)
                    }
                }
            }
        }
    }
    
    extension ViewController {
        func showAlert(title:String = "",
                       message: String) {
            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            self.present(alert, animated: true, completion: nil)
        }
    }
    

    Mình sẽ giải thích flow của việc purchase như sau:

    • Ở viewWillAppear() mình lấy tất cả producIDs đã set trong IAPProductIDs.plist và lưu lại với biến products: [SKProduct]
    • Khi người dùng bấm vào buy button mình thực hiện verify trước khi gọi đến IAPManager.shared.buy(product: product)
    • verifyBeforeBuy(): mình lấy product có id là Demo.TechoverInAppPurchase.co.Apple.Book, từ product đó mình lấy ra price của product đó bằng method getPriceFormatted trong IAPManager và thông báo cho người dùng số tiền cần bỏ ra để mua item này
    • Khi người dùng bấm confirm mua trên alert mình thực hiện kiểm tra xem device có cho phép thực hiện giao dịch hay không bằng method IAPManager.shared.canMakePayments, nếu có thì gọi đến IAPManager.shared.buy và nếu không cho phép thì hiển thị thông báo cho người dùng
    • IAPManager.shared.buy(): sau khi thực hiện method này hệ thống sẽ show một actionsheet để người dùng xác nhận trước khi thực hiện giao dịch
    • Sau khi thực hiện giao dịch nếu thành công sẽ nhận được một thông báo và thất bại thì mình cũng sẽ hiển thị thông báo cho người dùng

    Test In-App Purchase sau khi implement

    Trên thực tế khi thực hiện mỗi giao dịch đều mất tiền, vậy mỗi khi test in-app purchase thì đều mất tiền sao? Câu trả lời là không, vì apple đã support viêc testing thông qua apple sandbox account, điều này giúp developer cũng như tester thực hiện giao dịch mà không mất tiền.

    Chúng ta vào đây để add sandbox account. Mình add account [email protected]

    Tiếp theo chúng ta thực hiện login sandbox account trên device test.

    Setting -> App Store -> Sandbox account -> login

    Sau khi add sandbox account và login, chúng ta thực hiện purchase thì sẽ nhận được thông báo là đang purchase với account sandbox và không mất phí:

    Thông qua phần 1 và bài này mình đã giới thiệu đến các bạn cách để thực hiện in-app purchase trong Swift.
    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

  • Android Room Database Tips

    Android Room Database Tips

    Chào mọi người, chắc hẳn trong chúng ta nếu triển khai database của Android trong thời điểm hiện tại, chúng ta sẽ nghĩ ngay đến việc sử dụng Room. Chính vì vậy, hôm nay mình xin chia sẻ một số Tips nhỏ trong việc sử dụng Room đến mọi người.

    1. Thiết lập ràng buộc giữa các Entities thông qua ForeignKey

    Mặc dù Room không hỗ trợ trực tiếp ràng buộc giữa các mối quan hệ, nhưng nó cho phép bạn xác định các ràng buộc Foreign keys giữa các Entities.

    Thông qua annotation @ForeignKey, một phần trong bộ annotation của @Entity, để cho phép sử dụng các tính năng khóa ngoại của SQLite. No không những giúp thể hiện được tốt hơn mối quan hệ giữa các Entities, đảm bảo đúng thiết kế mà còn thực thi các ràng buộc trên các bảng để đảm bảo mối quan hệ hợp lệ khi bạn sửa đổi cơ sở dữ liệu.

    Cụ thể chúng ta sẽ cùng đến một ví dụ cụ thể:

    image

    Cùng nhìn quan hệ 1-n giữa Person và Dog. Cụ thể với 2 Primary Key tương ứng là PersonIdDogId cùng với PersonId được sử dụng như là một foreign key.

    @Entity(tableName = “dog”,
            foreignKeys = arrayOf(
                ForeignKey(entity = Person::class,
                           parentColumns = arrayOf(“personId),
                           childColumns = arrayOf("owner"))))
    
    data class Dog(@PrimaryKey val dogId: String,
                  val name: String,
                  val owner: String)
    

    Theo tùy chọn, bạn có thể tuỳ chọn hành động sẽ được thực hiện khi đối tượng Parent Entity bị xóa hoặc cập nhật trong cơ sở dữ liệu.

    Bạn có thể chọn một trong các tùy chọn sau: NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT hoặc CASCADE, tương tự sử dụng như trong SQLite.

    2. Tạo mối quan hệ Relation trong Room Database

    Vẫn là mối quan hệ 1-n ở ví dụ trước. Bây giờ mình muốn truy vấn thực hiện việc lấy dữ liệu của các Person và toàn bộ Dogs tương ứng kèm theo.

    image

    Cách thông thường để thực hiện, chúng ta sẽ cần thực hiện 2 truy vấn: một truy vấn để lấy danh sách tất cả Person và một truy vấn khác để lấy danh sách Dog dựa trên Id của Person. Cụ thể:

    @Query(“SELECT * FROM Person”)
    public List<Person> getPersons();
    
    @Query(“SELECT * FROM dog where owner = :personId”)
    public List<Dog> getDogsForPersons(String personId);
    

    Sau đây, mình sẽ triển khai theo cách thực hiện tạo mỗi quan hệ Relation giữa 2 đối tượng Person và Dog thông qua annotation @Relation

    class PersonAndDogs {
       @Embedded
       var person: Person? = null
       @Relation(parentColumn = “personId”,
                 entityColumn = “owner”)
       var dogs: List<Dog> = arrayListOf()
    }
    

    Trong DAO, chúng tôi chỉ thực hiện một truy vấn duy nhất và Rôm sẽ truy vấn cả bảng Person và Dog, tiếp đó xử lý mapping đối tượng.

    @Transaction
    @Query(“SELECT * FROM Person”)
    List<PersonAndDogs> getPersonAnDogs();
    

    3. Thực hiện câu lệnh trong một Transaction

    Khi một câu lệnh trong Room gắn với @Transaction, nó sẽ đảm bảo rằng tất cả các hoạt động cơ sở dữ liệu mà bạn đang thực hiện trong phương thức đó sẽ được chạy bên trong một transaction.

    Transaction sẽ thất bại khi một Exception trong một trong những truy vấn trong Transaction đó xảy ra.

    @Dao
    abstract class UserDao {
        
        @Transaction
        open fun updateData(users: List<User>) {
            deleteAllUsers()
            insertAll(users)
        }
    
        @Insert
        abstract fun insertAll(users: List<User>)
    
        @Query("DELETE FROM Users")
        abstract fun deleteAllUsers()
    }
    

    Bạn cũng có thể sử dụng @Transaction cho các phương thức @Query có câu lệnh chọn, trong các trường hợp sau:

    • Khi kết quả của truy vấn khá lớn. Bằng cách truy vấn cơ sở dữ liệu trong một giao dịch, bạn đảm bảo rằng nếu kết quả truy vấn không vừa với single cursor window, thì nó sẽ không bị hỏng do những thay đổi trong cơ sở dữ liệu giữa các lần cursor window swaps.

    • Khi kết quả của truy vấn là POJO với các trường @Relation. Các trường là các truy vấn riêng biệt nên việc chạy chúng trong một Transation sẽ đảm bảo kết quả nhất quán giữa các truy vấn.

    4. Tối ưu hoá đối tượng truy vấn

    Khi truy vấn cơ sở dữ liệu, bạn nên cân nhắc rằng có sử dụng tất cả các fields bạn trả về trong truy vấn của mình không?

    Quan tâm đến dung lượng bộ nhớ mà ứng dụng của bạn sử dụng và chỉ tải tập hợp con các trường mà bạn sẽ sử dụng. Điều này cũng sẽ cải thiện tốc độ truy vấn của bạn bằng cách giảm IO cost.

    Room sẽ thực hiện ánh xạ giữa các Columns và Objects cho bạn.

    Cùng xem một ví dụ sau:

    @Entity(tableName = "users")
    data class User(@PrimaryKey
                    val id: String,
                    val userName: String,
                    val firstName: String, 
                    val lastName: String,
                    val email: String,
                    val dateOfBirth: Date, 
                    val registrationDate: Date)
    

    Trên một số trường hợp cụ thể, các bạn không cần hiển thị tất cả thông tin này. Vì vậy, thay vào đó, chúng ta có thể tạo một đối tượng UserMinimal chỉ chứa dữ liệu cần thiết.

    data class UserMinimal(val userId: String,
                           val firstName: String, 
                           val lastName: String)
    

    Trong lớp DAO, bạn chỉ cần xác định truy vấn như sau.

    @Dao
    interface UserDao {
        @Query(“SELECT userId, firstName, lastName FROM Users)
        fun getUsersMinimal(): List<UserMinimal>
    }
    

    5. Khởi tạo trước dữ liệu cho Room Database

    Có rất nhiều trường hợp cụ thể mà bạn muốn thiết lập một bộ dữ liệu Default vào cơ sở dữ liệu trước. Khi đó bạn nên cân nhắc đến việc chèn dữ liệu ngay sau khi Room được khởi tạo.

    Cụ thể, bạn có thể cân nhắc sử dụng RoomDatabase#Callback! Gọi phương thức addCallback khi xây dựng RoomDatabase của bạn và ghi đè onCreate hoặc onOpen.

    • onCreate will be called when the database is created for the first time, after the tables have been created.
    • onOpen is called when the database was opened.
       companion object {
    
            @Volatile private var INSTANCE: DataDatabase? = null
    
            fun getInstance(context: Context): DataDatabase =
                    INSTANCE ?: synchronized(this) {
                        INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                    }
    
            private fun buildDatabase(context: Context) =
                    Room.databaseBuilder(context.applicationContext,
                            DataDatabase::class.java, "Test.db")
                            // prepopulate the database after onCreate was called
                            .addCallback(object : Callback() {
                                override fun onCreate(db: SupportSQLiteDatabase) {
                                    super.onCreate(db)
                                    // insert the data on the IO Thread
                                    ioThread {
                                        getInstance(context).dataDao().insertData(PRE_POPULATE_DATA)
                                    }
                                }
                            })
                            .build()
    
            val PRE_POPULATE_DATA = listOf(Data("1", "val"), Data("2", "val 2"))
        }
    
    private val IO_EXECUTOR = Executors.newSingleThreadExecutor()
    
    /**
     * Utility method to run blocks on a dedicated background thread, used for io/database work.
     */
    fun ioThread(f : () -> Unit) {
        IO_EXECUTOR.execute(f)
    }
    

    Note: Các bạn chú ý thực hiện việc Insert default data trên một Worker Thread.

    Trên đây là một số Tips nhỏ trong việc sử dụng và triển khai Room Database. Mong là ít nhiều sẽ giúp ích cho mọi người. Cảm ơn mọi người đã theo dõi.

  • Chọn FileOwner hay Custom class khi tạo một Custom UIView

    Chọn FileOwner hay Custom class khi tạo một Custom UIView

    Thông thường chúng ta có 3 cách dùng khi tạo 1 file custom UIView. Chúng ta hãy cùng làm theo cả 3 cách sau đây để xem ưu, nhược điểm của từng cái nhé.

    A. Set File owner

    B. Set Custom class

    C. Set cả hai File owner và Custom class

    Giờ hãy tạo 1 Tabbar Controller chứa 3 view controller để thực hành add custom view theo 3 cách trên nhé.

    Cách A. Thêm custom view dùng File owner

    – Trước hết, ta cần hiểu FIleOwner là gì? File owner là một controller object giúp ta kết nối code với các elements, UI trong file nib. 

    • Bước 1: Tạo file xib và class “CustomViewA”

    Views có thể được tạo bằng hai cách: 

    + Tạo view bằng code, ta gọi hàm init(frame: CGRect)

    + Nếu dùng file nib, khi file nib được load sẽ gọi hàm  init?(coder: NSCoder) 

    Nếu muốn support cả 2 cách, ta implement cả 2 hàm trên

    Ở ví dụ này ta tạo custom view bằng cách dùng file xib chứ không dùng code

    Tạo file xib CustomViewA.xib

    Tạo file CustomViewA.swift. Ở file CustomViewA.swift, ta setup code như sau:

    import UIKit
    
    final class CustomViewA: UIView {
        override init(frame: CGRect) {
            super.init(frame: frame)
            initView()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            initView()
        }
        
        private func initView() {
            // Để load file nib, ta tạo một instance của UINib
            // Ta set bundle là nil để dùng bundle default
            let nib = UINib(nibName: "CustomViewA", bundle: nil)
            
            // Khởi tạo contents trong file nib, objects là 1 array của tất cả top-level objects trong file nibs
            let objects = nib.instantiate(withOwner: self, options: nil)
            
            // Lưu ý: Vì list object trả ra từ file nib chưa thuộc view hierarchy nào nên ta phải addSubview vào CustomViewA
            if let view = objects.first as? UIView {
                view.backgroundColor = .clear
                addSubview(view)
                view.frame = bounds
                view.backgroundColor = .orange
            }
        }
    }
    • Bước 2: Set class cho file owner

    Mở file xib, chọn File’s Owner, rồi set Custom class là “CustomViewA” vừa tạo

    • Bước 3: Mở “ViewControllerA” trong storyboard, kéo một view vào rồi set Custom Class cho nó là “CustomViewA”.

    Chạy app và ta đã tạo được một custom view, kết quả như hình bên dưới.

    Cách B. Thêm custom view dùng Custom Class

    • Bước 1:

    Tạo file CustomViewB giống file CustomViewA như ở cách A

    • Bước 2:

    Thay vì FileOwner như cách A, ta chọn View ở dưới, rồi set Custom Class là “CustomViewB

    • Bước 3: Ta cũng kéo view “CustomViewB” vừa tạo vào ViewControllerB như bước 3 cách A, rồi chạy app

    Oops!!! Cách này dùng thì lại bị crash. Tại sao lại thế???

    Lý do vì khi load ViewControllerB, app sẽ call  “init?(coder: NSCoder)” của custom view (CustomViewB), tiếp theo sẽ call  “initView()“. Trong hàm này ta dùng “instantiate(withOwner)” để load file Nib.

    Tuy nhiên, vì ở bước 2 ta đã set top-level view của file nib là class CustomViewB nên nó sẽ load lại file nib và call hàm “init?(coder: NSCoder)” một lần nữa. Cứ thế nó sẽ tạo 1 vòng loop vô hạn khiến app bị crash.

    Để tránh trường hợp này, ta bỏ hàm “initView()” ra ngoài, không để trong “init?(coder: NSCoder)” or “init(frame: CGRect)” nữa.

    • Bước 4: Move hàm load xib ra bên ngoài View Controller (parent view controller)
    import UIKit
    
    class ViewControllerB: UIViewController {
        
        var viewCustomB: CustomViewB!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            initView()
        }
        
        private func initView() {
            let nib = UINib(nibName: "CustomViewB", bundle: nil)
            let objects = nib.instantiate(withOwner: nil, options: nil)
    
            if let firstView = objects.first as? CustomViewB {
                firstView.backgroundColor = .red
                viewCustomB = firstView
                view.addSubview(viewCustomB)
                viewCustomB.translatesAutoresizingMaskIntoConstraints = false
                viewCustomB.widthAnchor.constraint(equalToConstant: 240).isActive = true
                viewCustomB.heightAnchor.constraint(equalTo: viewCustomB.widthAnchor).isActive = true
                viewCustomB.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
                viewCustomB.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
            }
        }
    }

    Chạy app và ta đã tạo được một custom view, kết quả như hình bên dưới.

    Như vậy ta vẫn có thể add custom view bằng cách B này, nhưng nó có nhược điểm:

    • Ta sẽ phải load file nib (CustomViewB.xib) mọi nơi mà ta muốn dùng nó và sẽ gây ra duplicate code.

    Cách C. Thêm custom view dùng Custom Class và FileOwner

    • Bước 1: Tạo file CustomViewC.swift và CustomViewC.xib giống file CustomViewA như ở cách A
    • Bước 2: Tạo 1 button ở giữa view
    • Bước 3: Lần này, ta set CustomClass của view là “CustomViewC”, còn set FileOwner là parent class của nó (ViewControllerC)
    • Bước 4: Để logic load file xib ở parent class (để tránh crash như cách B) và kéo IBAction của button vào cả parent và custom view class

    Khi tap button, ta thấy debug đều chỉ ra cùng là một object.

    ==> Ok vậy ta đã thử qua 3 cách để tạo một custom view, ta có thể thấy rằng cách A đầu tiên dùng FileOwner là tiện lợi nhất

    • Dễ reuse trong toàn app thông qua code, xib hoặc storyboards
    • Không dính crash, tránh duplicate code như cách B

    Sau đây là một số tip mà các bạn chắc cũng đã biết =)))

    Tip1: Ở trong file xib, bạn có thể chọn Size “Freeform” trong Attribute Inspector để resize view

    Tip2: Trong cách A, ta có thể tạo một outlet contentView là top-level view của file xib, từ đó không cần phải get first object khi instantiate xib file nữa.

    Bài viết có tham khảo nguồn từ 2 link dưới đây, ở đó họ sẽ giải thích chi tiết view là gì, được khởi tạo như thế nào, file nib là gì, cách dùng customview bằng code hoặc file nib…

    https://github.com/codepath/ios_guides/wiki/Custom-Views#how-views-are-defined-and-instantiated

    https://medium.com/@bhupendra.trivedi14/understanding-custom-uiview-in-depth-setting-file-owner-vs-custom-class-e2cab4bb9df8

    Code: https://github.com/sonvuhwg/AddCustomView

    Have a nice day!

  • iOS/Swift In-App Purchase (P1)

    iOS/Swift In-App Purchase (P1)

    Trên AppStore chắc hẳn các bạn đã từng gặp các ứng dụng phải trả tiền để mua, các ứng dụng gắn quảng cáo hay những ứng dụng sử dụng miễn phí các chức năng cơ bản và sau đó phải bỏ tiền ra để nâng cấp VIP, hay nâng cấp các chức năng cao cấp hơn. Không phải ứng dụng nào cũng có thể sử dụng dịch vụ bên thứ 3 để thanh toán. Còn về phía người dùng chắc chắn họ cũng không muốn bị lừa đảo, chắc chắn ai cũng muốn sử dụng một phương thức thanh toán nhanh chóng, tin cậy và bảo mật.

    Khi thanh toán bất kì một dịch vụ nào chắc chắn bên cung cấp dịch vụ thanh toán sẽ cắt một phần nào đó trên mỗi giao dịch. Không bỏ lỡ cơ hội kiếm tiền dễ dàng đó, Apple cũng cung cấp cho các developer một API thanh toán có tên là In-App Purchase. Với In-App Purchase thì người dùng có thể thanh toán trực tiếp trên tài khoản Apple ID, quá trình thanh toán nhanh chóng, an toàn và đáng tin cậy.

    Trong bài này mình sẽ giới thiệu In-App Purchase đến các bạn và implement nó.

    Các kiểu Thanh toán

    • Consumable : Người dùng có thể thực hiện một giao dịch gì đó trong ứng dụng. Đặc điểm của kiểu thanh toán này là người dùng có thể mua 1 hay nhiều lần không giới hạn về thời gian.
    • Non-Consumable : Đây là kiểu thanh toán mà khi người dùng có thể mua và sử dụng mãi mãi.
    • Auto-Renewable Subscriptions : Đây là một kiểu thanh toán tự động gia hạn sau một khoảng thời gian nhất định. Ví dụ người dùng trả tiền để mua gói học tiếng anh VIP trong 1 tháng và được tự động gia hạn theo định kỳ cho đến khi người dùng huỷ.
    • Non-Renewing Subscriptions : Kiểu thanh toán này giống với Auto-Renewable Subscriptions ở điểm nó bán một loại dịch vụ trả phí giới hạn thời gian, điểm khác biệt duy nhất là nó sẽ không tự động gia hạn khi hết hạn.

    Đến đây chắc hẳn sẽ có bạn thắc mắc là người bán được bao nhiêu, và Apple được bao nhiêu trên mỗi lượt giao dịch?

    Thông thường tỷ lệ ăn chia là 70|30 tức là người bán sẽ được 70% và Apple sẽ được 30%. Kiểu thanh toán mà Apple khuyến khích dùng nhất là Auto-Renewable Subscriptions, riêng kiểu thanh toán này Apple chỉ lấy 15% trên mỗi giao dịch, tức là người bán sẽ nhận được 85%, theo như Apple giải thích là họ muốn giúp người dùng không phải đăng ký lại mỗi lần hết hạn, tuy nhiên có thể mục đích chính ở đây là người dùng sẽ sử dụng dịch vụ lâu dài hơn.

    Về cơ bản lý thuyết chỉ có vậy thôi, giờ chúng ta đi vào phần setup nhé!

    Setup môi trường

    Khi người dùng thực hiện một giao dịch thì tiền đó sẽ về đâu, tính thuế như thế nào?

    Chắc chắn tiền sẽ phải về với người bán dịch vụ trong ứng dụng rồi đúng không? Trước khi public ứng dụng có sử dụng In-App Purchase thì chúng ta cần setup Agreements, tax, và banking.

    Để setup Agreements, tax, và banking các bạn vào itunes-connect và vào Agreements, tax, and banking và setup Paid Application.

    Sau khi bạn vào Agreements, tax, and banking thì giao diện chi tiết sẽ như thế này:

    Mặc định thì Free Apps sẽ được active và các bạn setup với Paid Apps nhé. Vì mình đã setup trước đó rồi nên trạng thái đã được active, mình nhớ là phải mất 24h để Apple verify thông tin mà các bạn khai báo.

    Các bạn có thể sử dụng thẻ ATM nội địa để setup cho mục banking nhé

    Setup Payments

    Tiếp theo chúng ta đến phần setup payments.

    Trong itunes-connect truy cập vào my Apps

    Trường hợp các bạn chưa có app thì tạo app mới.
    Ở đây mình tạo 1 app mới. Để tạo app mới ở itunes-connect thì các bạn cần chọn một Identifier từ list Identifiers vì thế hay chuẩn bị một bundleID và tạo Identifier với BundleID đó. Đây cũng chính là bundleID sử dụng cho ứng dụng của chúng ta luôn.


    Tiếp theo chúng ta vào features -> In-app Purchases


    Tiếp theo chúng ta tạo một In-App Purchase. Ở Demo lần này mình sẽ tạo phương thức thanh toán là Consumable

    • Type: Kiểu thanh toán.
    • Reference Name: Tên này sẽ giúp bạn gợi nhớ và không hiển thị cho người dùng nên hãy chọn cái tên nào mà khi bạn nhìn vào đó hiểu ngay item này nó làm gì.
    • Product ID: Đây là một thông tin rất quan trọng nó là định danh duy nhất cho 1 loại thanh toán của bạn, bạn sẽ cần nó để matching trong code. Ở đây mình đặt tên với bundleID + tên của item thanh toán.

    Sau khi tạo xong thì status của item thanh toán đó là Missing Metadata. Các bạn hay setup tiếp các thông tin tiếp theo cho status chuyển thành Ready to Submit nhé:

    • Availability: chọn các quốc gia có thể thanh toán.
    • Price Schedule: lựa chọn giá bán, mặc định Apple sẽ quy định ra các giá bán, do đó các bạn không thể điền môt giá trị nào đó mà phải chọn trong đây.
    • Localizations: đây chính là phần sẽ hiển thị cho người dùng, bạn sẽ chọn Localizations tuỳ theo từng ngôn ngữ.
    • Review Information: Bạn sẽ phải cung cấp ảnh chụp design chức năng thanh toán trên app tương ứng với những gì bạn đang setup, bên cạnh đó là mô tả và giải thích cho chức năng này, điều này phục vụ cho việc review của apple.

    Sau khi các bạn điền tất cả các thông tin cần thiết thì trạng thái của item thanh toán này sẽ là Ready to Submit

    Đến đây chúng ta đã hoàn thành việc setup môi trường và setup payments rồi. Phần tiếp theo mình sẽ hướng dẫn tiếp phần implement code và thực hiện test sau khi implement!.

    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

  • Cách Grab sử dụng DynamoDB để xử lý hàng triệu đơn hàng mỗi ngày

    Cách Grab sử dụng DynamoDB để xử lý hàng triệu đơn hàng mỗi ngày

    Nội dung

    Giới thiệu

    Trong thực tế, sau khi một khách hàng đặt một đơn hàng GrabFood từ ứng dụng Grab, đối tác bán hàng sẽ chuẩn bị đơn hàng. Một đối tác tài xế (Grab Bike) sẽ tiếp nhận đơn hàng và vận chuyển cho khách hàng. Làm cách nào mà nền tảng đặt hàng (Order Platform) có thể xử lý hàng triệu đơn hàng như vậy mỗi ngày !?

    Nhìn chung, có thể phân loại các truy vấn mà Order Platform cần xử lý làm 2 loại: transactional queries vs analytical queries. Các truy vấn transactional ví dụ như: Create đơn hàng, Update đơn hàng, Get đơn hàng theo ID, Get các đơn hàng đang được xử lý của một khách hàng,… Những truy vấn này là những truy vấn quan trọng cần xử lý chính xác, hiệu quả. Các truy vấn analytical ví dụ như: Get lịch sử đơn hàng, Get các metrics thống kê về đơn hàng,…

    → Order Platform cần được thiết kế để có thể xử lý một lượng lớn dữ liệu transaction hàng tháng.

    Sau khi phân loại các mẫu truy vấn như trên, nhóm Kỹ sư của Grab đã đặt ra 3 mục tiêu thiết kế hệ thống Database để lưu trữ và xử lý như sau:

    • Stability (Tính ổn định): Giải pháp cơ sở dữ liệu phải đảm bảo phục vụ được khả năng đọc/ghi với thông lượng cao (Queries per Second, hay QPS). Xử lý đơn hàng trực tuyến phải có tính sẵn sàng cao.
    • Scalability and cost (Khả năng mở rộng và chi phí): Giải pháp phải đáp ứng được sự phát triển nhanh chóng về mặt business, ngoài ra giải pháp phải hiệu quả về chi phí trên quy mô lớn.
    • Consistency (Tính nhất quán): strong consistency cho các đối với các transactional querieseventually consistency với các analytical queries.

    Với các mục tiêu đó, Grab team đã quyết định thiết kế giải pháp cơ sở dữ liệu sử dụng các loại Database khác nhau, một để xử lý các transactional queries (OLTP) và một để xử lý các analytical queries (OLAP).

    Hình 1: Tổng quan giải pháp cơ sở dữ liệu cho Order Platform

    Vì các mục đích sử dụng khác nhau nên các cơ sở dữ liệu OLTP và OLAP sẽ lưu trữ data theo các cách khác nhau, với cùng một nguồn data nhưng OLTP sẽ lưu trữ chúng trong thời gian ngắn, ngược lại thì OLAP sẽ lưu trữ data trong thời gian dài hơn nhằm truy xuất các thông tin lịch sử và thống kê với dữ liệu. Trong Hình 1, Grab team sử dụng một Data Ingestion pipeline để có thể lưu trữ giữ liệu nhất quán trong cả 2 cơ sở dữ liệu. Cần lưu ý rằng, pipeline này sẽ hoạt động bằng cách sử dụng các phương pháp xử lý asynchronous (bất đồng bộ) vì chúng ta không cần phải xử lý các truy vấn với OLAP trực tuyến, real-time như OLTP.

    Trong bài viết này, tôi sẽ tập trung vào phân tích cách Grab xử lý hàng triệu đơn hàng mỗi ngày, chính vì vậy mà trong các phần tiếp theo, tôi sẽ không đề cập đến cơ sở dữ liệu OLAP. Các bạn nếu có hứng thú, có thể tìm đọc thêm các tài liệu từ Grab Tech Blog.

    Với các truy vấn OLTP, Grab team chọn DynamoDB là công nghệ lưu trữ và xử lý dữ liệu. Trong phần 2, tôi sẽ phân tích tại sao DynamoDB lại phù hợp. Phần 3, tôi sẽ phân tích Grab sử dụng DynamoDB như thế nào trong thiết kế mô hình dữ liệu hiệu quả.

    Tại sao lại là DynamoDB?

    Để trả lời câu hỏi này, tôi sẽ phân tích tính phù hợp của DynamoDB đối với việc đáp ứng các mục tiêu thiết kế được nêu ở phần trước.

    Điều đầu tiên để nói về DynamoDB, nó là một cơ sở dữ liệu NoSQL, fully-managed được cung cấp bởi AWS, có tính khả dụng cao (high availability), hiệu năng cao (high performance) với hiệu suất chỉ 1 phần nghìn giây (single-digit millisecond performance), khả năng mở rộng quy mô vô hạn mà không giảm hiệu suất (infinite scaling with no performance degradation) → Stability.

    High availability

    DynamoDB tự động phân phối dữ liệu và lưu lượng truy cập qua một số lượng máy chủ vừa đủ để xử lý các yêu cầu lưu trữ và thông lượng trong khi vẫn đảm bảo hiệu suất nhanh chóng và nhất quán. Tất cả dữ liệu được lưu trữ sử dụng ổ đĩa SSD và tự động được replicated qua các AZs khác nhau → high availability & high durability. Hơn nữa, chúng ta có thể sử dụng Global Tables để đồng bộ dữ liệu qua các Regions.

    Data models

    Trước khi nói về hiệu suất, tôi đề cập một chút về các mô hình dữ liệu mà DynamoDB hỗ trợ. Trong DynamoDB, có 2 mô hình dữ liệu đó là key-value store (sử dụng hash table) → cho phép hiệu suất truy vấn O(1) và wide-column store (sử dụng B-tree) → cho phép hiệu suất truy vấn O(log(n)), n là kích thước của 1 collections các phần tử có chung partition key.

    DynamoDB cho phép truy cập dữ liệu theo nhiều cách (access patterns) khác nhau với hiệu suất cao → điều quan trọng với DynamoDB đó là chúng ta phải thiết kế mô hình dữ liệu hiệu quả, tôi sẽ tiếp tục đề cập đến điều này trong phần 3 – Grab đã sử dụng DynamoDB thế nào?

    Infinite scaling with no performance degradation

    DynamoDB có khả năng scale vô hạn là nhờ cơ chế sharding dữ liệu qua nhiều server instances. Partitions là đơn vị lưu trữ cốt lõi của các DynamoDB tables. Khi một HTTP request (ứng dụng giao tiếp với DynamoDB qua HTTP connection thay vì TCP connection như các cơ sở dữ liệu truyền thống khác) tới DynamoDB, request router sẽ lấy partition key trong request đó và áp dụng một hàm băm (hash function) đối với partition key này. Kết quả của hàm băm sẽ chỉ định server nơi mà dữ liệu được lưu trữ, request sau đó sẽ được chuyển tiếp đến server nơi lưu trữ dữ liệu để thực hiện read/write với dữ liệu. Thiết kế này giúp cho DynamoDB có thể bổ sung thêm các storage nodes vô hạn khi dữ liệu scale up.

    Trong các phiên bản sớm hơn của DynamoDB, thông lượng được chia sẻ đều giữa các partitions → có thể dẫn đến mất cân bằng truy cập dữ liệu (có partition thì cần truy cập nhiều → không đủ tài nguyên, có partition thì nhu cầu truy cập ít hơn → thừa tài nguyên). Tuy nhiên, DynamoDB team hiện nay đã cho phép adaptive capacity, có nghĩa là thông lượng sẽ tự động được đáp ứng đủ cho các items cần truy cập nhiều, DynamoDB tự động cung cấp capacity cao hơn cho các partitions có nhiều lưu lượng truy cập hơn → DynamoDB có thể đáp ứng thông lượng lên đến 1000 WCUs (write capacity units) và 3000 RCUs (read capacity units). Hay nói một cách khác, DynamoDB có thể đáp ứng đến tối đa 3000 read Queries per second1000 write Queries per Second → hiệu suất cao.

    Ngoài ra, AWS còn cung cấp một tính năng nữa cho DynamoDB, gọi là DynamoDB Accelerator (DAX) – một fully-managed in-memory cache cho bảng DynamoDB. Mặc dù hiệu suất của DAX có thể không thực sự mạnh như Redis nhưng DAX có thể là một giải pháp caching phù hợp cho phép tốc độ đủ nhanh, ít bảo trì và tiết kiệm chi phí hơn so với các bộ đệm khác.

    Tóm lại, với DynamoDB, chúng ta vẫn có thể đảm bảo single-digit millisecond performance khi dữ liệu scale up. Chúng ta có thể mở rộng dữ liệu lên đến 10TB mà vẫn không suy giảm hiệu suất như sử dụng với 10GB. Điều mà không thể được đáp ứng trong các cơ sở dữ liệu quan hệ – ở đó, hiệu suất giảm dần khi dữ liệu scale up.

    Consistency

    Khi nói về các databases và các hệ thống phân tán, một trong những thuộc tính cần xem xết đó là các cơ chế consistency mà nó chúng hỗ trợ.

    Với DynamoDB, có 2 tùy chọn consistency mà cơ sở dữ liệu này hỗ trợ:

    • Strong consistency
    • Eventual consistency

    Với strong consistency, bất kỳ item nào chúng ta đọc từ DynamoDB sẽ phản ánh tất cả các yêu cầu writes đã xảy ra trước đó, trước khi yêu cầu đọc được thực thi. Ngược lại, với eventual consistency thì các yêu cầu đọc items có thể không phản ánh tất cả các yêu cầu writes xảy ra trước đó.

    → Các transactional queries trong Order Platform hoàn toàn có thể được sử dụng với strong consistency trong DynamoDB.

    Cost effective

    DynamoDB cung cấp một mô hình định giá linh hoạt. Với hầu hết các cơ sở dữ liệu, chi phí dựa trên 1 lượng capacities nhất định cho máy chủ bao gồm CPU, RAM, Hard Disks,… → điều này không tối ưu vì chúng ta sẽ phải trả tiền cho tài nguyên thay vì trả cho workload ta sử dụng (bao nhiêu truy vấn mỗi giây/queries per second)

    Ngược lại, DynamoDB được định giá trực tiếp dựa trên workload mà ứng dụng cần, chúng ta chỉ định thông lượng dựa trên WCU (write capacity units) và RCU (read capacity units). Một RCU cung cấp 1 strongly-consistent read per second hoặc 2 eventually-consistent reads per second, lên đến 4KB kích thước. Một WCU cho phép 1 write per second, lên đến 1KB kích thước → Chi phí cho Read và Write là riêng biệt → Có thể tối ưu 1 trong 2 độc lập dựa trên nhu cầu ứng dụng.

    Điều thú vị với DynamoDB là chúng ta có thể điều chỉnh thông lượng WCU và RCU nếu cần. Ví dụ như vào ban đêm hoặc cuối tuần, chúng ta có thể giảm thông lượng để tiết kiệm chi phí.

    Nếu chúng ta không estimate được thông lượng của ứng dụng → DynamoDB cung cấp tùy chọn On-Demand capacity mode (chi phí cao hơn Provisioned capacity mode) → Có thể sử dụng On-Demand capacity mode trong quá trình phát triển, thực thi load testing để đưa ra Provisioned capacity phù hợp → tiết kiệm chi phí.

    Năm 2017, AWS công bố một tính năng gọi là Time-to-live (TTL) cho DynamoDB, cho phép DynamoDB tự động xóa các item khi chúng ta chỉ muốn lưu trữ chúng cho 1 thời gian ngắn → điều này phù hợp với nhu cầu sử dụng DynamoDB của Grab, khi mà Grab team đã thiết kế một Data Ingestion Pipeline để lưu trữ dữ liệu lâu dài trong cơ sở dữ liệu OLAP. Việc lưu trữ dữ liệu trong DynamoDB trong thời gian ngắn còn cho phép hiệu quả chi phí cũng như đảm bảo hiệu suất luôn ổn định cao → đáp ứng được các transactional queries hàng ngày.

    Sử dụng DynamoDB như thế nào?

    Single-table design

    Khi mô hình hóa dữ liệu với DynamoDB, chúng ta nên sử dụng càng ít bảng càng tốt, lý tưởng nhất là chỉ sử dụng 1 bảng cho toàn bộ ứng dụng → single-table design. Tại sao lại như vậy !?

    Với các cơ sở dữ liệu quan hệ, thông thường chúng ta sẽ tạo 1 bảng ứng với mỗi entity (thực thể) trong ứng dụng. Các bảng có thể liên kết với nhau thông qua foreign keys → qua đó dữ liệu có thể được truy vấn qua các bảng sử dụng các phép toán joins. Các phép joins này khá tốn kém, khi mà nó phải scan nhiều bảng, so sánh các giá trị,… và trả về các kết quả.

    DynamoDB được xây dựng cho các use cases với hiệu suất truy vấn cao, quy mô dữ liệu lớn, như Order Platform của Grab → Các phép toán joinskhông phù hợp với các use cases như vậy. Thay vì cải thiện các phép toán joins trên quy mô dữ liệu lớn, DynamoDB giải quyết vấn đề bằng cách loại bỏ hoàn toàn việc sử dụng joins trong truy vấn. Loại bỏ bằng cách nào !?

    Cơ chế pre-join trong DynamoDB có thể cho phép liên kết dữ liệu tương tự như các cơ sở dữ liệu quan hệ. Cơ chế này sử dụng các item collections. Một item collection là tập các items trong bảng chia sẻ chung partition key. Như vậy, nhờ pre-join mà DynamoDB cho phép thiết kế cơ sở dữ liệu ứng dụng chỉ với 1 bảng. Tất nhiên, có những hạn chế nhất định với single-table design, tuy nhiên trong phạm vi bài viết này tôi sẽ không đi sâu vào phân tích những hạn chế đó. Trong các phần sau, tôi muốn làm nổi bật thiết kế của Grab sử dụng single-table design như thế nào để xử lý các transactional queries với các đơn hàng.

    Data modeling

    Trong DynamoDB, khi thiết kế mô hình dữ liệu, chúng ta phải xác định Primary keys của bảng. DynamoDB cung cấp 2 loại Primary keys khác nhau:

    • Simple primary keys: chỉ bao gồm một thuộc tính, gọi là partition key.
    • Composite primary keys: là sự kết hợp của partition key và một thuộc tính khác, gọi là sort key. Đôi khi, chúng ta có thể gọi partition key là hash key, sort key là range key.

    Để thiết kế cơ sở dữ liệu hiệu quả, trước hết chúng ta cần hình dung về mô hình dữ liệu của Order Platform. Order Platform cần thực hiện các truy vấn với đơn hàng (order), các đơn hàng liên quan đến khách hàng hay người đặt hàng, mỗi đơn hàng sẽ có 1 trong 3 trạng thái: ongoing, completed, hoặc canceled → Khá đơn giản và phù hợp với single-table design trong DynamoDB. Hình dưới đây cho thấy thiết kế này của Grab.

    Hình 2: Minh họa bảng DynamoDB lưu trữ các đơn hàng của Order Platform

    Trong hình 2 ở trên, Grab team đã đưa ra một minh họa về bảng DynamoDB nơi mà lưu trữ các đơn hàng, với Partition keymã đơn hàng (order_id) (Lưu ý rằng hình trên chỉ mang tính chất minh họa, không phải là toàn bộ thiết kế của Order Platform).

    Thiết kế này cho phép các transactional queries với đơn hàng như Get, Create, Update được thực hiện với hiệu suất cao, strong consistency. Bởi cấu trúc dữ liệu sử dụng hash table → các truy vấn sử dụng order_id sẽ có độ phức tạp thời gian là O(1).

    Với các truy vấn liên quan đến 1 đơn hàng cụ thể, chúng ta có thể sử dụng order_id (partition key). Vậy với các truy vấn liên quan đến 1 khách hàng, ví dụ như lấy danh sách các đơn hàng ở trạng thái ongoing của khách hàng Alice như trong Hình 2, chúng ta có thể tiếp tục sử dụng mô hình dữ liệu trên không !?

    → Câu trả lời là có. Đến đây, chúng ta phải sử dụng một tính năng hữu ích khác được cung cấp bởi DynamoDB, đó là Global secondary index.

    Secondary indexes

    Primary keys có thể giới hạn các access patterns, như trong trường hợp của Grab, khi sử dụng primary keys với partition keyorder_id thì chúng ta không thể tiếp tục truy vấn với 1 khách hàng cụ thể trong bảng đơn hàng. Để giải quyết hạn chế này, DynamoDB đưa ra một khái niệm gọi là secondary indexes. Secondary indexes cho phép chúng ta re-shape lại dữ liệu sang 1 cấu trúc khác để phục vụ các truy vấn với access pattern khác so với thiết kế ban đầu.

    Khi tạo secondary index, chúng ta cần chỉ định key schema của index, key schema này tương tự như Primary keys của bảng, có thể chỉ cần partition key hoặc kết hợp partition key và sort key. Có 2 loại secondary indexes trong DynamoDB:

    • Local secondary indexes (LSI)
    • Global secondary indexes (GSI)

    Một LSI sử dụng chung partition key như primary key của bảng nhưng với một sort key khác đi → điều này cho phép các truy vấn theo range khác với access pattern ban đầu.

    Ngược lại, với GSI, chúng ta có thể chọn bất cứ thuộc tính nào làm partition key và sort key.

    → Trong trường hợp của Grab, với thiết kế ban đầu như Hình 2, bởi LSI không thay đổi partition key → không phù hợp với các truy vấn theo 1 khách hàng cụ thể → Grab team đã sử dụng GSI để giải quyết vấn đề này.

    Ví dụ với một truy vấn Get tất cả các đơn hàng trong trạng thái Ongoing của khách hàng có pax_idAlice. Nếu chúng ta sử dụng GSI chỉ với partition keypax_id thì việc truy vấn dựa trên pax_id (trường hợp này là Alice) sẽ trả về tất cả các đơn hàng của Alice → Filter trên các kết quả đó để trích xuất ra các đơn hàng của Alice có trạng thái Ongoing. Như vậy, nếu như Alice có nhiều đơn hàng thì việc Filter như vậy sẽ ảnh hưởng đến độ trễ truy vấn nói riêng và hiệu năng tổng thể của hệ thống nói chung. Giải quyết bằng cách nào !?

    → Sử dụng Sparse indexes

    Hình 3: GSI cho bảng đơn hàng

    GSI được Grab team thiết kế sử dụng ID của khách hàng (pax_id_gsi) làm partition key và thời gian tạo đơn hàng (created_at) làm sort key, sparse index cho phép tại bất kỳ thời điểm nào, bảng GSI chỉ lưu trữ các đơn hàng với trạng thái Ongoing (Khi một đơn hàng chuyển từ Ongoing → Completed thì nó sẽ tự động bị xóa khỏi GSI) → Cho phép độ trễ truy vấn thấp, hiệu suất tốt hơn và hiệu quả chi phí.

    Một vài lưu ý khi sử dụng GSI với DynamoDB:

    • Với GSI, chúng ta cần provision thêm throughput. RCU và WCU được sử dụng riêng biệt so với bảng ban đầu.
    • Dữ liệu được replicated từ bảng ban đầu sang GSI theo cơ chế asynchronous → chỉ cho phép eventual consistency.

    Time to live (TTL)

    Vì mục đích chỉ lưu trữ giữ liệu trong DynamoDB trong một thời gian ngắn (chúng ta có thể thấy điều này trên ứng dụng Grab Food, các đơn hàng sau một thời gian ~ 3 tháng sẽ không còn thấy trong lịch sử nữa :v) → Grab team sử dụng tính năng TTL được cung cấp bởi DynamoDB → cho phép tự động xóa các items khi expired. Để sử dụng TTL hiệu quả, Grab team chỉ thêm TTL với các items mới được add vào bảng, xóa các items không có thuộc tính TTL theo cách thủ công và chạy các script để xóa các items có TTL đã out of date khá lâu → cho phép tính năng TTL trên bảng với kích thước đủ nhỏ → hiệu quả khi DynamoDB tự động thực hiện scan bảng và xóa các items đã expired.

    Data ingestion pipeline

    Hình 4: Data ingestion pipeline

    Grab team sử dụng Kafka để đồng bộ dữ liệu giữa 2 cơ sở dữ liệu OLTP và OLAP. Sử dụng Kafka như thế nào, các bạn có thể đọc thêm từ Grab tech blog như trong tài liệu tham khảo [1]. Trong phạm vi bài viết này, tôi không phân tích về lựa chọn này. Tuy nhiên, nếu trong tương lai có thể, tôi muốn phân tích về tính khả thi của giải pháp sử dụng DynamoDB Streams để xử lý và đồng bộ dữ liệu giữa các cơ sở dữ liệu OLTP và OLAP.

    Kết luận

    Trong bài viết này, tôi đã dựa trên một case study là Order Platform của Grab Food để phân tích một số tính năng, nguyên lý của DynamoDB phù hợp với các ứng dụng cần OLTP. DynamoDB đã cho phép Order Platform đạt được tính ổn định, tính khả dụng cao, hiệu suất cao cùng với khả năng mở rộng quy mô vô hạn. Ngoài ra, tôi cũng giới thiệu các cơ chế để cải thiện hiệu suất, chi phí hiệu quả mà Grab team sử dụng như GSI, TTL. Data ingestion pipeline được giới thiệu như là 1 biện pháp mà Grab sử dụng để đồng bộ dữ liệu sang cơ sở dữ liệu OLAP phục vụ các truy vấn thống kê cũng như đảm bảo khả năng mở rộng nghiệp vụ ứng dụng về sau.

    Tài liệu tham khảo

    [1] Xi Chen, Siliang Cao, “How we store and process millions of orders daily”, https://engineering.grab.com/how-we-store-millions-orders, Accessed: 2022-03-31

    [2] Alex DeBrie, “The DynamoDB Book”

    [3] Amazon DynamoDB, https://disaster-recovery.workshop.aws/en/services/databases/dynamodb.html, Accessed: 2022-03-31

  • Bài toán tìm kiếm tương tự

    Bài toán tìm kiếm tương tự

    Danh mục nội dung

    Tìm kiếm KNN

    Thuật toán K-hàng xóm gần nhất, còn được gọi là KNN hoặc k-NN, là một thuật toán học máy có giám sát, không có tham số, được dùng trong phân loại dữ liệu. Thuật toán này sử dụng khoảng cách để phân loại hoặc dự đoán về một nhóm điểm dữ liệu, dựa trên giả định rằng các điểm dữ liệu tương tự sẽ gần nhau về khoảng cách.

    Ngày nay, tìm kiếm hàng xóm gần nhất đã trở thành một chủ đề nghiên cứu nóng bởi tính ứng dụng thực tiễn của nó, nhằm giúp người dùng có thể dễ dàng tìm thấy thông tin họ đang tìm kiếm trong thời gian hợp lý. Thuật toán k-NN là một trong những kỹ thuật giúp tìm chính xác các hàng xóm gần nhất, bởi nó so sánh khoảng cách của mỗi điểm dữ liệu với mọi điểm dữ liệu khác, vì vậy nó yêu cầu thời gian truy vấn tuyến tính (kích thước tập dữ liệu). Nhưng thật không may, hầu hết các ứng dụng hiện đại ngày nay đều có tập dữ liệu khổng lồ (hàng triệu) với chiều cao (hàng trăm hoặc hàng nghìn), vì vậy mà tìm kiếm tuyến tính sẽ tốn kém thời gian. Hãy thử tưởng tượng một thị trường C2C trong thế giới thực với hàng triệu sản phẩm có trong cơ sở dữ liệu và có thể có hàng nghìn sản phẩm mới được tải lên mỗi ngày. So sánh từng sản phẩm với tất cả hàng triệu sản phẩm là lãng phí và mất quá nhiều thời gian, có thể nói giải pháp này là không thể mở rộng. Ngoài ra, các ứng dụng hiện đại còn có nhiều ràng buộc bổ sung khác như mức tiêu thụ bộ nhớ hợp lý và/hoặc độ trễ thấp.

    Điều quan trọng cần lưu ý là mặc dù đã có rất nhiều tiến bộ gần đây về chủ đề này, nhưng k-NN vẫn là phương pháp khả dụng duy nhất để đảm bảo truy xuất chính xác hàng xóm gần nhất.

    Tìm kiếm ANN

    Để giải quyết những vấn đề của k-NN, một lớp các thuật toán mới ra đời có tên là ANN (Approximate Nearest Neighbors). Các thuật toán ANN đánh đổi độ chính xác để mang lại hiệu quả tìm kiếm nhanh chóng, trong thời gian chấp nhận được. Trong một thị trường C2C thực tế, nơi mà số lượng hàng xóm thực tế cao hơn K hàng xóm gần nhất cần tìm kiếm rất nhiều, ANN có thể cho phép đạt được độ chính xác đáng kể khi so sánh với KNN, trong một thời gian ngắn.

    Trong các ứng dụng hiện đại, sai số nhỏ về độ chính xác đổi lại với độ trễ thấp mang lại nhiều lợi ích cho người dùng. Hai ví dụ dưới đây cho thấy điều đó:

    • Tìm kiếm trực quan – Là một người dùng, nếu tôi đang muốn tìm kiếm một bức ảnh về chiếc giày yêu thích, tôi sẽ không bận tâm đến thứ tự xuất hiện của các kết quả trả về, tôi có thể thỏa mãn nhu cầu tìm kiếm của mình nếu như một số ít kết quả mong muốn được hiển thị gần nhất trong khung nhìn của mình.
    • Các hệ gợi ý – Tương tự như trên, tôi cũng không bận tâm quá nhiều đến thứ tự ưu tiên của các kết quả gần nhất khi mà tôi chỉ cần khoảng 8 đến 10 kết quả tương tự hiển thị trong khung nhìn của mình.

    Các kỹ thuật ANN tăng tốc độ tìm kiếm bằng cách tiền xử lý dữ liệu thành một chỉ mục hiệu quả và thường được xử lý qua các giai đoạn sau:

    • Vector Transformation – được áp dụng trên các véc-tơ trước khi chúng được lập chỉ mục, ví dụ như giảm chiều dữ liệu.
    • Vector Encoding – được áp dụng trên các véc-tơ để xây dựng chỉ mục thực sự cho tìm kiếm; một số kỹ thuật dựa trên cấu trúc dữ liệu được áp dụng như: Cây, LSH, và lượng tử hóa – một kỹ thuật để mã hóa véc-tơ thành dạng nén, nhỏ gọn hơn nhiều.
    • Thành phần loại bỏ tìm kiếm toàn bộ – được áp dụng trên các véc-tơ để tránh tìm kiếm toàn bộ như k-NN diễn ra, sử dụng các kỹ thuật như: Các tệp đảo ngược, các đồ thị hàng xóm lân cận,…

    Vì sự hữu ích cũng như ứng dụng thực tiễn mà ANN mang lại, nên hiện nay đã có một số thuật toán ANN được triển khai nguồn mở và được sử dụng phổ biến, như: Annoy của Spotify [1], ScaNN của Google [2], Faiss của Facebook [3], và Hnsw [4].

    Tuy nhiên, các kỹ thuật ANN cũng tồn tại nhược điểm, một trong số đó là tài nguyên điện toán, cụ thể là RAM, các kỹ thuật này phải tải toàn bộ các véc-tơ vào RAM để có thể truy xuất các hàng xóm gần nhất.

    Tài liệu tham khảo

    [1] ANNOY library, https://github.com/spotify/annoy

    [2] ScaNN library, https://github.com/google-research/google-research/tree/master/scann

    [3] Faiss library, https://github.com/facebookresearch/faiss

    [4] Hnsw library, https://github.com/nmslib/hnswlib

    Author

    Ha Huu Linh

  • Một số nguyên lý của Elasticsearch

    Một số nguyên lý của Elasticsearch

    Elasticsearch là một công cụ phân tích và tìm kiếm phân tán theo thời gian thực. Nó cho phép khám phá dữ liệu với tốc độ và quy mô chưa từng có trước đây. Nó được sử dụng trong tìm kiếm toàn văn bản (full-text search), tìm kiếm có cấu trúc, phân tích và kết hợp cả ba. Một số hệ thống nổi tiếng sử dụng Elasticsearch có thể kể đến như: GitHub, StackOverflow, Wikipedia,… Elasticsearch còn là một công cụ tìm kiếm nguồn mở, được xây dựng dựa trên Apache Lucene – một thư viện tìm kiếm toàn văn bản.

    Elasticsearch lưu trữ các documents theo mô hình phân tán, các documents là các đối tượng lưu trữ dữ liệu dưới dạng khóa-giá trị (key-value), có thể được chuyển đổi từ định dạng JSON, và thực tế là Elasticsearch nhận các JSON documents để làm đầu vào cho xử lý hoặc để trả về các kết quả cho máy khách. Elasticsearch không chỉ lưu trữ các documents, nó còn lập chỉ mục (indexing) chúng để làm cho chúng có thể tìm kiếm được.

    Elasticsearch cung cấp khả năng mở rộng và tính khả dụng cao nhờ bản chất phân tán, nhờ che giấu toàn bộ việc quản lý hạ tầng giúp cho ứng dụng có thể dễ dàng làm việc với Elasticsearch mà không cần phải có một hiểu biết sâu sắc về vận hành hạ tầng cũng như tổ chức dữ liệu trong Elasticsearch. Tuy nhiên, để có vể vận hành Elasticsearch hiệu quả, việc hiểu cách Elasticsearch tổ chức dữ liệu là cần thiết. Về mặt vật lý, Elasticsearch tổ chức dữ liệu theo 3 cấp độ, với các khái niệm: cluster, node, và shard.

    Một node là một instance đang chạy của Elasticsearch; trong khi đó một cluster bao gồm 1 hoặc nhiều nodes phối hợp để chia sẻ dữ liệu và workload. Khi một node được thêm vào hoặc loại bỏ khỏi cluster thì cluster sẽ tự tổ chức lại để có thể điều phối dữ liệu đều giữa các nodes. Để đạt được điều đó thì một node trong cluster sẽ được chọn làm master node – đóng vai trò như người quản lý, nó chịu trách nhiệm cho việc quản lý các thay đổi trong cluster như thêm hoặc xóa một node khỏi cluster. Master node không tham gia vào các thay đổi hoặc tìm kiếm ở cấp độ document trừ phi cluster chỉ có 1 node duy nhất, việc này giúp cho nó không trở thành bottleneck của hệ thống khi lưu lượng truy cập tăng lên. Trong Elasticsearch thì bất kỳ node nào cũng có thể trở thành master node. Trong khi nói về tổ chức vật lý bên trong Elasticsearch thì không thể không nói về các shards; trong Elasticsearch thì shard là đơn vị vật lý cấp thấp được sử dụng để tổ chức dữ liệu – nơi mà các documents trong một index sẽ được phân bổ trong một vài shards.

    Hình trên minh họa mối quan hệ giữa index và các shards trong Elasticsearch. Về bản chất, index chỉ là một không gian tên logic chứa các documents, các nhà phát triển phần mềm ứng dụng sẽ làm việc với index thay vì trực tiếp với các shards; trong khi đó ở phía dưới, Elasticsearch sẽ tổ chức các documents trong cùng một index vào các shards.

    Như vậy, khi làm việc với Elasticsearch, chúng ta quan tâm chính là về index. Thuật ngữ index có thể được hiểu theo nhiều nghĩa khác nhau, phụ thuộc vào ngữ cảnh: (i) index là một danh từ chỉ nơi lưu trữ các documents, nó giống như một cơ sở dữ liệu trong các cơ sở dữ liệu truyền thống; (ii index cũng có thể được hiểu là một động từ chỉ việc lưu trữ 1 document vào một index (danh từ), thường được gọi là quá trình indexing (lập chỉ mục); và (iii) inverted index chỉ một cấu trúc dữ liệu mà Elasticsearch và Lucene sử dụng để hỗ trợ truy xuất dữ liệu toàn văn bản nhanh chóng.

    Ngày nay, Elasticsearch đã được sử dụng trong rất nhiều ứng dụng nói chung và thương mại điện tử nói riêng bởi nó được thiết kế để hỗ trợ mạnh mẽ trong tìm kiếm và phân tích với dữ liệu. Elasticsearch có thể được triển khai trên các máy chủ tại chỗ (on-premise) nhưng phổ biến hơn hết là trên môi trường đám mây. Vào năm 2015, Amazon Web Services đã triển khai Elasticsearch trên đám mây AWS để cho phép các nhà phát triển phần mềm chạy và vận hành các cụm máy chủ Elasticsearch trên môi trường đám mây. Việc triển khai Elasticsearch trên môi trường đám mây AWS là cần thiết bởi nó cho phép các ứng dụng trên hệ sinh thái AWS có thể dễ dàng giao tiếp, và nhà phát triển phần mềm dễ dàng giám sát hiệu năng hệ thống nhờ sự hỗ trợ gián tiếp từ AWS Cloudwatch và nhiều dịch vụ hạ tầng khác.

    Đến năm 2021, Amazon Web Services khởi chạy dự án nguồn mở OpenSearch, như một bản sao chép từ phiên bản 7.10.2 của Elasticsearch. Và OpenSearch được phát triển, bảo trì và quản lý bởi Amazon Web Services. Dịch vụ hạ tầng từ đó cũng được đổi tên thành AWS OpenSearch.

    Author: Ha Huu Linh

  • Deprecate old API in Swift

    Deprecate old API in Swift

    Trong quá trình develop của mình chắc hẳn các bạn đã từng gặp các warning khi sử dụng các methods cũ, cụ thể là method đó đã đổi tên hoặc không còn sử dụng được trên version nào đó.

    Trong quá trình code nếu có mothod nào không sử dụng chắc các bạn comment hoặc xoá code luôn rồi. Thông thường chỉ có các Mothods của Apple hoặc các Framework, Library thì mới có các warning này. Nhưng làm thế nào để họ có thể đánh dấu methods nào dùng ở version nào? Bài này mình cùng tìm hiểu vấn đề này nhé.

    Định nghĩa:

    Deprecate là một cách thông báo đến người dùng method rằng method này không được sử dụng nữa, không còn hữu ích nữa

    Một số nguyên nhân chính dẫn đến deprecate

    • Đổi tên
    • Có một sự thay thế tốt hơn
    • Không còn sử dụng được (Đã lỗi thời)

    Các cách để deprecate một method

    Basic usage

    Chúng ta có thể đánh dấu một method không còn sử dụng nữa bằng thuộc tính @available

    Đây là hình thức đơn giản nhất để đánh dấu bất kì methods nào không còn sử dụng nữa

    Parameter đầu tiên chỉ ra nền tảng mà method này hỗ trợ. Trong trường hợp này, mình sẽ sử dụng dấu hoa thị (*) để cho biết rằng tất cả các nền tảng đều được hỗ trợ.
    Tập chung và parameter thứ 2, ở đây mình truyền vào là deprecated nghĩa là thông báo rằng method này không còn sử dụng được nữa.

    Chúng ta sẽ nhận được một warning nếu cố gắng sử dụng nó:

    Deprecated version

    Bạn có thể chỉ định phiên bản đầu tiên của method không dùng nữa bằng cách chỉ định số phiên bản bên cạnh parameter deprecated.


    Trong ví dụ này, mình đặt deprecated cho oldApi trên iOS 13. Lưu ý rằng chúng ta cần chỉ định nền tảng, trong trường hợp này là iOS. Cũng warning khi sử dụng, giống với việc không chỉ định version nhưng nội dung warning ở đây là chỉ rõ ra rằng method này bị deprecated ở version nào.

    Deprecated message

    Bạn có thể thêm message mô tả rõ hơn về deprecated, thông báo này sẽ hiển thị cùng với cảnh báo mặc định hoặc thông báo lỗi.

    Dưới dây là một ví dụ khi chúng ta thêm message cho deprecated

    Và đây là là warning khi chúng ta sử dụng cho method

    Rename

    Nếu mục đích deprecate của bạn là vì đổi tên thì Swift đã có parameter hỗ trợ cho việc này

    Sử dụng rename là cách hay nhất để thông báo cho người dùng về tên mới

    Xcode có 2 cách để hỗ trợ thông báo cho người dùng về rename

    • Xcode sẽ generate một Fix button cho chúng ta. Khi click và button đó sẽ rename từ method cũ sang method mới.
    • Xcode sẽ tạo một message về việc đổi tên ở trong warning message.

    Đây là một ví dụ từ Apple, họ thay đổi index(of:) thành firstIndex(of:) trên Swift 5

    Obsoleted

    Nếu một method không thể sử dụng được nữa thì bạn nên sử dụng parameter Obsoleted

    Khi sử dụng obsoleted thay vì nhận được warning thì chúng ta sẽ nhận được một error, error message này gồm message đã được thêm vào parameter và message mặc định

    Đây là ví dụ khi bạn dùng obsoleted

    Vì nhận được lỗi nên chắc chắn chương trình của chúng ta không thể chạy được rồi, các bạn phải bỏ đi hoặc sử dụng method khác để chạy chường trình.

    Thông qua bài này mình đã giới thiệu đến các bạn cách để đánh dấu deprecated.
    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!