Progress on relationships.
This commit is contained in:
parent
2d915a890e
commit
e76beee988
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
ToDo.ooutline
BIN
ToDo.ooutline
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user