Progress on relationships.

This commit is contained in:
Brent Simmons 2017-08-19 22:07:31 -07:00
parent 2d915a890e
commit e76beee988
2 changed files with 162 additions and 135 deletions

View File

@ -1,5 +1,5 @@
// //
// LookupTable.swift // DatabaseLookupTable.swift
// RSDatabase // RSDatabase
// //
// Created by Brent Simmons on 8/5/17. // Created by Brent Simmons on 8/5/17.
@ -10,36 +10,51 @@ import Foundation
// Implement a lookup table for a many-to-many relationship. // Implement a lookup table for a many-to-many relationship.
// Example: CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); // Example: CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID));
// authorID is primaryKey; articleID is foreignKey. // articleID is objectID; authorID is relatedObjectID.
public final class DatabaseLookupTable { public final class DatabaseLookupTable {
private let name: String private let name: String
private let primaryKey: String private let objectIDKey: String
private let foreignKey: String private let relatedObjectIDKey: String
private let relationshipName: String private let relationshipName: String
private weak var relatedTable: DatabaseTable? private weak var relatedTable: DatabaseTable?
private let cache: DatabaseLookupTableCache private let cache: DatabaseLookupTableCache
public init(name: String, primaryKey: String, foreignKey: String, relatedTable: DatabaseTable, relationshipName: String) { public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseTable, relationshipName: String) {
self.name = name self.name = name
self.primaryKey = primaryKey self.objectIDKey = objectIDKey
self.foreignKey = foreignKey self.relatedObjectIDKey = relatedObjectIDKey
self.relatedTable = relatedTable self.relatedTable = relatedTable
self.relationshipName = relationshipName self.relationshipName = relationshipName
self.cache = DatabaseLookupTableCache(relationshipName) self.cache = DatabaseLookupTableCache(relationshipName)
} }
public func attachRelationships(to objects: [DatabaseObject], database: FMDatabase) { public func attachRelationships(to objects: [DatabaseObject], in database: FMDatabase) {
guard let lookupTable = fetchLookupTable(objects.databaseIDs(), database) else { let objectsThatMayHaveRelatedObjects = cache.objectsThatMayHaveRelatedObjects(objects)
return; if objectsThatMayHaveRelatedObjects.isEmpty {
} return
attachRelationshipsUsingLookupTable(to: objects, lookupTable: lookupTable, database: database)
} }
public func saveRelationships(for objects: [DatabaseObject], database: FMDatabase) { attachRelatedObjectsUsingCache(objectsThatMayHaveRelatedObjects, database)
let objectsNeedingFetching = objectsThatMayHaveRelatedObjects.filter { (object) -> Bool in
return object.relatedObjectsWithName(self.relationshipName) == nil
}
if objectsNeedingFetching.isEmpty {
return
}
if let lookupTable = fetchLookupTable(objectsNeedingFetching.databaseIDs(), database) {
attachRelatedObjectsUsingLookupTable(objectsNeedingFetching, lookupTable, database)
}
cache.update(with: objectsNeedingFetching)
}
public func saveRelationships(for objects: [DatabaseObject], in database: FMDatabase) {
var objectsWithNoRelationships = [DatabaseObject]() var objectsWithNoRelationships = [DatabaseObject]()
var objectsWithRelationships = [DatabaseObject]() var objectsWithRelationships = [DatabaseObject]()
@ -53,23 +68,35 @@ public final class DatabaseLookupTable {
} }
} }
removeRelationships(for: objectsWithNoRelationships, database: database) removeRelationships(for: objectsWithNoRelationships, database)
updateRelationships(for: objectsWithRelationships, database: database) updateRelationships(for: objectsWithRelationships, database)
cache.update(with: objects)
} }
} }
private extension DatabaseLookupTable { private extension DatabaseLookupTable {
func removeRelationships(for objects: [DatabaseObject], database: FMDatabase) { // MARK: Removing
removeLookupsForForeignIDs(objects.databaseIDs(), database) func removeRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
let objectIDs = objects.databaseIDs()
let objectIDsToRemove = objectIDs.subtracting(cache.objectIDsWithNoRelationship)
if objectIDsToRemove.isEmpty {
return
} }
func updateRelationships(for objects: [DatabaseObject], database: FMDatabase) { database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name)
let objectsNeedingUpdate = objects.filter { (object) -> Bool in
return !relationshipsMatchCache(object)
} }
// MARK: Saving/Updating
func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
// let objectsNeedingUpdate = objects.filter { (object) -> Bool in
// return !relationshipsMatchCache(object)
// }
} }
func relationshipsMatchCache(_ object: DatabaseObject) -> Bool { func relationshipsMatchCache(_ object: DatabaseObject) -> Bool {
@ -91,81 +118,64 @@ private extension DatabaseLookupTable {
} }
} }
func attachRelationshipsUsingLookupTable(to objects: [DatabaseObject], lookupTable: LookupTable, database: FMDatabase) { // MARK: Attaching
let primaryIDs = lookupTable.primaryIDs() func attachRelatedObjectsUsingCache(_ objects: [DatabaseObject], _ database: FMDatabase) {
if (primaryIDs.isEmpty) {
let lookupTable = cache.lookupTableForObjectIDs(objects.databaseIDs())
attachRelatedObjectsUsingLookupTable(objects, lookupTable, database)
}
func attachRelatedObjectsUsingLookupTable(_ objects: [DatabaseObject], _ lookupTable: LookupTable, _ database: FMDatabase) {
let relatedObjectIDs = lookupTable.relatedObjectIDs()
if (relatedObjectIDs.isEmpty) {
return return
} }
guard let relatedObjects: [DatabaseObject] = relatedTable?.fetchObjectsWithIDs(primaryIDs, database), !relatedObjects.isEmpty else { guard let relatedObjects = fetchRelatedObjectsWithIDs(relatedObjectIDs, database) else {
return return
} }
let relatedObjectsDictionary = relatedObjects.dictionary() let relatedObjectsDictionary = relatedObjects.dictionary()
for object in objects { for object in objects {
attachRelatedObjectsToObjectUsingLookupTable(object, relatedObjectsDictionary, lookupTable)
}
}
func attachRelatedObjectsToObjectUsingLookupTable(_ object: DatabaseObject, _ relatedObjectsDictionary: [String: DatabaseObject], _ lookupTable: LookupTable) {
let identifier = object.databaseID let identifier = object.databaseID
if let lookupValues = lookupTable[identifier], !lookupValues.isEmpty { guard let relatedObjectIDs = lookupTable[identifier], !relatedObjectIDs.isEmpty else {
let primaryIDs = lookupValues.primaryIDs()
let oneObjectRelatedObjects = primaryIDs.flatMap{ (primaryID) -> DatabaseObject? in
return relatedObjectsDictionary[primaryID]
}
object.setRelatedObjects(oneObjectRelatedObjects, name: relationshipName)
}
}
}
func fetchLookupTable(_ foreignIDs: Set<String>, _ database: FMDatabase) -> LookupTable? {
let foreignIDsToLookup = foreignIDs.subtracting(foreignIDsWithNoRelationship)
guard let lookupValues = fetchLookupValues(foreignIDsToLookup, database) else {
return nil
}
updateCache(lookupValues, foreignIDsToLookup)
return LookupTable(lookupValues)
}
func cacheForeignIDsWithNoRelationships(_ foreignIDs: Set<String>) {
foreignIDsWithNoRelationship.formUnion(foreignIDs)
for foreignID in foreignIDs {
cache[foreignID] = nil
}
}
func updateCache(_ lookupValues: Set<LookupValue>, _ foreignIDs: Set<String>) {
// Maintain foreignIDsWithNoRelationship.
// If a relationship exist, remove the foreignID from foreignIDsWithNoRelationship.
// If a relationship does not exist, add the foreignID to foreignIDsWithNoRelationship.
let foreignIDsWithRelationship = lookupValues.foreignIDs()
let foreignIDs
for foreignID in foreignIDs {
if !foreignIDsWithRelationship.contains(foreignID) {
foreignIDsWithNoRelationship.insert(foreignID)
}
}
}
func removeLookupsForForeignIDs(_ foreignIDs: Set<String>, _ database: FMDatabase) {
let foreignIDsToRemove = foreignIDs.subtracting(foreignIDsWithNoRelationship)
if foreignIDsToRemove.isEmpty {
return return
} }
let relatedObjects = relatedObjectIDs.flatMap { relatedObjectsDictionary[$0] }
foreignIDsWithNoRelationship.formUnion(foreignIDsToRemove) if !relatedObjects.isEmpty {
object.setRelatedObjects(relatedObjects, name: relationshipName)
database.rs_deleteRowsWhereKey(foreignKey, inValues: Array(foreignIDsToRemove), tableName: name) }
} }
func fetchLookupValues(_ foreignIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? { // MARK: Fetching
guard !foreignIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(foreignKey, inValues: Array(foreignIDs), tableName: name) else { func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set<String>, _ database: FMDatabase) -> [DatabaseObject]? {
guard let relatedObjects = relatedTable?.fetchObjectsWithIDs(relatedObjectIDs, database), !relatedObjects.isEmpty else {
return nil
}
return relatedObjects
}
func fetchLookupTable(_ objectIDs: Set<String>, _ database: FMDatabase) -> LookupTable? {
guard let lookupValues = fetchLookupValues(objectIDs, database) else {
return nil
}
return LookupTable(lookupValues: lookupValues)
}
func fetchLookupValues(_ objectIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else {
return nil return nil
} }
return lookupValuesWithResultSet(resultSet) return lookupValuesWithResultSet(resultSet)
@ -178,130 +188,147 @@ private extension DatabaseLookupTable {
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? { func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
guard let primaryID = row.string(forColumn: primaryKey) else { guard let objectID = row.string(forColumn: objectIDKey) else {
return nil return nil
} }
guard let foreignID = row.string(forColumn: foreignKey) else { guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else {
return nil return nil
} }
return LookupValue(primaryID: primaryID, foreignID: foreignID) return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID)
} }
} }
struct LookupTable { struct LookupTable {
private let dictionary: [String: Set<LookupValue>] private let dictionary: [String: Set<String>] // objectID: Set<relatedObjectID>
init(_ lookupValues: Set<LookupValue>) { init(dictionary: [String: Set<String>]) {
var d = [String: Set<LookupValue>]() self.dictionary = dictionary
}
init(lookupValues: Set<LookupValue>) {
var d = [String: Set<String>]()
for lookupValue in lookupValues { for lookupValue in lookupValues {
let foreignID = lookupValue.foreignID let objectID = lookupValue.objectID
if d[foreignID] == nil { let relatedObjectID: String = lookupValue.relatedObjectID
d[foreignID] = Set([lookupValue]) if d[objectID] == nil {
d[objectID] = Set([relatedObjectID])
} }
else { else {
d[foreignID]!.insert(lookupValue) d[objectID]!.insert(relatedObjectID)
} }
} }
self.dictionary = d self.init(dictionary: d)
} }
func primaryIDs() -> Set<String> { func relatedObjectIDs() -> Set<String> {
var ids = Set<String>() var ids = Set<String>()
for (_, lookupValues) in dictionary { for (_, relatedObjectIDs) in dictionary {
ids.formUnion(lookupValues.primaryIDs()) ids.formUnion(relatedObjectIDs)
} }
return ids return ids
} }
subscript(_ foreignID: String) -> Set<LookupValue>? { subscript(_ objectID: String) -> Set<String>? {
get { get {
return dictionary[foreignID] return dictionary[objectID]
} }
} }
} }
struct LookupValue: Hashable { struct LookupValue: Hashable {
let primaryID: String let objectID: String
let foreignID: String let relatedObjectID: String
let hashValue: Int let hashValue: Int
init(primaryID: String, foreignID: String) { init(objectID: String, relatedObjectID: String) {
self.primaryID = primaryID self.objectID = objectID
self.foreignID = foreignID self.relatedObjectID = relatedObjectID
self.hashValue = (primaryID + foreignID).hashValue self.hashValue = (objectID + relatedObjectID).hashValue
} }
static public func ==(lhs: LookupValue, rhs: LookupValue) -> Bool { static public func ==(lhs: LookupValue, rhs: LookupValue) -> Bool {
return lhs.primaryID == rhs.primaryID && lhs.foreignID == rhs.foreignID return lhs.objectID == rhs.objectID && lhs.relatedObjectID == rhs.relatedObjectID
} }
} }
private final class DatabaseLookupTableCache { private final class DatabaseLookupTableCache {
var objectIDsWithNoRelationship = Set<String>()
private let relationshipName: String private let relationshipName: String
private var foreignIDsWithNoRelationship = Set<String>() private var cachedLookups = [String: Set<String>]() // objectID: Set<relatedObjectID>
private var cachedLookups = [String: Set<String>]() // foreignID: Set<primaryID>
init(_ relationshipName: String) { init(_ relationshipName: String) {
self.relationshipName = relationshipName self.relationshipName = relationshipName
} }
func updateCacheWithIDsWithNoRelationship(_ foreignIDs: Set<String>) { func update(with objects: [DatabaseObject]) {
foreignIDsWithNoRelationship.formUnion(foreignIDs) var idsWithRelationships = Set<String>()
for foreignID in foreignIDs { var idsWithNoRelationships = Set<String>()
cachedLookups[foreignID] = nil
}
}
func updateCacheWithObjects(_ object: [DatabaseObject]) {
var foreignIDsWithRelationship = Set<String>()
for object in objects { for object in objects {
let objectID = object.databaseID
if let relatedObjects = object.relatedObjectsWithName, !relatedObjects.isEmpty { if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty {
foreignIDsWithRelationship.insert(object.databaseID) idsWithRelationships.insert(objectID)
self[objectID] = relatedObjects.databaseIDs()
} }
else { else {
updateCacheWithIDsWithNoRelationship(objects.databaseIDs()) idsWithNoRelationships.insert(objectID)
self[objectID] = nil
} }
} }
foreignIDsWithNoRelationship.subtract(foreignIDsWithRelationships) objectIDsWithNoRelationship.subtract(idsWithRelationships)
objectIDsWithNoRelationship.formUnion(idsWithNoRelationships)
} }
func foreignIDHasNoRelationship(_ foreignID: String) -> Bool { subscript(_ objectID: String) -> Set<String>? {
get {
return foreignIDsWithNoRelationship.contains(foreignID) return cachedLookups[objectID]
}
set {
cachedLookups[objectID] = newValue
}
} }
func relationshipIDsForForeignID(_ foreignID: String) -> Set<String>? { func objectsThatMayHaveRelatedObjects(_ objects: [DatabaseObject]) -> [DatabaseObject] {
return cachedLookups[foreignID] // Filter out objects that are known to have no related objects
return objects.filter{ !objectIDsWithNoRelationship.contains($0.databaseID) }
}
func lookupTableForObjectIDs(_ objectIDs: Set<String>) -> LookupTable {
var d = [String: Set<String>]()
for objectID in objectIDs {
if let relatedObjectIDs = self[objectID] {
d[objectID] = relatedObjectIDs
}
}
return LookupTable(dictionary: d)
} }
} }
private extension Set where Element == LookupValue { private extension Set where Element == LookupValue {
func primaryIDs() -> Set<String> { func objectIDs() -> Set<String> {
return Set<String>(self.map { $0.primaryID }) return Set<String>(self.map { $0.objectID })
} }
func foreignIDs() -> Set<String> { func relatedObjectIDs() -> Set<String> {
return Set<String>(self.map { $0.foreignID }) return Set<String>(self.map { $0.relatedObjectID })
} }
} }

Binary file not shown.