Wolf
Wolf

Reputation: 23

Realm: Partially update

I'm using realm in my Swift project and I'm having issues partially updating objects.

The problem is that I have an object that contains information from the server plus user generated informations. In my case it is a Topic that can be either visible or hidden by default, but the user can change the visibility value.

When I launch my app for the first time I call my server API to fetch the information to create a Topic object: it has as visibility value undefined. Then the user makes a choice and set the visibility value to visible.

The second time that I launch the application I fetch the info again from the server and I recreate the Topic. Then I call the Realm method add:update: to update the object but this updates even the visibility property to undefined again.

I know that there is another method create:value:update: but this means that I have to create a big dictionary with all the values I want to update. My model objects are not so small, in some cases I have a lot of properties and the dictionary would be huge. I don't like this approach, it is complicated to maintain.

Do you have any hint on how to handle a case like this?

A possibile way would be to create another Object (table) that has a relationship to Topic and one property visibility that is not overridden when I create the Topic again, but it sounds odd to create a table just for this thing.

Upvotes: 0

Views: 754

Answers (2)

apocolipse
apocolipse

Reputation: 617

I've encountered a similar problem with the way our service endpoint operates as well. It provides a full entity view, along with smaller views of properties on that entity which I'd like to update the model when they are hit. Example:

class User: Object {
  var id: Int
  var name: String
  var age: Int
  var average: Double
  likes: List<User>
}

One endpoint gets all 4 fields, another endpoint just gets average and id, and a 3rd only likes and id, however I'd like to create/update the model from any of them. Now, I could use create(value:update:) on the raw JSON thats returned, however my model maps different keynames and does other transforming things (to Dates and other things as well) which I want preserved, so out goes the ability to just call create(value:update:) with the JSON dict.

We got around this by leveraging Reflection and a protocol called Serializable, that looks something like this:

protocol Serializable {
  func toDictionary() -> [String: Any]
}

extension Serializable {
  func toDictionary() -> [String: Any] {
    var propertiesDictionary: [String: Any] = [:]
    let mirror = Mirror(reflecting: self)
    for (propName, propValue) in mirror.children {
      guard let propName = propName else { continue }
      // Attempt to unwrap the value as AnyObject
      if let propValue: AnyObject = self.unwrap(propValue) as AnyObject? {
        switch propValue {
        case let serializablePropValue as Serializable:
          propertiesDictionary[propName] = serializablePropValue.toDictionary()
        case let arrayPropValue as [Serializable]:
          propertiesDictionary[propName] = Array(arrayPropValue.flatMap { $0.toDictionary() })
        case let data as Data:
          propertiesDictionary[propName] = data.base64EncodedString(options: .lineLength64Characters)
        case _ as Bool: fallthrough
        case _ as Int: fallthrough
        case _ as Double: fallthrough
        case _ as Float: fallthrough
        default:
          propertiesDictionary[propName] = propValue
        }
      } else {
        // Couldn't treat as AnyObject, treat as Any
        switch propValue {
        case let arrayPropValue as [Serializable]:
          propertiesDictionary[propName] = arrayPropValue.flatMap { $0.toDictionary() }
        case let primative as Int8:   propertiesDictionary[propName] = primative
        case let primative as Int16:  propertiesDictionary[propName] = primative
        case let primative as Int32:  propertiesDictionary[propName] = primative
        case let primative as Int64:  propertiesDictionary[propName] = primative
        case let primative as UInt8:  propertiesDictionary[propName] = primative
        case let primative as UInt16: propertiesDictionary[propName] = primative
        case let primative as UInt32: propertiesDictionary[propName] = primative
        case let primative as UInt64: propertiesDictionary[propName] = primative
        case let primative as Float:  propertiesDictionary[propName] = primative
        case let primative as Double: propertiesDictionary[propName] = primative
        case let primative as Bool:   propertiesDictionary[propName] = primative
        case let primative as String: propertiesDictionary[propName] = primative
        case let primative as Date:   propertiesDictionary[propName] = primative
        case let primative as Data:   propertiesDictionary[propName] = primative
        default: break
        }
      }
    }
    return propertiesDictionary
  }
  /// Unwraps 'any' object.
  /// See http://stackoverflow.com/questions/27989094/how-to-unwrap-an-optional-value-from-any-type
  /// - parameter any: Any, Pretty clear what this is....
  /// - returns: The unwrapped object.
  private func unwrap(_ any: Any) -> Any? {
    let mi = Mirror(reflecting: any)
    guard let displayStyle = mi.displayStyle else { return any }
    switch displayStyle {
    case .optional:
      if mi.children.count == 0 {
        return nil
      }
      if let (_, some) = mi.children.first {
        return some
      } else {
        return nil
      }
    case .enum:
      let implicitTypes: [Any.Type] = [ImplicitlyUnwrappedOptional<Int>.self,
                                       ImplicitlyUnwrappedOptional<String>.self,
                                       ImplicitlyUnwrappedOptional<Double>.self,
                                       ImplicitlyUnwrappedOptional<Bool>.self,
                                       ImplicitlyUnwrappedOptional<Float>.self,
                                       ImplicitlyUnwrappedOptional<Date>.self]
      if implicitTypes.contains(where: { $0 == mi.subjectType }) {
        if mi.children.count == 0 { return nil }
        if let (_, some) = mi.children.first {
          return some
        } else {
          return nil
        }
      }
      return any
    default: return any
    }
}

With this you can serialize your partially filled object, and then pass with create(value: obj.toDictionary(), update: true)

Couple caveats to note: RealmOptional<T>, List<T>, and LinkingObjects<T> are not handled here, you can add cases for explicit primitive types for RealmOptional<T>, a skip case for LinkingObjectsBase, and a skip case with custom handling for ListBase (reflection /does not/ play nicely with Lists :/)

Upvotes: 1

TiM
TiM

Reputation: 16021

This probably would be easier solved if you were passing a dictionary to create:value:update: since you could simply omit the visibility property, but as you said for size constraints, you're using a new instance of the model object that has visibility already set to a different value by the server response, then this can't really be helped. Realm cannot tell that you wish to keep this particular property and change the rest.

If you're using a primary key, then the quickest fix to this would be to simply fetch the original object via its primary key beforehand, make a note of its visibility property, and then re-set it after you've performed the update.

Breaking the visibility property off into its own object would work, but you'd most likely run into the same issue with the new object instance setting that property to nil.

Upvotes: 0

Related Questions