Entidades, propriedades e chaves

Os objetos de dados no Datastore são conhecidos como entidades. Uma entidade tem uma ou mais propriedades nomeadas, que podem ter um ou mais valores. Entidades do mesmo tipo não precisam ter as mesmas propriedades. Além disso, os valores de uma entidade para uma determinada propriedade não precisam ser todos do mesmo tipo de dados. Se necessário, um aplicativo pode estabelecer e aplicar essas restrições no próprio modelo de dados.

O Datastore é compatível com uma grande variedade de tipos de dados como valores de propriedade. Estes são alguns deles:

  • Números inteiros
  • Números de ponto flutuante
  • Strings
  • Datas
  • Dados binários

Para uma lista completa de tipos, consulte Propriedades e tipos de valor.

Cada entidade no Datastore tem uma chave que a identifica de maneira exclusiva. A chave tem os seguintes componentes:

  • O namespace da entidade, que possibilita a multilocação
  • O tipo da entidade, que a categoriza para as consultas do Datastore
  • Um identificador da entidade individual, que pode ser:
    • Uma string de nome da chave
    • um código numérico inteiro.
  • Um caminho ancestral opcional que localiza a entidade na hierarquia do Datastore.

Um aplicativo pode buscar uma entidade individual do Datastore usando a chave da entidade ou recuperar uma ou mais entidades emitindo uma consulta com base nas chaves ou nos valores de propriedade das entidades.

O SDK do App Engine para Java inclui uma API simples, fornecida no pacote com.google.appengine.api.datastore, que é diretamente compatível com os recursos do Datastore. Todos os exemplos neste documento são baseados nessa API de baixo nível. Você pode optar por usá-la diretamente no seu aplicativo ou como base para construir a própria camada de gerenciamento de dados.

O Datastore em si não aplica nenhuma restrição sobre a estrutura das entidades, como, por exemplo, se uma determinada propriedade tem um valor de um tipo específico. Essa tarefa é do aplicativo.

Tipos e identificadores

Cada entidade do Datastore é de um tipo específico, que a classifica para fins de consultas. Por exemplo, um aplicativo de recursos humanos pode representar cada funcionário de uma empresa com uma entidade do tipo Employee. Na API Java Datastore, você especifica o tipo da entidade quando a cria, como um argumento para o construtor Entity(). Por serem reservados, todos os nomes de tipo que começam com dois sublinhados (__) não podem ser usados.

O exemplo a seguir cria uma entidade do tipo Employee, preenche os valores da propriedade e a salva no Datastore:

Entity employee = new Entity("Employee", "asalieri");
employee.setProperty("firstName", "Antonio");
employee.setProperty("lastName", "Salieri");
employee.setProperty("hireDate", new Date());
employee.setProperty("attendedHrTraining", true);

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(employee);

Além de um tipo, cada entidade tem um identificador, que é atribuído quando ela é criada. Como ele é parte da chave da entidade, o identificador é associado permanentemente à entidade e não pode ser alterado. Ele pode ser atribuído de duas formas:

  • Seu aplicativo pode especificar a própria string de nome de chave para a entidade.
  • O Datastore atribui automaticamente à entidade um ID numérico inteiro.

Para atribuir um nome de chave a uma entidade, forneça o nome como o segundo argumento ao construtor quando criar a entidade:

Entity employee = new Entity("Employee", "asalieri");

Para que o Datastore atribua um ID numérico automaticamente, omita esse argumento:

Entity employee = new Entity("Employee");

Como atribuir identificadores

O Datastore pode ser configurado para gerar códigos automaticamente usando duas políticas de identificação automática diferentes:

  • A política default gera uma sequência aleatória de IDs não utilizados que são aproximadamente distribuídos de maneira uniforme. Cada ID pode ter até 16 dígitos decimais.
  • A política legacy cria uma sequência de IDs inteiros menores e não consecutivos.

Para exibir os IDs de entidade para o usuário e/ou depender da ordem deles, use a alocação manual.

O Datastore gera uma sequência aleatória de IDs não utilizados que são distribuídos de maneira quase uniforme. Cada ID pode ter até 16 dígitos decimais.

Caminhos ancestrais

As entidades do Cloud Datastore formam um espaço hierarquicamente estruturado, semelhante à estrutura de diretórios de um sistema de arquivos. Ao criar uma entidade, é possível designar outra entidade como mãe e a nova como filha. Ao contrário do que ocorre em um sistema de arquivos, a entidade mãe não precisa existir de verdade. Uma entidade sem mãe é uma entidade raiz. A associação entre uma entidade e a mãe é permanente e não pode ser alterada depois que a entidade é criada. O Cloud Datastore nunca atribuirá o mesmo ID numérico a duas entidades com a mesma mãe ou a duas entidades raiz (sem mãe).

A mãe de uma entidade, a mãe da mãe, e assim por diante são ancestrais dela. A filha, a filha da filha, e assim por diante são descendentes dela. Uma entidade raiz e todos os descendentes pertencem ao mesmo grupo de entidades. A sequência de entidades começando com uma entidade raiz e prosseguindo de pai para filho, levando a uma determinada entidade, constitui o caminho do ancestral dessa entidade. A chave completa que identifica a entidade consiste em uma sequência de pares de identificadores de tipo que especifica o caminho ancestral e termina com os da própria entidade:

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

Para uma entidade raiz, o caminho ancestral está vazio, e a chave consiste unicamente no próprio tipo e identificador da entidade:

[Person:GreatGrandpa]

Esse conceito é ilustrado pelo seguinte diagrama:

Mostra a relação entre a entidade raiz e as entidades filho no grupo de entidades

Para designar a mãe de uma entidade, informe a chave da entidade pai como argumento do construtor Entity() ao criar a entidade filho. Para receber a chave, chame o método getKey() da entidade pai:

Entity employee = new Entity("Employee");
datastore.put(employee);

Entity address = new Entity("Address", employee.getKey());
datastore.put(address);

Se a nova entidade também tiver um nome de chave, forneça esse nome como o segundo argumento para o construtor Entity() e a chave da entidade pai como o terceiro argumento:

Entity address = new Entity("Address", "addr1", employee.getKey());

Transações e grupos de entidades

Toda tentativa de criar, atualizar ou excluir uma entidade ocorre no contexto de uma transação. Uma única transação pode incluir qualquer número dessas operações. Para manter a consistência dos dados, a transação assegura que todas as operações que ela contém sejam aplicadas ao Datastore como uma unidade ou, se alguma das operações falhar, que nenhuma delas seja aplicada. Além disso, todas as leituras com consistência forte (consultas ou operações get de ancestral) executadas na mesma transação seguem um instantâneo consistente dos dados.

Como mencionado acima, um grupo de entidades é um conjunto de entidades conectadas por meio de um ancestral a um elemento raiz comum. A organização dos dados em grupos de entidades pode limitar quais transações podem ser realizadas:

  • Todos os dados acessados por uma transação devem estar contidos em, no máximo, 25 grupos de entidades.
  • Se você quiser usar consultas em uma transação, os dados precisam estar organizados em grupos de entidades para que você possa especificar filtros de ancestral que corresponderão aos dados corretos.
  • Há um limite de capacidade de gravação de aproximadamente uma transação por segundo em um único grupo de entidades. Essa limitação existe porque, para fornecer alta confiabilidade e tolerância a falhas, o Datastore executa a replicação síncrona sem mestre de cada grupo de entidades em uma ampla área geográfica.

Em muitos aplicativos, é aceitável usar a consistência eventual (ou seja, uma consulta que não seja de ancestral abrangendo vários grupos de entidades, que às vezes pode retornar dados um pouco desatualizados) para uma visualização ampla de dados não relacionados e depois usar a consistência forte (uma consulta de ancestral ou uma operação get para uma única entidade) para visualizar ou editar um único conjunto de dados altamente relacionados. Em tais aplicativos, geralmente é uma boa abordagem usar um grupo de entidades separado para cada conjunto de dados altamente relacionados. Para mais informações, consulte Como estruturar uma consistência forte.

Propriedades e tipos de valores

Os valores de dados associados a uma entidade consistem em uma ou mais propriedades. Cada propriedade tem um nome e um ou mais valores. Uma propriedade pode ter valores de mais de um tipo, e duas entidades podem ter valores de tipos diferentes para a mesma propriedade. As propriedades podem ser indexadas ou não (consultas que ordenam ou filtram com base em uma propriedade P ignoram entidades em que P não seja indexada). Uma entidade pode ter no máximo 20.000 propriedades indexadas.

Estes são os tipos de valor compatíveis:

Tipo de valor Tipo(s) de Java Ordem de classificação Observações
Número inteiro short
int
long
java.lang.Short
java.lang.Integer
java.lang.Long
Numérico Armazenado como número inteiro longo e convertido no tipo de campo

Excesso de valores fora do intervalo
Número de ponto flutuante float
double
java.lang.Float
java.lang.Double
Numérico Precisão dupla de 64 bits,
IEEE 754
Booleano boolean
java.lang.Boolean
false<true
String de texto (curta) java.lang.String Unicode Até 1.500 bytes

Valores maiores que 1.500 bytes geram IllegalArgumentException
String de texto (longa) com.google.appengine.api.datastore.Text Nenhum Até 1 megabyte

Não indexado
String de bytes (curta) com.google.appengine.api.datastore.ShortBlob Ordem de bytes Até 1.500 bytes

Valores maiores que 1.500 bytes geram IllegalArgumentException
String de bytes (longa) com.google.appengine.api.datastore.Blob Nenhum Até 1 megabyte

Não indexado
Data e hora java.util.Date Cronológica
Ponto geográfico com.google.appengine.api.datastore.GeoPt Por latitude,
e depois longitude
Endereço postal com.google.appengine.api.datastore.PostalAddress Unicode
Número de telefone com.google.appengine.api.datastore.PhoneNumber Unicode
Endereço de e-mail com.google.appengine.api.datastore.Email Unicode
Usuário das Contas do Google com.google.appengine.api.users.User Endereço de e-mail
em ordem Unicode
Identificador de mensagens instantâneas com.google.appengine.api.datastore.IMHandle Unicode
Link com.google.appengine.api.datastore.Link Unicode
Categoria com.google.appengine.api.datastore.Category Unicode
Classificação com.google.appengine.api.datastore.Rating Numérico
Chave do Datastore com.google.appengine.api.datastore.Key
ou o objeto referenciado (como um filho)
Por elementos do caminho
(tipo, identificador,
tipo, identificador...)
Até 1.500 bytes

Valores maiores que 1.500 bytes geram IllegalArgumentException
Chave Blobstore com.google.appengine.api.blobstore.BlobKey Ordem de bytes
Entidade incorporada com.google.appengine.api.datastore.EmbeddedEntity Nenhum Não indexado
Null null Nenhum

Importante: é altamente recomendável evitar o armazenamento de um users.User como um valor de propriedade, porque isso inclui o endereço de e-mail e o ID exclusivo. Se um usuário alterar o endereço de e-mail, e você comparar o antigo user.User armazenado com o novo valor de user.User, eles não serão correspondentes. Em vez disso, utilize o valor do ID do usuário User como o identificador estável e exclusivo do usuário.

Para strings de texto e dados binários não codificados (strings de bytes), o Datastore aceita dois tipos de valor:

  • Strings curtas (até 1500 bytes) são indexadas e podem ser usadas em condições de filtro de consulta e ordens de classificação.
  • Strings longas (até 1 megabyte) não são indexadas e não podem ser usadas em filtros de consulta e ordens de classificação.
Observação: o tipo de string de bytes longa é nomeado como Blob na API Datastore. Esse tipo não está relacionado aos blobs, conforme usado na API Blobstore.

Quando uma consulta envolve um campo com valores de tipos mistos, o Firestore usa uma ordem determinista com base nas representações internas:

  1. Valores nulos
  2. Números de ponto fixo
    • Números inteiros
    • Datas e horas
    • Classificações
  3. Valores booleanos
  4. Sequências de bytes
    • String de bytes
    • String Unicode
    • Chaves do Blobstore
  5. Números de ponto flutuante
  6. Pontos geográficos
  7. Usuários das Contas do Google
  8. Chaves do armazenamento de dados

Como strings de texto longas, strings de bytes longas e entidades incorporadas não são indexadas, elas não têm nenhuma ordem definida.

Trabalhar com entidades

Os aplicativos podem usar a API Datastore para criar, recuperar, atualizar e excluir entidades. Se o aplicativo souber a chave completa de uma entidade ou puder derivá-la da própria chave pai, tipo e identificador, ele pode usá-la para operar diretamente na entidade. O aplicativo também pode conseguir a chave de uma entidade por meio de uma consulta do Datastore. Para mais informações, consulte a página Consultas do Datastore.

A API Java Datastore usa métodos da interface DatastoreService para operar em entidades. Para conseguir um objeto DatastoreService, chame o método estático DatastoreServiceFactory.getDatastoreService():

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Como criar uma entidade

É possível criar uma nova entidade construindo uma instância da classe Entity, fornecendo o tipo da entidade como um argumento para o construtor Entity().

Depois de preencher as propriedades da entidade, se necessário, salve-a no armazenamento de dados transferindo-a como um argumento para o método DatastoreService.put(). Você pode especificar o nome da chave da entidade passando-o como o segundo argumento para o construtor.

Entity employee = new Entity("Employee", "asalieri");
// Set the entity properties.
// ...
datastore.put(employee);

Se você não informar um nome de chave, o Datastore gerará automaticamente um ID numérico para a chave da entidade:

Entity employee = new Entity("Employee");
// Set the entity properties.
// ...
datastore.put(employee);

Recuperar uma entidade

Para recuperar uma entidade identificada por determinada chave, transmita o objeto Key para o método DatastoreService.get():

// Key employeeKey = ...;
Entity employee = datastore.get(employeeKey);

Atualizar uma entidade

Para atualizar uma entidade atual, modifique os atributos do objeto Entity e, em seguida, transmita-o para o método DatastoreService.put(). Os dados do objeto sobrescrevem a entidade atual. O objeto inteiro é enviado ao Datastore com todas as chamadas para put().

Excluir uma entidade

Com a chave de uma entidade, é possível excluir a entidade usando o método DatastoreService.delete():

// Key employeeKey = ...;
datastore.delete(employeeKey);

Propriedades repetidas

É possível armazenar diversos valores dentro de uma única propriedade.

Entity employee = new Entity("Employee");
ArrayList<String> favoriteFruit = new ArrayList<String>();
favoriteFruit.add("Pear");
favoriteFruit.add("Apple");
employee.setProperty("favoriteFruit", favoriteFruit);
datastore.put(employee);

// Sometime later
employee = datastore.get(employee.getKey());
@SuppressWarnings("unchecked") // Cast can't verify generic type.
    ArrayList<String> retrievedFruits = (ArrayList<String>) employee
    .getProperty("favoriteFruit");

Entidades incorporadas

Às vezes, pode ser conveniente incorporar uma entidade como propriedade de outra entidade. Isso pode ser útil, por exemplo, para criar uma estrutura hierárquica de valores de propriedade dentro de uma entidade. A classe Java EmbeddedEntity permite que você realize as ações a seguir:

// Entity employee = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678");
embeddedContactInfo.setProperty("phoneNumber", "555-555-5555");
embeddedContactInfo.setProperty("emailAddress", "test@example.com");

employee.setProperty("contactInfo", embeddedContactInfo);

Quando uma entidade incorporada está incluída nos índices, é possível consultar subpropriedades. Se você excluir da indexação uma entidade incorporada, todas as subpropriedades também serão excluídas da indexação. Você também pode associar uma chave a uma entidade incorporada, mas, ao contrário de uma entidade completa, a chave não é necessária e, mesmo presente, não pode ser usada para recuperar a entidade.

Em vez de preencher manualmente as propriedades da entidade incorporada, use o método setPropertiesFrom() para copiá-las de uma entidade atual:

// Entity employee = ...;
// Entity contactInfo = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setKey(contactInfo.getKey()); // Optional, used so we can recover original.
embeddedContactInfo.setPropertiesFrom(contactInfo);

employee.setProperty("contactInfo", embeddedContactInfo);

Você pode usar o mesmo método depois para recuperar a entidade original da entidade incorporada:

Entity employee = datastore.get(employeeKey);
EmbeddedEntity embeddedContactInfo = (EmbeddedEntity) employee.getProperty("contactInfo");

Key infoKey = embeddedContactInfo.getKey();
Entity contactInfo = new Entity(infoKey);
contactInfo.setPropertiesFrom(embeddedContactInfo);

Operações em lote

Os métodos DatastoreService put(), get() e delete() (e seus correspondentes AsyncDatastoreService) têm versões de lote que aceitam um objeto iterável (da classe Entity para put(), Key para get() e delete()) e o usam para operar em várias entidades em uma única chamada do Datastore:

Entity employee1 = new Entity("Employee");
Entity employee2 = new Entity("Employee");
Entity employee3 = new Entity("Employee");
// ...

List<Entity> employees = Arrays.asList(employee1, employee2, employee3);
datastore.put(employees);

Essas operações em lote agrupam todas as entidades ou chaves por grupo de entidades e, em seguida, executam a operação solicitada em cada grupo de entidades em paralelo. Essas chamadas em lote são mais rápidas do que fazer chamadas separadas para cada entidade porque elas geram sobrecarga para apenas uma chamada de serviço. Se vários grupos de entidades estiverem envolvidos, o trabalho para todos os grupos é executado em paralelo no servidor.

Como gerar chaves

Os aplicativos podem usar a classe KeyFactory para criar um objeto Key para uma entidade a partir de componentes conhecidos, como o tipo e o identificador da entidade. No caso de uma entidade sem pai, transmita o tipo e o identificador (que podem ser uma string de nome de chave ou um ID numérico) ao método estático KeyFactory.createKey() para criar a chave. Os exemplos a seguir criam uma chave para uma entidade do tipo Person com o nome de chave "GreatGrandpa" ou o ID numérico 74219:

Key k1 = KeyFactory.createKey("Person", "GreatGrandpa");
Key k2 = KeyFactory.createKey("Person", 74219);

Se a chave incluir um componente de caminho, use a classe auxiliar KeyFactory.Builder para criar o caminho. Com o método addChild dessa classe, uma única entidade é adicionada ao caminho e o próprio criador é retornado. Assim, é possível encadear uma série de chamadas, começando com a entidade raiz, para criar o caminho com uma entidade por vez. Depois de criar o caminho completo, chame getKey para recuperar a chave resultante:

Key k =
    new KeyFactory.Builder("Person", "GreatGrandpa")
        .addChild("Person", "Grandpa")
        .addChild("Person", "Dad")
        .addChild("Person", "Me")
        .getKey();

A classe KeyFactory também inclui os métodos estáticos keyToString e stringToKey para a conversão entre chaves e as respectivas representações de string:

String personKeyStr = KeyFactory.keyToString(k);

// Some time later (for example, after using personKeyStr in a link).
Key personKey = KeyFactory.stringToKey(personKeyStr);
Entity person = datastore.get(personKey);

A representação de string de uma chave é "segura para Web": não contém caracteres considerados especiais em HTML nem em URLs.

Como usar uma lista vazia

Historicamente, o Datastore não tinha uma representação para uma propriedade que representasse uma lista vazia. O SDK do Java contornou isso armazenando coleções vazias como valores nulos. Portanto, não há como fazer a distinção entre valores nulos e listas vazias. Para manter a compatibilidade com versões anteriores, isso continua sendo o comportamento padrão, resumido da seguinte maneira:

  • As propriedades nulas são gravadas como nulas no Datastore.
  • As coleções vazias são gravadas como nulas no Datastore.
  • Um nulo é lido como nulo pelo Datastore.
  • Uma coleção vazia é lida como nula.

No entanto, se você alterar o comportamento padrão, o Java SDK aceitará o armazenamento de listas vazias. Recomendamos que considere as implicações de alterar o comportamento padrão do aplicativo e ativar o suporte para listas vazias.

Para alterar o comportamento padrão para usar listas vazias, defina a propriedade DATASTORE_EMPTY_LIST_SUPPORT durante a inicialização do app conforme a seguir:

System.setProperty(DatastoreServiceConfig.DATASTORE_EMPTY_LIST_SUPPORT, Boolean.TRUE.toString());

Com esta propriedade definida como true, como mostrado acima:

  • As propriedades nulas são gravadas como nulas no Datastore.
  • As coleções vazias são gravadas como uma lista vazia no Datastore
  • Um nulo é lido como nulo pelo Datastore.
  • Durante a leitura pelo Datastore, uma lista vazia é retornada como uma Collection vazia.