Entidades, propriedades e chaves

Os objetos de dados no Datastore são conhecidos como entidades. Uma entidade tem uma ou mais propriedades com nome, cada uma das quais pode ter um ou mais valores. As entidades do mesmo tipo não têm de ter as mesmas propriedades e os valores de uma entidade para uma determinada propriedade não têm de ser todos do mesmo tipo de dados. (Se necessário, uma aplicação pode estabelecer e aplicar essas restrições no seu próprio modelo de dados.)

O Datastore suporta uma variedade de tipos de dados para valores de propriedades. Estas incluem, entre outras:

  • Números inteiros
  • Números de vírgula flutuante
  • Strings
  • Datas
  • Dados binários

Para ver uma lista completa de tipos, consulte o artigo Propriedades e tipos de valores.

Cada entidade no Datastore tem uma chave que a identifica de forma exclusiva. A chave é composta pelos seguintes componentes:

  • O espaço de nomes da entidade, que permite a multilocação
  • O tipo da entidade, que a categoriza para efeitos de consultas do Datastore
  • Um identificador para a entidade individual, que pode ser
    • Uma string de nome da chave
    • um ID numérico inteiro
  • Um caminho do antepassado opcional que localiza a entidade na hierarquia do Datastore

Uma aplicação pode obter uma entidade individual do Datastore através da chave da entidade ou pode obter uma ou mais entidades emitindo uma consulta com base nas chaves ou nos valores das propriedades das entidades.

O SDK do App Engine Go inclui um pacote para representar entidades do Datastore como estruturas Go e para as armazenar e obter no Datastore.

O Datastore em si não aplica restrições à estrutura das entidades, como se uma determinada propriedade tem um valor de um tipo específico. Esta tarefa é deixada à aplicação.

Tipos e identificadores

Cada entidade do Datastore é de um tipo específico,que categoriza a entidade para fins de consultas: por exemplo, uma aplicação de recursos humanos pode representar cada funcionário de uma empresa com uma entidade do tipo Employee. Na API Go Datastore, especifica o tipo de uma entidade quando cria um datastore.Key. Todos os nomes de tipos que começam com dois sublinhados (__) estão reservados e não podem ser usados.

O exemplo seguinte cria uma entidade do tipo Employee, preenche os respetivos valores de propriedades e guarda-a 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, atribuído quando a entidade é criada. Uma vez que faz parte da chave da entidade, o identificador está associado permanentemente à entidade e não pode ser alterado. Pode ser atribuído de duas formas:

  • A sua aplicação pode especificar a sua própria string de nome da chave para a entidade.
  • Pode fazer com que o Datastore atribua automaticamente à entidade um ID numérico inteiro.

Para atribuir um nome de chave a uma entidade, forneça um argumento stringID não vazio a 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 que o Datastore atribua automaticamente um ID numérico, 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)

Atribuir identificadores

O Datastore pode ser configurado para gerar IDs automáticos através de duas políticas de IDs automáticos diferentes:

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

Se quiser apresentar os IDs das entidades ao utilizador e/ou depender da respetiva ordem, a melhor opção é usar a atribuição manual.

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

Caminhos de antecessores

As entidades no Cloud Datastore formam um espaço estruturado hierarquicamente semelhante à estrutura de diretórios de um sistema de ficheiros. Quando cria uma entidade, pode, opcionalmente, designar outra entidade como respetiva principal. A nova entidade é uma secundária da entidade principal (tenha em atenção que, ao contrário de um sistema de ficheiros, a entidade principal não tem de existir efetivamente). Uma entidade sem um elemento principal é uma entidade raiz. A associação entre uma entidade e a respetiva entidade principal é permanente e não pode ser alterada depois de a entidade ser criada. O Cloud Datastore nunca atribui o mesmo ID numérico a duas entidades com o mesmo elemento principal ou a duas entidades raiz (as que não têm um elemento principal).

O principal de uma entidade, o principal do principal e assim sucessivamente, são os seus ancestrais; os seus secundários, os secundários dos secundários e assim sucessivamente, são os seus descendentes. Uma entidade raiz e todos os respetivos descendentes pertencem ao mesmo grupo de entidades. A sequência de entidades que começa com uma entidade de raiz e prossegue do elemento principal para o elemento secundário, conduzindo a uma determinada entidade, constitui o caminho do antepassado dessa entidade. A chave completa que identifica a entidade consiste numa sequência de pares de tipo-identificador que especificam o respetivo caminho de antepassados e terminam com os da própria entidade:

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

Para uma entidade raiz, o caminho de antepassados está vazio e a chave consiste apenas no tipo e no identificador da própria entidade:

[Person:GreatGrandpa]

Este conceito é ilustrado pelo diagrama seguinte:

Mostra a relação da entidade raiz com as entidades
  secundárias no grupo de entidades

Para designar o principal de uma entidade, use o argumento parent para datastore.NewKey. O valor deste argumento deve ser a chave da entidade principal. O exemplo seguinte cria uma entidade do tipo Address e designa uma entidade Employee como a respetiva entidade principal:

// 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

Todas as tentativas de criar, atualizar ou eliminar uma entidade ocorrem 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 garante que todas as operações que contém são aplicadas ao Datastore como uma unidade ou, se alguma das operações falhar, que nenhuma delas é aplicada. Além disso, todas as leituras fortemente consistentes (consultas de antecessores ou obtenções) realizadas na mesma transação observam um instantâneo consistente dos dados.

Conforme mencionado acima, um grupo de entidades é um conjunto de entidades ligadas através da ascendência a um elemento raiz comum. A organização dos dados em grupos de entidades pode limitar as transações que podem ser realizadas:

  • Todos os dados acedidos por uma transação têm de estar contidos num máximo de 25 grupos de entidades.
  • Se quiser usar consultas numa transação, os seus dados têm de estar organizados em grupos de entidades de forma que possa especificar filtros de antecessores que correspondam aos dados certos.
  • Existe um limite de débito de gravação de cerca de uma transação por segundo num único grupo de entidades. Esta limitação existe porque o Datastore executa a replicação síncrona sem mestre de cada grupo de entidades numa vasta área geográfica para oferecer elevada fiabilidade e tolerância a falhas.

Em muitas aplicações, é aceitável usar a consistência eventual (ou seja, uma consulta não ancestral que abranja vários grupos de entidades, que pode, por vezes, devolver dados ligeiramente desatualizados) quando obtém uma vista geral de dados não relacionados e, em seguida, usar a consistência forte (uma consulta ancestral ou um get de uma única entidade) quando visualiza ou edita um único conjunto de dados altamente relacionados. Em tais aplicações, é normalmente uma boa abordagem usar um grupo de entidades separado para cada conjunto de dados altamente relacionados. Para mais informações, consulte o artigo Estruturar para uma forte consistência.

Propriedades e tipos de valores

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

São suportados os seguintes tipos de valores:

Tipo de valor Tipos de deslocações Ordenação Notas
Número inteiro int
int8
int16
int32
int64
Numérico Número inteiro de 64 bits, com sinal
Número de vírgula 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 com mais de 1500 bytes resultam num erro no tempo de execução.
String (longa) string (com noindex) Nenhum Até 1 megabyte

Não indexado
Intervalo de bytes (curto) datastore.ByteString Ordem dos bytes Até 1500 bytes. Os valores com mais de 1500 bytes resultam num erro no tempo de execução.
Segmento de bytes (longo) []byte Nenhum Até 1 megabyte

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

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

Quando uma consulta envolve uma propriedade com valores de tipos mistos, o Datastore usa uma ordenação determinística 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 (curto)
    • String Unicode
    • Chaves do Blobstore
  5. Números de vírgula flutuante
  6. Pontos geográficos
  7. Chaves do Datastore

Uma vez que as fatias de bytes longas e as strings longas não são indexadas, não têm uma ordem definida.

Trabalhar com entidades

As aplicações podem usar a API Datastore para criar, obter, atualizar e eliminar entidades. Se a aplicação souber a chave completa de uma entidade (ou puder derivá-la da respetiva chave principal, tipo e identificador), pode usar a chave para operar diretamente na entidade. Uma aplicação também pode obter a chave de uma entidade como resultado de uma consulta do Datastore. Consulte a página Consultas do Datastore para mais informações.

Criar uma entidade

Em Go, cria uma nova entidade construindo uma instância de uma struct Go, preenchendo os respetivos campos e chamando datastore.Put para a guardar no Datastore. Apenas os campos exportados (que começam com uma letra maiúscula) são guardados no Datastore. Pode especificar o nome da chave da entidade transmitindo um argumento stringID não vazio a 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 fornecer um nome de chave vazio ou usar datastore.NewIncompleteKey, o Datastore gera 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)

Obter uma entidade

Para obter uma entidade identificada por uma determinada chave, transmita o *datastore.Key como um argumento para a função datastore.Get. Pode gerar o *datastore.Key através da 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)

datastore.Get preenche uma instância da estrutura Go adequada.

Atualizar uma entidade

Para atualizar uma entidade existente, modifique os atributos da struct e, em seguida, chame datastore.Put. Os dados substituem a entidade existente. O objeto completo é enviado para o Datastore com cada chamada para datastore.Put.

Eliminar uma entidade

Dada a chave de uma entidade, pode eliminar 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 denominadas datastore.PutMulti, datastore.GetMulti e datastore.DeleteMulti. Permitem agir em várias entidades numa ú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 seus custos. É-lhe cobrado o valor de cada chave numa operação em lote, quer a chave exista ou não. A dimensão das entidades envolvidas numa operação não afeta o custo.