Como sincronizar CoreData e um serviço da Web REST de forma assíncrona e, ao mesmo tempo, propagar corretamente quaisquer erros REST na IU

85

Ei, estou trabalhando na camada do modelo para nosso aplicativo aqui.

Alguns dos requisitos são assim:

  1. Deve funcionar no iPhone OS 3.0+.
  2. A fonte de nossos dados é um aplicativo RESTful Rails.
  3. Devemos armazenar os dados em cache localmente usando Core Data.
  4. O código do cliente (nossos controladores de UI) deve ter o mínimo de conhecimento possível sobre qualquer coisa de rede e deve consultar / atualizar o modelo com a API Core Data.

Verifiquei a Sessão 117 do WWDC10 sobre como criar uma experiência do usuário orientada para o servidor, passei algum tempo verificando o recurso objetivo , o recurso principal e frameworks RestfulCoreData .

A estrutura do Objective Resource não se comunica com os Core Data por conta própria e é apenas uma implementação de cliente REST. O Core Resource e RestfulCoreData presumem que você fala com Core Data em seu código e eles resolvem todas as porcas e parafusos no plano de fundo na camada do modelo.

Tudo parece bem até agora e, inicialmente, pensei que o Core Resource ou o RestfulCoreData cobririam todos os requisitos acima, mas ... Há algumas coisas que nenhum deles aparentemente resolveu corretamente:

  1. O thread principal não deve ser bloqueado ao salvar atualizações locais no servidor.
  2. Se a operação de salvamento falhar, o erro deve ser propagado para a IU e nenhuma alteração deve ser salva no armazenamento local de Core Data.

Recurso central passa a emitir todos os seus pedidos para o servidor quando você chama - (BOOL)save:(NSError **)error seu Contexto de Objeto Gerenciado e, portanto, é capaz de fornecer uma instância NSError correta das solicitações subjacentes para o servidor falhar de alguma forma. Mas ele bloqueia o thread de chamada até que a operação de salvamento seja concluída. FALHOU.

RestfulCoreData mantém suas -save:chamadas intactas e não introduz nenhum tempo de espera adicional para o encadeamento do cliente. Ele simplesmente observa NSManagedObjectContextDidSaveNotificatione emite as solicitações correspondentes ao servidor no manipulador de notificação. Mas desta forma a -save:chamada sempre é concluída com êxito (bem, dada Core Data é aprovado com as alterações salvas) e o código do cliente que realmente chamado não tem nenhuma maneira de saber o salvamento pode ter falhado para propagar para o servidor por causa de algum 404ou 421ou qualquer outra coisa Ocorreu um erro do lado do servidor. E mais ainda, o armazenamento local passa a ter os dados atualizados, mas o servidor nunca fica sabendo das mudanças. FALHOU.

Portanto, estou procurando uma solução possível / práticas comuns para lidar com todos esses problemas:

  1. Eu não quero que o thread de chamada seja bloqueado em cada -save: chamada enquanto as solicitações de rede acontecem.
  2. Quero, de alguma forma, receber notificações na IU de que alguma operação de sincronização deu errado.
  3. Quero que o salvamento do Core Data real também falhe se as solicitações do servidor falharem.

Alguma ideia?

Eploko
fonte
1
Uau, você não tem ideia de quantos problemas você me salvou fazendo esta pergunta. Atualmente, implementei meu aplicativo para fazer o usuário esperar pelos dados cada vez que faço uma chamada (embora para um serviço da web .NET). Tenho pensado em uma maneira de torná-lo assíncrono, mas não consegui descobrir como. Obrigado por todos os recursos que você forneceu!
Tejaswi Yerukalapudi
Excelente pergunta, obrigado.
justin
O link para o Core Resource está quebrado. Alguém sabe onde ele está hospedado agora?
O Core Resource ainda está hospedado no GitHub aqui: github.com/mikelaurence/CoreResource
eploko
E o site original também pode ser encontrado no gitHub: github.com/mikelaurence/coreresource.org
eploko

Respostas:

26

Você realmente deve dar uma olhada no RestKit ( http://restkit.org ) para este caso de uso. Ele é projetado para resolver os problemas de modelagem e sincronização de recursos JSON remotos para um cache local baseado em Core Data. Ele suporta um modo offline para trabalhar inteiramente a partir do cache quando não houver rede disponível. Toda a sincronização ocorre em um thread de segundo plano (acesso à rede, análise de carga útil e mesclagem de contexto de objeto gerenciado) e há um rico conjunto de métodos delegados para que você possa saber o que está acontecendo.

Blake Watters
fonte
18

Existem três componentes básicos:

  1. A ação da IU e a persistência da mudança no CoreData
  2. Persistir essa mudança até o servidor
  3. Atualizando a IU com a resposta do servidor

Um NSOperation + NSOperationQueue ajudará a manter os pedidos de rede em ordem. Um protocolo delegado ajudará suas classes de IU a entender em que estado as solicitações de rede estão, algo como:

@protocol NetworkOperationDelegate
  - (void)operation:(NSOperation *)op willSendRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity;
  - (void)operation:(NSOperation *)op didSuccessfullySendRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity;
  - (void)operation:(NSOperation *)op encounteredAnError:(NSError *)error afterSendingRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity;
@end

O formato do protocolo certamente dependerá do seu caso de uso específico, mas essencialmente o que você está criando é um mecanismo pelo qual as alterações podem ser "enviadas" para o seu servidor.

Em seguida, há o loop de IU a ser considerado; para manter seu código limpo, seria bom chamar save: e ter as alterações enviadas automaticamente para o servidor. Você pode usar notificações NSManagedObjectContextDidSave para isso.

- (void)managedObjectContextDidSave:(NSNotification *)saveNotification {
  NSArray *inserted = [[saveNotification userInfo] valueForKey:NSInsertedObjects];
  for (NSManagedObject *obj in inserted) {
    //create a new NSOperation for this entity which will invoke the appropraite rest api
    //add to operation queue
  }

  //do the same thing for deleted and updated objects
}

A sobrecarga computacional para inserir as operações de rede deve ser bastante baixa, no entanto, se isso criar um atraso perceptível na IU, você poderia simplesmente pegar os IDs de entidade da notificação de salvamento e criar as operações em um thread de segundo plano.

Se a sua API REST oferecer suporte ao envio em lote, você poderá até mesmo enviar o array inteiro de uma vez e notificar sua UI de que várias entidades foram sincronizadas.

O único problema que prevejo, e para o qual não existe uma solução "real", é que o usuário não desejará esperar que suas alterações sejam enviadas ao servidor para poder fazer mais alterações. O único bom paradigma que descobri é que você permite que o usuário continue editando objetos e agrupe suas edições quando apropriado, ou seja, você não pressiona todas as notificações de salvamento.

ImHuntingWabbits
fonte
2

Isso se torna um problema de sincronização e não fácil de resolver. Aqui está o que eu faria: Na IU do seu iPhone, use um contexto e, em seguida, usando outro contexto (e outro thread), baixe os dados do seu serviço web. Assim que estiver tudo pronto, siga os processos de sincronização / importação recomendados abaixo e atualize sua IU depois que tudo tiver sido importado corretamente. Se as coisas derem errado ao acessar a rede, apenas reverta as alterações no contexto não da IU. É um monte de trabalho, mas acho que é a melhor maneira de abordar isso.

Dados principais: importando dados com eficiência

Dados Principais: Gestão da Mudança

Dados principais: Multi-Threading com dados principais

David Weiss
fonte
0

Você precisa de uma função de retorno de chamada que será executada em outro encadeamento (aquele em que ocorre a interação real do servidor) e, em seguida, coloque o código de resultado / informações de erro em dados semi-globais que serão periodicamente verificados pelo encadeamento de interface do usuário. Certifique-se de que o wirting do número que serve como sinalizador é atômico ou você terá uma condição de corrida - digamos que se sua resposta de erro for de 32 bytes, você precisa de um int (que deve ter acesso atômico) e então mantenha esse int no estado desligado / falso / não pronto até que seu bloco de dados maior tenha sido escrito e só então escreva "verdadeiro" para virar a chave, por assim dizer.

Para o salvamento correlacionado no lado do cliente, você deve apenas manter os dados e não salvá-los até obter o OK do servidor ou certificar-se de que possui uma opção de reversão - digamos, uma maneira de excluir o servidor falhou.

Esteja ciente de que nunca será 100% seguro a menos que você execute o procedimento de confirmação de 2 fases completo (o salvamento ou exclusão do cliente pode falhar após o sinal do servidor do servidor), mas isso vai custar a você 2 viagens para o servidor, no mínimo ( pode custar 4 se sua única opção de reversão for excluir).

Idealmente, você faria toda a versão de bloqueio da operação em um thread separado, mas você precisaria do 4.0 para isso.

ZXX
fonte