swiftswiftdata

Does `modelContext.save()` make any sense when context saving is set to automatic?


Since one of conveniences that SwiftData offers, which is automatically saving of a record when something is added or changed in the model context, does explicit calling save() makes any difference? I guess in both cases, SwiftData will decide?

@ModelActor
public actor DataHandler {
    @discardableResult
    public func new(item: Item) throws -> PersistentIdentifier {
      modelContext.insert(item)
      try modelContext.save()
      return item.persistentModelID
    }
    
  @discardableResult
  public func newEmptyItem() throws -> PersistentIdentifier {
    let item = Item()
    modelContext.insert(item)
    try modelContext.save()
    return item.persistentModelID
  }

Can I remove this line:

try modelContext.save()

without any worries?


Solution

  • Can I remove this line without any worries?

    No. From some simple experiments, it can be observed that while save is called, it is not called synchronously with insert. It is called asynchronously, possibly in the next run loop cycle. This makes a lot of sense, because if you are calling insert in a loop, it is most likely good for performance, if only one save operation is performed for all those inserts.

    // suppose Foo is a @Model and ctx is a ModelContext with autosave enabled
    let foo = Foo(name: "foo")
    ctx.insert(foo)
    
    // this prints true, and a non-empty array, showing that the newly inserted model is not saved
    print(ctx.hasChanges, ctx.insertedModelsArray)
    
    // let's get the model's ID at this point and save it for later
    let id = foo.persistentModelID
    var retrievedFoo: Foo? = ctx.registeredModel(for: id)
    print(retrievedFoo) // this prints non-nil, as expected
    
    // wait for a while to let 'save' be automatically called
    try! await Task.sleep(for: .milliseconds(100))
    
    // this prints false, []
    // showing that save has indeed been called
    print(ctx.hasChanges, ctx.insertedModelsArray)
    
    retrievedFoo = ctx.registeredModel(for: id)
    print(retrievedFoo) // this prints nil, showing that the old ID no longer works
    

    As demonstrated above, if you remove the manual save call, the ID you return from new and newEmptyItem would be a temporary ID that will become invalid after a subsequent save call. So in this case, the manual save matters a lot! See also the documentation for insert:

    A model is given a temporary persistent identifier until the first time a context saves it, after which that context assigns a permanent identifier.