Swift / SwiftUI

JSON e JSONB tipo-safos em StructuredQueries

A versão 0.35.0 do StructuredQueries, uma biblioteca de construção de consultas poderosa, introduz suporte para armazenamento e consulta de JSON em formatos binários (JSONB) no SQLite, além de funções JSON tipo-safas e a função table-valued json_each. Essa atualização permite que os desenvolvedores armazene dados complexos de forma mais eficiente e consulte-os com segurança de esquema e tipos.

Compartilhar
Type-safe JSON and JSONB in StructuredQueries

Estamos animados em anunciar a versão 0.35.0 da nossa poderosa biblioteca de construção de consultas: StructuredQueries. Ela traz novas ferramentas para armazenar e consultar JSON em seus bancos de dados SQLite, incluindo suporte ao formato binário JSONB eficiente do SQLite, um conjunto completo de funções tipo-safos JSON e até mesmo a poderosa função de tabela json_each.

Participe conosco em uma rápida visão geral dessas ferramentas, e certifique-se de atualizar suas dependências para ter acesso a elas.

Armazenando JSON e JSONB em suas tabelas

Sempre foi possível armazenar tipos de dados complexos Codable em uma única coluna de uma tabela SQLite usando a ferramenta JSONRepresentation da biblioteca:

@Table struct Trip: Identifiable { let id: UUID var name = "" @Column(as: [Location].JSONRepresentation.self) var geofence: [Location] = [] }

Isso serializa o array de localizações para texto JSON e armazena-o em uma única coluna TEXT da tabela. É um truque útil, mas texto simples é uma maneira bastante ineficiente de manter esses dados. O JSON é um formato verboso com muitos rótulos repetidos, e toda vez que o SQLite precisa ler um valor do payload ele deve re-analisar todo o documento.

Por essa razão, o SQLite oferece uma alternativa em formato binário para JSON chamada JSONB. Ele é ajustado especificamente para ler e atualizar dados em um payload JSON de forma eficiente sem re-analisar e re-renderizar o documento, e até mesmo ocupa menos espaço no disco.

E agora StructuredQueries tem suporte completo para JSONB. Basta uma pequena alteração em seu esquema:

-@Column(as: [Location].JSONRepresentation.self) +@Column(as: [Location].JSONBRepresentation.self) var geofence: [Location] = []

... e seus dados serão armazenados no banco de dados como um blob binário eficiente. A biblioteca lida automaticamente com todos os detalhes da codificação e decodificação desse formato para você. Ao selecionar uma coluna JSONB, a biblioteca envolve-a na função SQLite json, e ao inserir ou atualizar ela envolve o valor na função jsonb:

Trip.insert { $0.geofence } values: { locations }
INSERT INTO "trips" ("geofence") VALUES (jsonb('[…]'))
SELECT …, json("trips"."geofence") FROM "trips"

Você pode simplesmente esquecer que JSONB está envolvido e escrever consultas como o normal.

Armazenar dados como JSON não significa abandonar a capacidade de consultar esses dados. O SQLite vem com uma família extensa de funções JSON para acessar, extrair, atualizar, inserir e remover valores em um payload JSON, e a versão 0.35.0 do StructuredQueries traz ferramentas tipo-safas e esquema-safas para todas elas.

Por exemplo, suponha que a localização de uma viagem seja armazenada em uma coluna JSON:

@Table struct Trip: Identifiable {
  // …
  @Column(as: Location.JSONRepresentation.self)
  var location: Location
}

@Selection struct Location: Codable {
  var latitude = 0.0
  var longitude = 0.0
}

Ao aplicar o macro @Selection ao tipo Location, a estrutura do tipo torna-se visível para o construtor de consultas, e você pode agora navegar no JSON usando o método jsonExtract e uma familiar chave Swift:

Trip.where {
  $0.location
    .jsonExtract(\.longitude) < 0
}
SELECT … FROM "trips"
WHERE json_extract(
  "trips"."location", 
  '$."longitude"') < 0

Note que a chave é traduzida para o equivalente JSON path no SQLite. E o tipo de longitude é entendido pelo construtor de consultas para ajudar a evitar que você faça algo sem sentido, como comparar a longitude com uma string:

Trip.where { $0.location.jsonExtract(\.longitude) < "" }

Isso significa que você obtém segurança de esquema nas colunas da tabela, segurança de esquema nos campos dentro dos payloads JSON e segurança de tipo nos valores extraídos. E essas expressões podem ser usadas em qualquer lugar na consulta: cláusulas where, selects, orders e além.

Você também pode atualizar payloads JSON diretamente no banco de dados, sem precisar carregá-los na memória, usando jsonSet, jsonInsert, jsonAppend, jsonRemove e jsonReplace:

Profile.update {
  $0.author = $0.author
    .jsonSet(\.name, "Blob")
}
UPDATE "profiles"
SET "author" = json_set(
  "profiles"."author", 
  '$."name"', 'Blob'
)
Profile.update {
  $0.tags = $0.tags
    .jsonAppend("new")
}
UPDATE "profiles"
SET "tags" = json_insert(
  "profiles"."tags", 
  '$[#]', 'new'
)

Há também ferramentas para agregações JSON (jsonGroupArray), construindo objetos JSON a partir das colunas de uma tabela (jsonObject), calculando o comprimento de um array JSON (jsonArrayLength) e muito mais. E cada uma dessas ferramentas vem com uma variante JSONB (jsonbExtract, jsonbSet, jsonbGroupArray, ...) para quando o resultado deve permanecer no formato binário, como ao atribuir a uma coluna JSONB.

Consultando coleções JSON com json_each

O SQLite fornece uma função de tabela, json_each, que transforma um array ou objeto JSON em uma tabela virtual do SQLite que pode ser consultada como qualquer outra tabela.

A StructuredQueries agora oferece o método jsonEach() em colunas baseadas em JSON e JSONB, expondo essa funcionalidade de forma segura por tipo. Suponha que queremos encontrar todas as viagens cuja geofence inteira está no hemisfério sul. Isso requer iterar sobre cada coordenada do array da geofence e verificar se nenhuma latitude é maior que zero, e isso pode ser feito diretamente em SQL:

Trip.where {
  !$0.geofence.jsonEach()
    .where { 
      $0.value
        .jsonExtract(\.latitude) > 0 
    }
    .exists()
}
SELECT … FROM "trips"
WHERE NOT EXISTS (
  SELECT … 
  FROM json_each("trips"."geofence")
  WHERE json_extract(
    "json_each"."value", 
    '$."latitude"') > 0
)

O método jsonEach() retorna uma instrução SELECT cujas linhas têm um key (o índice em um array JSON, ou nome do membro em um objeto JSON) e um value (o elemento em si), e você pode encadear cláusulas where, selects e mais, assim como qualquer outra consulta. Não há necessidade de carregar tudo na memória apenas para processar os resultados, não é necessário recorrer a SQL tipado por string, e o construtor de consultas tem você coberto durante todo o caminho.

Isso também funciona para arrays de valores escalares simples:

Reminder.where {
  $0.tags.jsonEach()
    .where { 
      $0.value.eq("urgent") 
    }
    .exists()
}
SELECT … FROM "reminders"
WHERE EXISTS (
  SELECT … 
  FROM json_each("reminders"."tags")
  WHERE "json_each"."value" = 'urgent'
)

Isso é apenas um gostinho do que é possível com as novas ferramentas JSON. Para uma imersão ainda mais profunda, incluindo a classificação de viagens pela distância ao vivo da localização do usuário calculada inteiramente em SQL, certifique-se de conferir nossa nova série de episódios, WWDC26, e atualize para a versão 0.35.0 do StructuredQueries para começar a usar essas ferramentas hoje!

Fonte original

Conteúdo traduzido e adaptado pela redação do Notícias Mobile. Confira também a matéria na fonte original.

Leia a matéria completa