Core database relationship are only partially updating.

I created a data structure based on a dictionary of words. The purpose is to link each word to all other words made up of the same letters plus one.

Example: table -> ablate, cablet, tabled, gablet, albeit, albite, etc.

For this I built a data model made of three entities: Word, Draw, Link.

A Draw is a set of letters corresponding to a Word and sorted in alphabetic order, like : HOUSE -> EHOSU. A Link is a letter that you add to a Draw to get another Draw.

So my data model looks like this:

And here is how I implemented it in Xcode:

Entity Word

(let's forget the attribute optComp that plays no role here)

Entity Draw

Entity Link

I am populating the data in two steps:

  1. first I read a list of words from a .txt source and I populate the Word entity and at the same time the Draw entity with the corresponding relationship (function loadDic())

This first step apparently works fine. I can easily find all anagrams of any word with something like word.sort.word.spelling

  1. I read through the Draw entity. For each draw I seek all existing +1 draws considering each letter of the alphabet. If there are, I create a Link and add the relationships (function createLinks())

Here is where something goes wrong. If the Link's and the relationship Draw.plus seem to be correctly created, the other relationship Link.gives is only partially populated, say 50%.

Moreover, I tried to apply an additional routine (updateLinks()) , focusing only on Link's with an empty Link.gives relationship and updating them. But again, only 50% of the nil relationships appear to be populated.

I could not find out why those relationships are not properly populated. If someone can help me out I would be grateful.

Here is the code:

LoadDic() function (OK) :

func loadDic() {
    print("Loading dictionary...")
    dataAlreadyLoaded.toggle()
    guard let url = Bundle.main.url(forResource: INPUT_FILE, withExtension: "txt") else {
        fatalError("\(INPUT_FILE).txt not found")
    }
    if let dico = try? String(contentsOf: url, encoding: String.Encoding.utf8 ) {
        let lines = dico.split(separator: "\r\n")
        for line in lines {
            let lineArray = line.split(separator: " ")
            print("\(lineArray[0])") // word
            let wordSorted = String(lineArray[0].sorted())
            let draw = getDraw(drawLetters: wordSorted) ?? addDraw(drawLetters: wordSorted) // look if draw already exists, otherwise create new one.
            let wordItem = Word(context: viewContext) // create word entry with to-one-relationship to draw
            wordItem.spelling = String(lineArray[0])
            wordItem.optComp = (Int(String(lineArray[1])) == 1)
            wordItem.sort = draw
            do {
                try viewContext.save()
            } catch {
                print("Errort saving ods9: \(error)")
            }
        }
    }
    print("Ods Chargé")
}

func addDraw(drawLetters: String) -> Draw {
    let newDraw = Draw(context: viewContext)
    newDraw.draw = drawLetters
    return(newDraw)
}

func getDraw(drawLetters: String) -> Draw? {
    let request: NSFetchRequest<Draw> = Draw.fetchRequest()
    request.entity = Draw.entity()
    request.predicate = NSPredicate(format: "draw == %@", drawLetters)
    do {
        let drw = try viewContext.fetch(request)
        return drw.isEmpty ? nil : drw[0]
    } catch {
        print("Erreur recherche Tirage")
        return nil
    }
}

createLinks() function (NOK):

func createLinks() {
    var erreur = " fetch request <Draw>"
    let request: NSFetchRequest<Draw> = Draw.fetchRequest()
    request.entity = Draw.entity()
    request.predicate = NSPredicate(value: true)
    print("Building relationships...")
    do {
        let draws = try viewContext.fetch(request)
        count = draws.count
        for draw in draws {
            print("\(count) - \(draw.draw!)")
            linkTable.removeAll()
            for letter in ALPHABET {
                print(letter)
                let drawLettersPlus = String((draw.draw! + String(letter)).sorted()) // draw with one more letter
                if let drawPlus = draws.first(where: { $0.draw == drawLettersPlus }) { // look for Draw entity that matches augmented draw
                    let linkItem = Link(context: viewContext) // if found, create new link based on letter with relationship to augmented draw
                    linkItem.letter = String(letter)
                    linkItem.gives = drawPlus
                    erreur = " saving \(draw.draw!) + \(letter)"
                    try viewContext.save()
                    linkTable.append(linkItem) // saves link to populate the one-to-many relationship of the initial draw, once the alphabet is through
                }
            }
            let drawUpdate = draw as NSManagedObject // populate the one-to-many relationship of the initial draw
            let linkSet = Set(linkTable) as NSSet
            drawUpdate.setValue(linkSet, forKey: "plus")
            erreur = " saving \(draw.draw!) links plus"
            try viewContext.save()
            count -= 1 // next draw
        }
    } catch {
        print("Error " + erreur)
    }
    print("Graph completed")
}

updateLinks function (NOK):

    func updateLinks() {
        var erreur = "fetch request <Link>"
        let request: NSFetchRequest<Link> = Link.fetchRequest()
        request.entity = Link.entity()
        print("Running patch...")
        do {
            request.predicate = NSPredicate(format: "gives == nil")
            let links = try viewContext.fetch(request)
            for link in links {
                let baseDraw = link.back!.draw!
                print("\(baseDraw) \(link.letter!)")
                let augmDrawLetters = String((baseDraw + link.letter!).sorted())
                if let augmDraw = getDraw(drawLetters: augmDrawLetters) {
                    viewContext.perform {
                        let updateLink = link as NSManagedObject
                        updateLink.setValue(augmDraw, forKey: "gives")
                        erreur = " saving \(augmDraw.draw!) \(link.letter!)"
                        do {
                            try viewContext.save()
                        } catch {
                            print("Erreur mise à jour lien")
                        }
                    }
                }
            }
        } catch {
            print("Error " + erreur)
        }
    }

RESULT

And this is the output showing the content of the Draw entity with relationships after createLinks() is applied:

And here after updateLinks() is applied :

Answered by DTS Engineer in 825984022

The problem is triggered because Link.gives and Draw.minus are both a to-one relationships, and so when when you connect a draw to link A via linkA.gives and the draw was already connected to link B, linkB.gives will be nullified.

You can visualize the issue by adding the following debugging code to createLinks:

func createLinks() {
...         
        let previousLink = drawPlus.minus
        if previousLink != nil {
            print("previousLink.gives.draw = \(String(describing: previousLink!.gives?.draw))")
        }
        
        linkItem.gives = drawPlus
        
        if previousLink != nil {
            print("previousLink.gives.draw = \(String(describing: previousLink!.gives?.draw))")
        }
...
}

Running your app gets the following output, which shows that previousLink.gives becomes nil after linkItem.gives is set to a new value (drawPlus):

1916 - AAABISSS
previousLink.gives.draw = Optional("AAABIISSS")
previousLink.gives.draw = nil

This is an as-designed behavior – When you set a relationship, Core Data automatically sets the inverse relationship to maintain the data integrity.

To fix the issue, Link.gives or Draw.minus or both will need to be a to-many relationship. If that isn't appropriate, you will need a new model that fit your data.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Could you please provide a minimal project that contains only the code relevant to the issue, with detailed steps to reproduce the issue? Your post can contain a link to where your test project is hosted, and you can include your project (as a .zip file) directly in your post using the forums attachment feature.

I know your post already has a lot of information, but that doesn't cover all the details. For example, the linkTable variable in createLinks() doesn't have a defintion. Having a runnable project that folks can run and debug will be a great help for folks who would help you.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

The problem is triggered because Link.gives and Draw.minus are both a to-one relationships, and so when when you connect a draw to link A via linkA.gives and the draw was already connected to link B, linkB.gives will be nullified.

You can visualize the issue by adding the following debugging code to createLinks:

func createLinks() {
...         
        let previousLink = drawPlus.minus
        if previousLink != nil {
            print("previousLink.gives.draw = \(String(describing: previousLink!.gives?.draw))")
        }
        
        linkItem.gives = drawPlus
        
        if previousLink != nil {
            print("previousLink.gives.draw = \(String(describing: previousLink!.gives?.draw))")
        }
...
}

Running your app gets the following output, which shows that previousLink.gives becomes nil after linkItem.gives is set to a new value (drawPlus):

1916 - AAABISSS
previousLink.gives.draw = Optional("AAABIISSS")
previousLink.gives.draw = nil

This is an as-designed behavior – When you set a relationship, Core Data automatically sets the inverse relationship to maintain the data integrity.

To fix the issue, Link.gives or Draw.minus or both will need to be a to-many relationship. If that isn't appropriate, you will need a new model that fit your data.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Core database relationship are only partially updating.
 
 
Q