Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Difference between Context.root and Context.main and why should we use Context.main as a default context #8

Open
naoyamakino opened this issue Nov 27, 2012 · 6 comments

Comments

@naoyamakino
Copy link
Contributor

Hi @alloy and @jonathanpenn, I am having a trouble understanding an use of different contexts and how to make a persistent save. By default it is using Context.main to do initialize an Entity and used as saving. but in the example Recipe, this way it prevents to save in a persistent data storage.

for example,

(main)> Recipe.new(:name => "this is from main")
=> <Recipe: 0xa181090> (entity: Recipe; id: 0xa17e720 <x-coredata:///Recipe/tEF6D2C4C-25D5-46BF-90FB-B2F3733FDC347> ; data: {
    image = nil;
    ingredients =     (
    );
    instructions = nil;
    name = "this is from main";
    overview = nil;
    prepTime = nil;
    thumbnailImage = nil;
    type = nil;
})
(nil)? MotionData::Context.main.save(Pointer.new(:object))
=> true
(main)> Recipe.all.to_a.count
=> 2
(main)> exit
#re open the app
(main)> Recipe.all.to_a.count
=> 1

but when I use Context.root

(main)> context = MotionData::Context.root
=> #<MotionData::Context:0x956eba0>
(nil)? recipe = Recipe.newInContext(context)
=> <Recipe: 0xa94e590> (entity: Recipe; id: 0xa94e5d0 <x-coredata:///Recipe/tD58E5327-A318-4A42-83B3-74BFAEF73CA67> ; data: {
    image = nil;
    ingredients =     (
    );
    instructions = nil;
    name = nil;
    overview = nil;
    prepTime = nil;
    thumbnailImage = nil;
    type = nil;
})
(main)> recipe.name = "this is from root"
=> "this is from root"
(main)> context.save(Pointer.new(:object))
=> true
(main)> Recipe.all.to_a.count
=> 2
(main)> exit
#exit an app and reopen it
(main)> Recipe.all.to_a.count
=> 2

By default, I would expect .new to create an in an context that can be saved in persistent storage, thus Context.root not Context.main or Context.current

so should new be like this?

#from lib/motion_data/managed_object.rb#28
      def new(properties = nil)
        newInContext(Context.root, properties)
      end

I might be misunderstanding the use of Context.root and Context.main, in fact I don't know the difference.

so a question is, should it be using Context.root instead of Context.main at ManagedObject#new, and if it needs to use Context.main, then how can you do saving in a persistent way?

thanks for your comment.

@jetpad
Copy link
Contributor

jetpad commented Nov 27, 2012

My understanding of the Context.root, Context.main and Context.context is that you should just use

context = Context.context

and then do

Context.withCurrent(context) do
...
end

and within that block do any .new of objects and it'll use the context you created. Another added benefit of doing it that way is that it'll create the right context depending on if you are in a background thread(or queue) or if you are on the main thread. Right before the end of the block do

Context.current.saveChanges

And that'll save the objects into Context.main

Next, do

Context.root.saveChanges

to persist the objects to the database.

BUT!!!!!!!, if your code is in a background thread (or queue) you should do something like

 if (NSThread.mainThread? == false)
    Dispatch::Queue.main.sync do 
      Context.main.saveChanges
      Context.root.saveChanges
    end # do
  end # if   

when you are saving. Bad things happen if you access a context from a thread that it wasn't created on. Only use the main and root contexts from the main thread.

Oh, and here is the context save method that I actually use.

def saveChanges
  error_ptr = Pointer.new(:object)
  unless save(error_ptr) 
    error = error_ptr[0]
    puts "Error when saving data: #{error.localizedDescription}"
    if !error.userInfo['NSDetailedErrors'].nil?
      error.userInfo['NSDetailedErrors'].each do |key, value|
        puts "#{key}: #{value}"
      end
    end 
    raise "Error when saving data: #{error.localizedDescription}"
  end
 end

@jonathanpenn
Copy link
Collaborator

Thanks for helping out with this, David! Just to let you know, that in this simple case, you don't need to create a new context and use withCurrent. It won't hurt if you do, but all you're doing is building a new context and setting it as the current for the thread. In the original example, he was using the main context which was the current context. It's equivalent.

So, Naoya, here's what's happening. The problem comes down to saving in child and parent contexts. In Core Data you can set up contexts to be children of other contexts, and if you save in a child context, the changes get "bubbled up" to the parent context. However, the parent context hasn't persisted them to disk yet. You have to save the top parent context to actually persist the changes. This one-way relationship lets you alter the objects in memory in child contexts but then throw them away if you decide to cancel the operation. "Saving" a child context is all about bubbling changes up to the parent context.

In MotionData, the root context is being set up as the main context to rule them all for the application. This is a pattern similar to what happens in the MagicalRecord library, and I think that's where Eloy got his inspiration for this. The idea is that you have a single context that everything else reports to. The main context is a child of root. I didn't build this part of the project, but I'm pretty sure that's what Eloy was going for. He wanted all managed objects created by default to come from the main context.

So, in your example, saving the main context bubbles the changes up to the root in memory. That's why you can see the new recipe when you use all.to_a. But it hasn't been persisted to disk yet. That's where the second half of David's answer comes in. You have to do the Context.root.save(nil) to put the changes on disk.

As I was investigating this, it turns out the Recipe demo app is broken because it doesn't properly save the parent contexts. When Eloy and I were last working on this, we weren't using the Recipe demo fully. We were just testing that defining a schema in Ruby code instead of a managed object definition file would work. Our test succeeded because the Recipe app boots up fine, but our testing didn't go farther beyond that yet.

Let me know if you need more clarification on this.

On Nov 27, 2012, at 9:22 AM, David Smith [email protected] wrote:

My understanding of the Context.root, Context.main and Context.context is that you should just use

context = Context.context
and then do

Context.withCurrent(context) do
...
end
and within that block do any .new of objects and it'll use the context you created. Another added benefit of doing it that way is that it'll create the right context depending on if you are in a background thread(or queue) or if you are on the main thread. Right before the end of the block do

Context.current.saveChanges
And that'll save the objects into Context.main

Next, do

Context.root.saveChanges
to persist the objects to the database.

BUT!!!!!!!, if your code is in a background thread (or queue) you should do something like

if (NSThread.mainThread? == false)
Dispatch::Queue.main.sync do
Context.main.saveChanges
Context.root.saveChanges
end # do
end # if
when you are saving. Bad things happen if you access a context from a thread that it wasn't created on. Only use the main and root contexts from the main thread.

Oh, and here is the context save method that I actually use.

def saveChanges
error_ptr = Pointer.new(:object)
unless save(error_ptr)
error = error_ptr[0]
puts "Error when saving data: #{error.localizedDescription}"
if !error.userInfo['NSDetailedErrors'].nil?
error.userInfo['NSDetailedErrors'].each do |key, value|
puts "#{key}: #{value}"
end
end
raise "Error when saving data: #{error.localizedDescription}"
end
end

Reply to this email directly or view it on GitHub.

@naoyamakino
Copy link
Contributor Author

@jonathanpenn @jetpad thanks for the comments, much appreciate it.
I made a change to MotionData::Context and Recipe app.
#9
please let me know if you are happy with this change.

@jonathanpenn
Copy link
Collaborator

Merged it in and left a comment. Thanks!

On Nov 27, 2012, at 1:41 PM, Naoya Makino [email protected] wrote:

@jonathanpenn @jetpad thanks for the comments, much appreciate it.
I made a change to MotionData::Context and Recipe app.
#9
please let me know if you are happy with this change.


Reply to this email directly or view it on GitHub.

@alloy
Copy link
Owner

alloy commented Nov 28, 2012

In MotionData, the root context is being set up as the main context to rule them all for the application. This is a pattern similar to what happens in the MagicalRecord library, and I think that's where Eloy got his inspiration for this. The idea is that you have a single context that everything else reports to. The main context is a child of root. I didn't build this part of the project, but I'm pretty sure that's what Eloy was going for. He wanted all managed objects created by default to come from the main context.

Indeed, I got the inspiration from MagicalRecord and from WWDC talks. The general idea is this:

  • The ‘main’ context is the default context for the main thread (initialized with NSMainQueueConcurrencyType) and the one that's generally used to provide data to controllers/views.
  • You want changes (made in the background) to show in the UI as soon as possible, therefor the contexts returned from Context.context are children of the ‘main’ context, so that saving these contexts will merge the data into the ‘main’ context ASAP. (In-memory merges are fast.)
  • Finally, you want to persist the data to disk without stalling the main thread. This is where the ‘root’ context comes in, it’s only purpose is to actually save the merged changes in the ‘main’ context to disk.

@alloy
Copy link
Owner

alloy commented Dec 7, 2012

PS: We should add something like MagicalRecord’s Context#saveNestedContexts method which will go through all the parent contexts and save the changes to the persistent store.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants