NetNewsWire/Frameworks/RSDatabase/Related Objects/DatabaseLookupTable.swift

225 lines
7.9 KiB
Swift
Raw Normal View History

2017-08-06 21:37:47 +02:00
//
2017-08-20 07:07:31 +02:00
// DatabaseLookupTable.swift
2017-08-06 21:37:47 +02:00
// RSDatabase
//
// Created by Brent Simmons on 8/5/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// 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));
2017-08-20 07:07:31 +02:00
// articleID is objectID; authorID is relatedObjectID.
2017-08-07 06:16:13 +02:00
2017-08-08 07:09:10 +02:00
public final class DatabaseLookupTable {
2017-08-06 21:37:47 +02:00
2017-08-14 21:54:57 +02:00
private let name: String
2017-08-20 07:07:31 +02:00
private let objectIDKey: String
private let relatedObjectIDKey: String
2017-08-14 21:54:57 +02:00
private let relationshipName: String
private let relatedTable: DatabaseRelatedObjectsTable
2017-09-11 15:46:32 +02:00
private var objectIDsWithNoRelatedObjects = Set<String>()
public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseRelatedObjectsTable, relationshipName: String) {
2017-08-06 21:37:47 +02:00
self.name = name
2017-08-20 07:07:31 +02:00
self.objectIDKey = objectIDKey
self.relatedObjectIDKey = relatedObjectIDKey
2017-08-14 21:54:57 +02:00
self.relatedTable = relatedTable
self.relationshipName = relationshipName
2017-08-06 21:37:47 +02:00
}
public func fetchRelatedObjects(for objectIDs: Set<String>, in database: FMDatabase) -> RelatedObjectsMap? {
2017-09-11 15:46:32 +02:00
let objectIDsThatMayHaveRelatedObjects = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
if objectIDsThatMayHaveRelatedObjects.isEmpty {
return nil
}
guard let relatedObjectIDsMap = fetchRelatedObjectIDsMap(objectIDsThatMayHaveRelatedObjects, database) else {
2017-09-11 15:46:32 +02:00
objectIDsWithNoRelatedObjects.formUnion(objectIDsThatMayHaveRelatedObjects)
return nil
}
if let relatedObjects = fetchRelatedObjectsWithIDs(relatedObjectIDsMap.relatedObjectIDs(), database) {
2017-09-11 15:46:32 +02:00
let relatedObjectsMap = RelatedObjectsMap(relatedObjects: relatedObjects, relatedObjectIDsMap: relatedObjectIDsMap)
2017-09-11 15:46:32 +02:00
let objectIDsWithNoFetchedRelatedObjects = objectIDsThatMayHaveRelatedObjects.subtracting(relatedObjectsMap.objectIDs())
2017-09-11 15:46:32 +02:00
objectIDsWithNoRelatedObjects.formUnion(objectIDsWithNoFetchedRelatedObjects)
return relatedObjectsMap
2017-09-11 15:46:32 +02:00
}
return nil
}
public func saveRelatedObjects(for objects: [DatabaseObject], in database: FMDatabase) {
2017-08-14 21:54:57 +02:00
var objectsWithNoRelationships = [DatabaseObject]()
var objectsWithRelationships = [DatabaseObject]()
for object in objects {
if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty {
objectsWithRelationships += [object]
}
else {
objectsWithNoRelationships += [object]
}
}
2017-08-20 07:07:31 +02:00
removeRelationships(for: objectsWithNoRelationships, database)
updateRelationships(for: objectsWithRelationships, database)
objectIDsWithNoRelatedObjects.formUnion(objectsWithNoRelationships.databaseIDs())
objectIDsWithNoRelatedObjects.subtract(objectsWithRelationships.databaseIDs())
2017-08-07 06:16:13 +02:00
}
2017-08-14 21:54:57 +02:00
}
2017-08-07 06:16:13 +02:00
// MARK: - Private
2017-08-14 21:54:57 +02:00
private extension DatabaseLookupTable {
2017-08-20 07:07:31 +02:00
// MARK: Removing
func removeRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
2017-08-20 07:07:31 +02:00
let objectIDs = objects.databaseIDs()
let objectIDsToRemove = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
2017-08-20 07:07:31 +02:00
if objectIDsToRemove.isEmpty {
return
}
database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name)
}
2017-08-20 07:07:31 +02:00
func deleteLookups(for objectID: String, _ relatedObjectIDs: Set<String>, _ database: FMDatabase) {
guard !relatedObjectIDs.isEmpty else {
assertionFailure("deleteLookups: expected non-empty relatedObjectIDs")
return
}
// delete from authorLookup where articleID=? and authorID in (?,?,?)
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(relatedObjectIDs.count))!
let sql = "delete from \(name) where \(objectIDKey)=? and \(relatedObjectIDKey) in \(placeholders)"
let parameters: [Any] = [objectID] + Array(relatedObjectIDs)
let _ = database.executeUpdate(sql, withArgumentsIn: parameters)
}
2017-08-20 07:07:31 +02:00
// MARK: Saving/Updating
func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
if let lookupTable = fetchRelatedObjectIDsMap(objects.databaseIDs(), database) {
for object in objects {
syncRelatedObjectsAndLookupTable(object, lookupTable, database)
}
}
// Save the actual related objects.
let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objects)
if relatedObjectsToSave.isEmpty {
assertionFailure("updateRelationships: expected relatedObjectsToSave would not be empty. This should be unreachable.")
return
}
relatedTable.save(relatedObjectsToSave, in: database)
}
2017-08-20 01:30:55 +02:00
func uniqueArrayOfRelatedObjects(with objects: [DatabaseObject]) -> [DatabaseObject] {
// Cant create a Set, because we cant make a Set<DatabaseObject>, because protocol-conforming objects cant be made Hashable or even Equatable.
// We still want the array to include only one copy of each object, but we have to do it the slow way. Instruments will tell us if this is a performance problem.
2017-08-20 01:30:55 +02:00
var relatedObjectsUniqueArray = [DatabaseObject]()
for object in objects {
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
assertionFailure("uniqueArrayOfRelatedObjects: expected every object to have related objects.")
continue
2017-08-20 01:30:55 +02:00
}
for relatedObject in relatedObjects {
if !relatedObjectsUniqueArray.includesObjectWithDatabaseID(relatedObject.databaseID) {
relatedObjectsUniqueArray += [relatedObject]
}
2017-08-20 01:30:55 +02:00
}
}
return relatedObjectsUniqueArray
}
func syncRelatedObjectsAndLookupTable(_ object: DatabaseObject, _ lookupTable: RelatedObjectIDsMap, _ database: FMDatabase) {
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
assertionFailure("syncRelatedObjectsAndLookupTable should be called only on objects with related objects.")
return
}
let relatedObjectIDs = relatedObjects.databaseIDs()
let lookupTableRelatedObjectIDs = lookupTable[object.databaseID] ?? Set<String>()
let relatedObjectIDsToDelete = lookupTableRelatedObjectIDs.subtracting(relatedObjectIDs)
if !relatedObjectIDsToDelete.isEmpty {
deleteLookups(for: object.databaseID, relatedObjectIDsToDelete, database)
}
let relatedObjectIDsToSave = relatedObjectIDs.subtracting(lookupTableRelatedObjectIDs)
if !relatedObjectIDsToSave.isEmpty {
saveLookups(for: object.databaseID, relatedObjectIDsToSave, database)
}
}
func saveLookups(for objectID: String, _ relatedObjectIDs: Set<String>, _ database: FMDatabase) {
for relatedObjectID in relatedObjectIDs {
let d: [NSObject: Any] = [(objectIDKey as NSString): objectID, (relatedObjectIDKey as NSString): relatedObjectID]
let _ = database.rs_insertRow(with: d, insertType: .orIgnore, tableName: name)
}
}
2017-08-20 07:07:31 +02:00
// MARK: Fetching
func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set<String>, _ database: FMDatabase) -> [DatabaseObject]? {
guard let relatedObjects = relatedTable.fetchObjectsWithIDs(relatedObjectIDs, in: database), !relatedObjects.isEmpty else {
2017-08-20 07:07:31 +02:00
return nil
2017-08-07 06:16:13 +02:00
}
2017-08-20 07:07:31 +02:00
return relatedObjects
2017-08-07 06:16:13 +02:00
}
2017-08-14 21:54:57 +02:00
func fetchRelatedObjectIDsMap(_ objectIDs: Set<String>, _ database: FMDatabase) -> RelatedObjectIDsMap? {
2017-08-14 21:54:57 +02:00
2017-08-20 07:07:31 +02:00
guard let lookupValues = fetchLookupValues(objectIDs, database) else {
return nil
2017-08-14 21:54:57 +02:00
}
return RelatedObjectIDsMap(lookupValues: lookupValues)
2017-08-14 21:54:57 +02:00
}
2017-08-20 07:07:31 +02:00
func fetchLookupValues(_ objectIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
2017-08-14 21:54:57 +02:00
2017-08-20 07:07:31 +02:00
guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else {
2017-08-14 21:54:57 +02:00
return nil
}
return lookupValuesWithResultSet(resultSet)
}
2017-08-06 21:37:47 +02:00
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
2017-08-20 07:07:31 +02:00
2017-08-06 21:37:47 +02:00
return resultSet.mapToSet(lookupValueWithRow)
}
2017-08-20 07:07:31 +02:00
2017-08-08 05:00:46 +02:00
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
2017-08-20 07:07:31 +02:00
guard let objectID = row.string(forColumn: objectIDKey) else {
2017-08-06 21:37:47 +02:00
return nil
}
2017-08-20 07:07:31 +02:00
guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else {
2017-08-06 21:37:47 +02:00
return nil
}
2017-08-20 07:07:31 +02:00
return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID)
2017-08-06 21:37:47 +02:00
}
}