JSONB in PostgreSQL with EF Core

Serhii Kokhan
6 min readJan 4, 2024

--

Introduction

JSONB in PostgreSQL is a big step forward for database management. It mixes the best parts of NoSQL and regular databases. This article looks at what JSONB does in PostgreSQL and how it connects with Entity Framework Core, helping developers build complex applications that rely heavily on data.

Understanding JSONB in PostgreSQL

What is JSONB?

JSONB, standing for JSON Binary, is a specialized data format in PostgreSQL designed for storing JSON data. It differs from the traditional json data type in PostgreSQL in that it stores data in a decomposed binary format. This format allows for efficient data processing as it eliminates the need for reparsing the JSON data each time it is accessed.

Benefits of JSONB

  • Efficient Indexing: JSONB supports GIN (Generalized Inverted Index) and B-tree indexing. This means faster searches, especially beneficial when querying large datasets.
  • Data Flexibility: It allows for storing and querying semi-structured data. This can be particularly useful for applications that require schema flexibility.
  • Operational Efficiency: JSONB offers a wide range of operators for querying and manipulating JSON data. It also supports full-text search.

JSONB Primitives & Operations

Selecting Data

The `->` and `->>` operators are used to access object fields and array elements in a JSONB column. The `->` operator returns JSONB objects/arrays, while `->>` returns text.

SELECT details->'specs' FROM products;

Filtering Data

The `@>` operator checks if the left JSONB value contains the right JSONB path/value entries at the top level.

SELECT * FROM products WHERE details @> '{"category": "Electronics"}';

Indexing for Performance

Create a GIN index on a jsonb column to enhance operations like containment checks.

CREATE INDEX idx_jsonb_gin ON products USING GIN (details);

Working with Nested JSON Data

For nested data, the `#>` and `#>>` operators can navigate through nested JSON objects.

SELECT details#>>'{specs, resolution}' FROM products;

Combining JSONB with SQL

JSONB queries can be integrated with SQL features like `JOIN`, `GROUP BY`, and aggregation functions.

JSONB Aggregation Functions

jsonb_agg

Aggregates values from a set of JSONB values into a single JSON array.

SELECT jsonb_agg(details) FROM products;

jsonb_object_agg

Aggregates JSONB values into a single JSON object, using a key and value.

SELECT jsonb_object_agg(details->>'name', details->>'price') FROM products;

JSONB Expansion Functions

jsonb_each

Expands the outermost JSON object into a set of key-value pairs.

SELECT jsonb_each(details) FROM products;

jsonb_each_text

Similar to jsonb_each, but returns all values as text.

SELECT jsonb_each_text(details) FROM products;

JSONB Query Examples

Filtering by Top-Level Attribute Value

Filter records where a jsonb column contains a specified value at its top level.

SELECT * FROM products WHERE details->>'brand' = 'Apple';

Selecting Specific Attribute Value from Items

Select a particular attribute’s value from a jsonb column.

SELECT details->>'price' AS price FROM products;

Filtering Items Containing Specific Attribute

Filter records that include a certain attribute in a jsonb column.

SELECT * FROM products WHERE details ? 'warranty';

Filtering by Nested Attribute Value

Filter records where a jsonb column contains a specified value in a nested object.

SELECT * FROM products WHERE details#>>'{specs, memory}' = '16GB';

Filtering by Attribute in Array

Filter records where a jsonb array contains an object with a specific attribute value.

SELECT * FROM products WHERE details->'colors' @> '["red"]';

Using IN Operator on Attributes

Check if the value of a jsonb attribute is within a set of values.

SELECT * FROM products WHERE details->>'category' IN ('Smartphone', 'Tablet');

Inserting JSON Object

Add a new record with a jsonb column containing a complete JSON object.

INSERT INTO products (details) VALUES ('{"name": "Smart Watch", "price": 250}');

Updating/Inserting Attribute

Modify an existing attribute or add a new one in a jsonb column.

UPDATE products SET details = jsonb_set(details, '{sale}', 'true', true) WHERE details->>'category' = 'Electronics';

Deleting Attribute

Delete a specific attribute from a jsonb column.

UPDATE products SET details = details - 'sale';

Joining Tables by JSONB Attribute

Perform a SQL join where a condition involves a jsonb attribute.

SELECT * FROM orders JOIN products ON orders.product_id = (products.details->>'id')::uuid;

JSONB with EF Core

EF Core with PostgreSQL offers powerful capabilities for managing and querying complex data structures. One such feature is the support for JSONB, a JSON binary format in PostgreSQL.

Defining Entities

Our primary entity is the Product, representing items in our inventory.

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Specifications Specifications { get; set; }
public List<Review> Reviews { get; set; } = new();
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public Dictionary<string, string> Translations { get; set; } = new();
}
  • Specifications: A nested object holding product specifications like material, color, and dimensions.
  • Reviews: A collection of customer reviews.
  • Translations: A dictionary to manage product names in multiple languages.

Specification class encapsulates details about the product.

public class Specifications
{
public string Material { get; set; }
public string Color { get; set; }
public string Dimensions { get; set; }
}

Review class represents customer feedback.

public class Review
{
public string User { get; set; }
public string Content { get; set; }
public int Rating { get; set; }
}

Configuring DbContext

The ProductContext is crucial for configuring the EF Core to work with PostgreSQL and JSONB.

public class ProductContext : DbContext
{
public DbSet<Product> Products => Set<Product>();

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseNpgsql("YourConnectionStringHere");

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Product>()
.OwnsOne(product => product.Specifications, builder => { builder.ToJson(); })
.OwnsMany(product => product.Reviews, builder => { builder.ToJson(); });

modelBuilder.Entity<Product>()
.Property(p => p.Translations)
.HasColumnType("jsonb")
.IsRequired();
}
}
  • ToJson(): This method tells EF Core to treat Specifications and Reviews as JSONB.
  • Translations Property: Configured as a JSONB column to store the dictionary.

Adding Product with Translations

using var db = new ProductContext();
var newProduct = new Product
{
Name = "Ergonomic Chair",
Specifications = new Specifications
{
Material = "Leather",
Color = "Black",
Dimensions = "24 x 24 x 35 inches"
},
Reviews = { new Review { User = "Alice", Content = "Very comfortable", Rating = 5 } },
Translations = {
{ "en", "Ergonomic Chair" },
{ "es", "Silla Ergonómica" }
}
};
db.Products.Add(newProduct);
await db.SaveChangesAsync();

Querying & Updating Translations

var productToUpdate = await db.Products.FirstAsync();
productToUpdate.Translations["de"] = "Ergonomischer Stuhl";
await db.SaveChangesAsync();

Projection with JSONB

var latestProducts = await db.Products
.OrderByDescending(x => x.CreatedAt)
.Select(x => new { x.Name, x.Specifications.Material })
.AsNoTracking()
.ToListAsync();

Best Practices & Considerations

  • Balancing JSONB and Normalized Data: While JSONB is flexible, it’s important not to overuse it. A balance between normalized relational data and JSONB is often the most efficient approach.
  • Indexing Strategy: Indexing should be carefully planned. While GIN indexes are powerful, they can be resource-intensive.
  • Query Optimization: Regularly analyze your query patterns and use the EXPLAIN command to optimize JSONB queries.
  • Write Operations: While jsonb is efficient for reads, write operations like updating nested attributes can be more resource-intensive compared to traditional relational data updates.
  • Memory Usage: Functions like jsonb_agg can consume significant memory when aggregating large datasets.
  • Database Migrations: EF Core will handle JSONB columns as string (nvarchar(max)) types in migrations.
  • Transparent Usage: The use of JSONB-backed properties is seamless in EF Core. The ORM handles serialization and deserialization automatically.
  • Performance: Using JSONB can optimize data retrieval by reducing the need for multiple joins.

Conclusion

The integration of JSONB in PostgreSQL with EF Core provides a robust solution for handling complex, nested, and dynamic data structures within a relational database context. By understanding how to define entities, configure the context, and perform CRUD operations with JSONB properties, developers can significantly enhance their applications’ data management capabilities. The key is to balance the use of JSONB with traditional relational models to maximize both flexibility and performance.

--

--

Serhii Kokhan

Microsoft MVP🔸CTO & .NET/Azure Architect🔸Stripe Certified Professional Developer