JSONB in PostgreSQL with EF Core
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
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
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();
}
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();
}
}
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
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.
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 -
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?
Engineering
1 年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+