JSONB in PostgreSQL with EF Core

JSONB in PostgreSQL with EF Core

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??Offering MVP Development, Legacy Migration, & Product Engineering??Expert in scalable solutions, high-load systems, and API integrations

10 个月

Hello guys! The second part of JSONB in PostgreSQL and EF Core series -

回复
Astemir Almov

Senior C# .NET engineer

10 个月

Hi, Serhii Kokhan. Great article. Agree with Chris Zioutas that it's not very well documented. That's why I have question which is not well discussed. What are the best practices for writing migrations? Is it preferable to utilize the invasive approach with PgSql? Is there a way to write migrations in a NoSql-style, interacting with fields as JsonObjects?

Hey Serhii, thanks for the article, this topic is actually not that greatly documented with examples! However from any implementation I have tried I keep receiving the same error message System.NotSupportedException: Type 'Dictionary`2' required dynamic JSON serialization, which requires an explicit opt-in; call 'EnableDynamicJson' on 'NpgsqlDataSourceBuilder' or NpgsqlConnection.GlobalTypeMapper (see https://www.npgsql.org/doc/types/json.html and the 8.0 release notes for more details). Alternatively, if you meant to use Newtonsoft JSON.NET instead of System.Text.Json, call UseJsonNet() instead. It essentially tells me that it does not know how to map the dictionary. Am I missing something? I am on npgsql 8+

要查看或添加评论,请登录

Serhii Kokhan的更多文章

  • AsyncLocal vs ThreadLocal

    AsyncLocal vs ThreadLocal

    ??Introduction Let’s be honest - working with async programming and multithreading in .NET can be a headache.

  • Enhancing .NET Logging with Serilog Custom Enrichers

    Enhancing .NET Logging with Serilog Custom Enrichers

    What Are Enrichers in Serilog? Enrichers in Serilog are components that automatically add extra context to log events…

    2 条评论
  • Data Synchronization in Chrome Extensions

    Data Synchronization in Chrome Extensions

    Introduction Data synchronization in Chrome extensions is a common challenge, especially for various tools ranging from…

  • Dalux Build API Changelog

    Dalux Build API Changelog

    Dalux unfortunately does not provide an official changelog for their API updates. To help developers stay informed, I…

    2 条评论
  • JSONB in PostgreSQL with EF Core - Part 2

    JSONB in PostgreSQL with EF Core - Part 2

    Introduction Welcome back to the second part of our series on using JSONB in PostgreSQL with EF Core. In our previous…

  • Proxy vs Reverse Proxy in the .NET 8 Universe

    Proxy vs Reverse Proxy in the .NET 8 Universe

    Today, we're diving into the world of proxies – but not just any proxies. We're talking about the classic proxy and its…

  • Mastering the use of System.Text.Json

    Mastering the use of System.Text.Json

    Introduction Handling JSON data is a daily task for many developers, given its widespread use in modern applications…

  • Firebase Multitenancy & .NET 7

    Firebase Multitenancy & .NET 7

    Introduction Firebase is a leading platform for developing mobile and web applications, offering a variety of tools and…

  • How to use Azure Maps in Blazor

    How to use Azure Maps in Blazor

    Introduction Blazor, a powerful and versatile framework for building web applications, allows developers to utilize C#…

  • Azure SQL Database Scaling

    Azure SQL Database Scaling

    Introduction In today's fast-paced digital world, enterprises must be agile and scalable to remain competitive. For…

社区洞察

其他会员也浏览了