iosswiftrealmrealm-list

Avoid duplication in array of custom object in realm database. When Primary key is exist in the relational table only


how to avoid duplications in array of custom objects in realm Database, Below is my code and related JSON Please correct me if I do something wrong in the model.

JSON ->

{
  "playlists": [
    {
      "id": "1f23bd3e-cc01-11e8-b25e-784f435e4a9a",
      "name": "disney nostalgia",
      "duration": 361,
    },
    {
      "id": "2e1f0e02-cc05-11e8-9efe-784f435e4a9a",
      "name": "songs from aladdin",
      "duration": 331,
    }
  ],
    "tracks": [
        {
          "id": "3e986a2a-cc01-11e8-bb04-784f435e4a9a",
          "name": "I'll Make a Man Out of You",
          "artist": "Donny Osmond & Chorus"
        },
        {
          "id": "aff8bcee-cc04-11e8-8c18-784f435e4a9a",
          "name": "A Whole New World",
          "artist": "Lea Salonga, Brad Kane"
        }
      ]
}

and Models are as follow

class Songs: Object, Codable {
    let playlists = List<Playlists>()
    let tracks = List<Tracks>()
    enum CodingKeys: String, CodingKey {
        case playlists
        case tracks
    }

    required convenience public init(from decoder: Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let playLists = try container.decodeIfPresent([Playlists].self, forKey: .playlists){
            playLists.forEach({self.playlists.append($0)})
        }
        if let tracksList = try container.decodeIfPresent([Tracks].self, forKey: .tracks){
            tracksList.forEach({self.tracks.append($0)})
        }
    }

    func encode(to encoder: Encoder) throws {
        //
    }
}

class Playlists: Object, Codable {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    @objc dynamic var duration: Int = 0


    enum CodingKeys: String, CodingKey {
        case id
        case name
        case duration
    }

    override static func primaryKey() -> String? {
        return "id"
    }

    required convenience public init(from decoder: Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.name = try container.decode(String.self, forKey: .name)
        self.duration = try container.decode(Int.self, forKey: .duration)
    }
}

class Tracks: Object, Codable {
    @objc dynamic var id: String = ""
    @objc dynamic var name: String = ""
    @objc dynamic var artist: Int = 0


    enum CodingKeys: String, CodingKey {
        case id
        case name
        case artist
    }

    override static func primaryKey() -> String? {
        return "id"
    }

    required convenience public init(from decoder: Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.name = try container.decode(String.self, forKey: .name)
        self.artist = try container.decode(Int.self, forKey: .artist)
    }
}

and this is how I save the data.

SongsData = try jsonDecoder.decode(Songs.self, from: data)
   let realm = try! Realm()
   try! realm.write {
          realm.add(SongsData)
    } catch {
          Logger.log.printOnConsole(string: "Unable to convert to data")
    }

How to avoid duplications into the data when there is same response from server.


Solution

  • You can just use Set to get rid of the duplicates before adding your objects to a List. Just make sure you make your types conform to Hashable that you want to add to a Set.

    Some general advice: you don't need to create CodingKeys when the property names match the JSON keys unless you create a custom init(from decoder:) method and you don't need to create a custom init(from:) method unless you do some custom stuff, like use decodeIfPresent and filter duplicate objects. For Playlists and Tracks, you can rely on the synthetised initializer.

    You also don't need to add elements from an array to a List in a loop, just use append(objectsIn:), which accepts a Sequence as its input argument.

    class Songs: Object, Decodable {
        let playlists = List<Playlists>()
        let tracks = List<Tracks>()
    
        enum CodingKeys: String, CodingKey {
            case playlists, tracks
        }
    
        required convenience public init(from decoder: Decoder) throws {
            self.init()
            let container = try decoder.container(keyedBy: CodingKeys.self)
            if let playLists = try container.decodeIfPresent([Playlists].self, forKey: .playlists){
                let uniquePlaylists = Set(playLists)
                self.playlists.append(objectsIn: uniquePlaylists)
            }
    
            if let tracksList = try container.decodeIfPresent([Tracks].self, forKey: .tracks){
                let uniqueTrackList = Set(tracksList)
                self.tracks.append(objectsIn: uniqueTrackList)
            }
        }
    }
    
    class Playlists: Object, Codable, Hashable {
        @objc dynamic var id: String = ""
        @objc dynamic var name: String = ""
        @objc dynamic var duration: Int = 0
    
        override static func primaryKey() -> String? {
            return "id"
        }
    
    }
    
    class Tracks: Object, Codable, Hashable {
        @objc dynamic var id: String = ""
        @objc dynamic var name: String = ""
        @objc dynamic var artist: Int = 0
    
        override static func primaryKey() -> String? {
            return "id"
        }
    }
    

    If you want to make sure you don't add any objects twice to Realm, you need to use add(_:,update:) instead of add and use the primaryKey to avoid adding elements with the same keys.

    SongsData = try jsonDecoder.decode(Songs.self, from: data)
    let realm = try! Realm()
    try! realm.write {
          realm.add(SongsData, update: true)
    } catch {
          Logger.log.printOnConsole(string: "Unable to convert to data")
    }