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 Go inclui um pacote para representar entidades do Datastore como estruturas do Go e para armazenar e recuperar essas entidades no Datastore.

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 Datastore para Go, você especifica o tipo de entidade ao criar um datastore.Key. 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:

import (
	"context"
	"time"

	"google.golang.org/appengine/datastore"
)

type Employee struct {
	FirstName          string
	LastName           string
	HireDate           time.Time
	AttendedHRTraining bool
}

func f(ctx context.Context) {
	// ...
	employee := &Employee{
		FirstName: "Antonio",
		LastName:  "Salieri",
		HireDate:  time.Now(),
	}
	employee.AttendedHRTraining = true

	key := datastore.NewIncompleteKey(ctx, "Employee", nil)
	if _, err := datastore.Put(ctx, key, employee); err != nil {
		// Handle err
	}
	// ...
}

O tipo Employee declara quatro campos para o modelo de dados: FirstName, LastName, HireDate e AttendedHRTraining.

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 um argumento stringID não vazio para datastore.NewKey:

// Create a key with a key name "asalieri".
key := datastore.NewKey(
	ctx,        // context.Context
	"Employee", // Kind
	"asalieri", // String ID; empty means no string ID
	0,          // Integer ID; if 0, generate automatically. Ignored if string ID specified.
	nil,        // Parent Key; nil means no parent
)

Para permitir que o Datastore atribua um ID numérico automaticamente, use um argumento stringID vazio:

// Create a key such as Employee:8261.
key := datastore.NewKey(ctx, "Employee", "", 0, nil)
// This is equivalent:
key = datastore.NewIncompleteKey(ctx, "Employee", nil)

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 o pai de uma entidade, use o argumento parent para datastore.NewKey. O valor desse argumento precisa ser a chave da entidade pai. O exemplo a seguir cria uma entidade do tipo Address e designa uma entidade Employee como pai:

// Create Employee entity
employee := &Employee{ /* ... */ }
employeeKey, err := datastore.Put(ctx, datastore.NewIncompleteKey(ctx, "Employee", nil), employee)

// Use Employee as Address entity's parent
// and save Address entity to datastore
address := &Address{ /* ... */ }
addressKey := datastore.NewIncompleteKey(ctx, "Address", employeeKey)
_, err = datastore.Put(ctx, addressKey, address)

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) do Go Ordem de classificação Observações
Número inteiro int
int8
int16
int32
int64
Numérico Número inteiro de 64 bits, assinado
Número de ponto flutuante float32
float64
Numérico Precisão dupla de 64 bits,
IEEE 754
Booleano bool false<true
String (curta) string Unicode
Até 1500 bytes. Os valores superiores a 1500 bytes resultam em um erro no tempo de execução.
String (longa) string (com noindex) Nenhum Até 1 megabyte

Não indexado
Fatia de bytes (curta) datastore.ByteString Ordem de bytes Até 1500 bytes. Os valores superiores a 1500 bytes resultam em um erro no tempo de execução.
Fatia de bytes (longa) []byte Nenhum Até 1 megabyte

Não indexado
Data e hora time.Time Cronológica
Ponto geográfico appengine.GeoPoint Por latitude,
e depois longitude
Chave do Datastore *datastore.Key Por elementos do caminho
(tipo, identificador,
tipo, identificador...)
Chave Blobstore appengine.BlobKey Ordem de bytes

Você também pode usar um struct ou slice para agregar propriedades. Consulte a referência do Datastore para mais detalhes.

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
  3. Valores booleanos
  4. Sequências de bytes
    • fatias de bytes (curtas)
    • string Unicode
    • Chaves do Blobstore
  5. Números de ponto flutuante
  6. pontos geográficos
  7. Chaves do armazenamento de dados

Como as fatias de bytes e as strings longas não são indexadas, elas não têm 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.

Como criar uma entidade

Para criar uma nova entidade no Go, gere uma instância de estrutura do Go preenchendo os campos dela e chamando datastore.Put para salvá-la no Datastore. Apenas os campos exportados (que começam com uma letra maiúscula) serão salvos no Datastore. É possível especificar o nome de chave da entidade transmitindo um argumento stringID não vazio para datastore.NewKey:

employee := &Employee{
	FirstName: "Antonio",
	LastName:  "Salieri",
	HireDate:  time.Now(),
}
employee.AttendedHRTraining = true
key := datastore.NewKey(ctx, "Employee", "asalieri", 0, nil)
_, err = datastore.Put(ctx, key, employee)

Se você fornecer um nome de chave vazio ou usar datastore.NewIncompleteKey, o Datastore gerará automaticamente um ID numérico para a chave da entidade:

employee := &Employee{
	FirstName: "Antonio",
	LastName:  "Salieri",
	HireDate:  time.Now(),
}
employee.AttendedHRTraining = true
key := datastore.NewIncompleteKey(ctx, "Employee", nil)
_, err = datastore.Put(ctx, key, employee)

Recuperar uma entidade

Para recuperar uma entidade identificada por uma determinada chave, transmita o *datastore.Key como um argumento para a função datastore.Get. É possível gerar o *datastore.Key usando a função datastore.NewKey.

employeeKey := datastore.NewKey(ctx, "Employee", "asalieri", 0, nil)
addressKey := datastore.NewKey(ctx, "Address", "", 1, employeeKey)
var addr Address
err = datastore.Get(ctx, addressKey, &addr)

O datastore.Get preenche uma instância da estrutura do Go apropriada.

Como atualizar uma entidade

Para atualizar uma entidade atual, modifique os atributos da estrutura e, em seguida, chame datastore.Put. Os dados substituem a entidade existente. O objeto inteiro é enviado ao Datastore com todas as chamadas para datastore.Put.

Como excluir uma entidade

Dada a chave de uma entidade, é possível excluir a entidade com a função datastore.Delete:

key := datastore.NewKey(ctx, "Employee", "asalieri", 0, nil)
err = datastore.Delete(ctx, key)

Operações em lote

datastore.Put, datastore.Get e datastore.Delete têm variantes em massa chamadas datastore.PutMulti, datastore.GetMulti e datastore.DeleteMulti. Eles permitem atuar em várias entidades em uma única chamada do Datastore:

// A batch put.
_, err = datastore.PutMulti(ctx, []*datastore.Key{k1, k2, k3}, []interface{}{e1, e2, e3})

// A batch get.
var entities = make([]*T, 3)
err = datastore.GetMulti(ctx, []*datastore.Key{k1, k2, k3}, entities)

// A batch delete.
err = datastore.DeleteMulti(ctx, []*datastore.Key{k1, k2, k3})

As operações em lote não alteram os custos. Você será cobrado por todas as chaves em uma operação em lote, independentemente de cada chave existir ou não. O tamanho das entidades envolvidas em uma operação não afeta o custo.