Upgrade to Pro — share decks privately, control downloads, hide ads and more …

APIからFirebase Realtime Databaseへ - クライアントサイドから見...

d_date
November 03, 2017

APIからFirebase Realtime Databaseへ - クライアントサイドから見るサーバーレスアーキテクチャー

2018/11/3 serverlessconf Tokyo
Firebase Realtime Database / Cloud Firestoreの話

d_date

November 03, 2017
Tweet

More Decks by d_date

Other Decks in Programming

Transcript

  1. Replace API to Firebase Database - The view from client

    side Daiki Matsudate / @d_date 2017/11/03 / Serverlessconf Tokyo 2017
  2. Agenda • Firebase Realtime Database • Feature • Introduce to

    Mobile App • Architecture • Testing • ???
  3. • NoSQL cloud database • Realtime Data Sync with JSON

    Tree • Available data on offline Firebase Database
  4. Replace API to Realtime Database • Reduce building API man-hours

    • No more performance tunings • Easy to handle data in offline Purpose
  5. Denormalization in NoSQL { "users": { "user1": { "name": "Alice"

    }, "user2": { "name": "Bob" } }, "links": { "link1": { "title": "Example", "href": "http://example.org", "submitted": "user1" } }, "comments": { "comment1": { "link": "link1", "body": "This is awesome!", "author": "user2" } } }
  6. Denormalization in NoSQL { "users": { "user1": { "name": "Alice"

    }, "user2": { "name": "Bob" } }, "links": { "link1": { "title": "Example", "href": "http://example.org", "submitted": "user1" } }, "comments": { "comment1": { "link": "link1", "body": "This is awesome!", "author": "user2" } } }
  7. Denormalization in NoSQL { "users": { "user1": { "name": "Alice"

    }, "user2": { "name": "Bob" } }, "links": { "link1": { "title": "Example", "href": "http://example.org", "submitted": "user1" } }, "comments": { "comment1": { "link": "link1", "body": "This is awesome!", "author": "user2" } } } Join data in client
  8. JOIN data in client in Swift self.ref.child("comments").observeSingleEvent(of: .value, with: {

    [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) })
  9. Not Swifty!! self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in

    guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) }) “Stringly“ typed Pyramid of Death
  10. Easy to replace from API? self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak

    self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) }) Need reference of database
  11. Ideal State • Data from Firebase ≠ Data in app

    -> Need translation • Easy to replace from API to Firebase -> I/F is same as API, then want to replace DAO
  12. What is this architecture? Entity Data Access Interface Data Access

    to Firebase Interactor Model Presenter View
  13. How to resolve Pyramid of Death self.ref.child("comments").observeSingleEvent(of: .value, with: {

    [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) }) Pyramid of Death
  14. How to resolve Pyramid of Death self.ref.child("comments").observeSingleEvent(of: .value, with: {

    [weak self] (snapshot) in guard let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) })
  15. protocol Entity: Codable {} struct UserEntity: Entity { let name:

    String } struct LinkEntity: Entity { let title: String let href: URL let submitted: String } struct CommentEntity: Entity { let link: String let body: String let author: String } Entity Entity Data Access Data Access Interactor Model Presenter View
  16. Data Access + RxSwift extension DatabaseReference { func get<T: Entity>()

    -> Single<T> { return Single.create(subscribe: { (observer) in self.observeSingleEvent(of: .value, with: { (snapshot) in guard let jsonObject = snapshot.value else { observer(.error(ProviderError.valueNotExist)) return } do { let data = try JSONSerialization.data(withJSONObject: jsonObject) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch(let error) { observer(.error(error)) } }) return Disposables.create() }) } }
  17. Data Access + RxSwift extension DatabaseReference { func get<T: Entity>()

    -> Single<T> { return Single.create(subscribe: { (observer) in self.observeSingleEvent(of: .value, with: { (snapshot) in guard let jsonObject = snapshot.value else { observer(.error(ProviderError.valueNotExist)) return } do { let data = try JSONSerialization.data(withJSONObject: jsonObject) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch(let error) { observer(.error(error)) } }) return Disposables.create() }) } } If value not exist, invoke error
  18. Data Access + RxSwift extension DatabaseReference { func get<T: Entity>()

    -> Single<T> { return Single.create(subscribe: { (observer) in self.observeSingleEvent(of: .value, with: { (snapshot) in guard let jsonObject = snapshot.value else { observer(.error(ProviderError.valueNotExist)) return } do { let data = try JSONSerialization.data(withJSONObject: jsonObject) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch(let error) { observer(.error(error)) } }) return Disposables.create() }) } } If fail to serialize JSON, invoke error
  19. Data Provider class DataProvider { static let shared = DataProvider()

    private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single<UserEntity> { return ref.child("users").child(key).get() } func getLink(key: String) -> Single<LinkEntity> { return ref.child("links").child(key).get() } func getComments() -> Single<CommentEntity> { return ref.child("comments").get() } }
  20. Data Provider class DataProvider { static let shared = DataProvider()

    private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single<UserEntity> { return ref.child("users").child(key).get() } func getLink(key: String) -> Single<LinkEntity> { return ref.child("links").child(key).get() } func getComments() -> Single<CommentEntity> { return ref.child("comments").get() } } Singleton in Swift
  21. Data Provider class DataProvider { static let shared = DataProvider()

    private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single<UserEntity> { return ref.child("users").child(key).get() } func getLink(key: String) -> Single<LinkEntity> { return ref.child("links").child(key).get() } func getComments() -> Single<CommentEntity> { return ref.child("comments").get() } } Retain DatabaseReference
  22. Data Provider class DataProvider { static let shared = DataProvider()

    private init() {} lazy var ref: DatabaseReference = { return Database.database().reference() }() func getUser(key: String) -> Single<UserEntity> { return ref.child("users").child(key).get() } func getLink(key: String) -> Single<LinkEntity> { return ref.child("links").child(key).get() } func getComments() -> Single<CommentEntity> { return ref.child("comments").get() } } Get Entity from Database, then return Single<Entity>
  23. Data Access struct UserRepository { let dataStore: LinkDataStore func get(userId:

    String) -> Single<UserEntity> { return dataStore.get(key: userId) } } struct UserDataStore { let provider = DataProvider.shared func get(key: String) -> Single<UserEntity> { return provider.getUser(key: key) } } Entity Data Access Data Access Interactor Model Presenter View
  24. Data Access struct UserRepository { let dataStore: LinkDataStore func get(userId:

    String) -> Single<UserEntity> { return dataStore.get(key: userId) } } struct UserDataStore { let provider = DataProvider.shared func get(key: String) -> Single<UserEntity> { return provider.getUser(key: key) } } I/F for data access Implementation for data access Entity Data Access Data Access Interactor Model Presenter View
  25. Interactor (UseCase) struct UseCase { let commentsRepository: CommentRepository let usersRepository:

    UserRepository let linksRepository: LinkRepository func getComments() -> Single<[CommentModel]> { return commentsRepository.get() .map { (comment) -> [CommentModel] in let link = self.getLink(linkId: comment.link) let author = self.getUser(userId: comment.author) return Single<(LinkModel, UserModel)>.zip(link, author) { ($0, $1) }).map(translator: CommentTranslator()) } } } Entity Data Access Data Access Interactor Model Presenter View
  26. Before self.ref.child("comments").observeSingleEvent(of: .value, with: { [weak self] (snapshot) in guard

    let strongSelf = self else { return } guard let comments = snapshot.value as? [String: Any] else { return } comments.forEach({ (key, value) in guard let comment = value as? [String: Any] else { return } let linkKey: String = comment["link"] as! String // "link1" let authorKey: String = comment["author"] as! String // "user2" strongSelf.ref.child("links").child(linkKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let link = snapshot.value as? [String: Any] else { return } let submittedKey: String = link["submitted"] as! String // "user1" strongSelf.ref.child("users").child(submittedKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Alice" print(name) }) }) strongSelf.ref.child("users").child(authorKey).observeSingleEvent(of: .value, with: { (snapshot) in guard let user = snapshot.value as? [String: Any] else { return } let name: String = user["name"] as! String // “Bob” print(name) }) }) })
  27. Presenter class Presenter { let useCase: UseCase init(useCase: UseCase) {

    self.useCase = useCase } func getComments() -> Single<[CommentModel]> { return useCase.getComments() } } Entity Data Access Data Access Interactor Model Presenter View
  28. View class ViewController: UIViewController { weak var presenter: Presenter! private

    let disposeBag = DisposeBag() func inject(presenter: Presenter) { self.presenter = presenter } override func viewDidLoad() { super.viewDidLoad() presenter.getComments() .subscribe(onSuccess: { (models) in //Handling model }, onError: { (error) in //Handling Error }).disposed(by: disposeBag) } } Entity Data Access Data Access Interactor Model Presenter View
  29. View class ViewController: UIViewController { weak var presenter: Presenter! private

    let disposeBag = DisposeBag() func inject(presenter: Presenter) { self.presenter = presenter } override func viewDidLoad() { super.viewDidLoad() presenter.getComments() .subscribe(onSuccess: { (models) in //Handling model }, onError: { (error) in //Handling Error }).disposed(by: disposeBag) } } Entity Data Access Data Access Interactor Model Presenter View Focus on UI Handling
  30. Side Effect - Easy Unit Testing Entity Data Access Interface

    Mock Object Interactor Model Presenter View Easy to replace
  31. class DatabaseReference { func get<T: Entity>() -> Single<T> { return

    Single.create(subscribe: { (observer) in guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Stub", ofType: "bundle") else { observer(.error(SerializeError.missingBundle)) return Disposables.create() } let jsonName = String(describing: T.self) guard let path = Bundle(path: bundlePath)?.path(forResource: jsonName, ofType: "json") else { observer(.error(SerializeError.missingJson(jsonName: jsonName))) return Disposables.create() } do{ let data = try Data(contentsOf: URL(fileURLWithPath: path)) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch { observer(.error(error)) } return Disposables.create() }) } } Side Effect - Easy Unit Testing
  32. class DatabaseReference { func get<T: Entity>() -> Single<T> { return

    Single.create(subscribe: { (observer) in guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Stub", ofType: "bundle") else { observer(.error(SerializeError.missingBundle)) return Disposables.create() } let jsonName = String(describing: T.self) guard let path = Bundle(path: bundlePath)?.path(forResource: jsonName, ofType: "json") else { observer(.error(SerializeError.missingJson(jsonName: jsonName))) return Disposables.create() } do{ let data = try Data(contentsOf: URL(fileURLWithPath: path)) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch { observer(.error(error)) } return Disposables.create() }) } } Side Effect - Easy Unit Testing FackObject
  33. class DatabaseReference { func get<T: Entity>() -> Single<T> { return

    Single.create(subscribe: { (observer) in guard let bundlePath = Bundle(for: type(of: self)).path(forResource: "Stub", ofType: "bundle") else { observer(.error(SerializeError.missingBundle)) return Disposables.create() } let jsonName = String(describing: T.self) guard let path = Bundle(path: bundlePath)?.path(forResource: jsonName, ofType: "json") else { observer(.error(SerializeError.missingJson(jsonName: jsonName))) return Disposables.create() } do{ let data = try Data(contentsOf: URL(fileURLWithPath: path)) let entity = try JSONDecoder().decode(T.self, from: data) observer(.success(entity)) } catch { observer(.error(error)) } return Disposables.create() }) } } Return Entity created from Stub JSON Side Effect - Easy Unit Testing FackObject
  34. Tips - Realtime update or not lazy var realtimeRef: DatabaseReference

    = { let ref = Database.database().reference() ref.keepSynced(true) return ref }() func getFavorite(userId: String) -> Single<Void> { return realtimeRef.child(userId).child("favorites").get() }
  35. Tips - Realtime update or not lazy var realtimeRef: DatabaseReference

    = { let ref = Database.database().reference() ref.keepSynced(true) return ref }() func getFavorite(userId: String) -> Single<Void> { return realtimeRef.child(userId).child("favorites").get() } Reflect database update immediately
  36. Pros/Cons • Reduce building API man-hours • No more performance

    tunings • Easy to handle data in offline Pros
  37. Pros/Cons • Reduce building API man-hours • No more performance

    tunings • Easy to handle data in offline Pros #
  38. Pros/Cons • Must have join logic in client side •

    Need more money $ to build in some case • Data cache system doesn’t suit for your app in some case • Sorting and Query are hard to use • Scaling requires sharding (100,000 concurrent connections & 1,000 writes/sec.) Cons
  39. Pros/Cons • Must have join logic in client side •

    Need more money $ to build in some case • Data cache system doesn’t suit for your app in some case • Sorting and Query are hard to use • Scaling requires sharding (100,000 concurrent connections & 1,000 writes/sec.) Cons %
  40. • Stores data in documents organized in collections • Sending

    data with protobuf, which have more types • Introduced reference type, so requires less denormalization and data flattening • Indexed queries with compound sorting and filtering Cloud Firestore
  41. What will be changed on architecture Entity Data Access Interface

    Data Access to Firebase Interactor Model Presenter View
  42. What will be changed on architecture Entity Data Access Interface

    Data Access to Firebase Interactor Model Presenter View
  43. Recap • Serverless architecture ( mBaaS / SaaS and etc.

    ) is also effective to use on native apps • API server can be replaced by Firebase Database / Cloud firestore in some case • Replaceable DAO and separate Entity and Model in app is important • With side effect, easy to test modules
  44. References • Clean Code: A Handbook of Agile Software Craftsmanship

    by Robert C. Martin • https://firebase.google.com/docs/firestore/rtdb-vs-firestore • https://firebase.google.com/docs/firestore/firestore-for- rtdb • https://speakerdeck.com/bpyamasinn/api-wo-firebase- realtime-database-niyi-xing-siteqi-fu-itakoto-ver2