Files
sql-utilities/docs/EFCore_Integration_Guide.md

372 lines
11 KiB
Markdown

# Strata.SqlTools.EFCore Integration Guide
## Overview
The `Strata.SqlTools.EFCore` project provides seamless integration between Strata.SqlTools QueryBreakdown functionality and Entity Framework Core, allowing you to persist, query, and manage SQL query breakdowns directly within your EF Core DbContext and database.
## Architecture
### Entity Models
The project defines three main EF Core entity models:
1. **QueryBreakdownEntity**: The main entity that stores all query clause information
2. **QueryParameterEntity**: Stores individual query parameters with relationship to QueryBreakdownEntity
3. **WithClauseEntity**: Stores Common Table Expressions (CTEs) with relationship to QueryBreakdownEntity
### Services
- **IQueryBreakdownMapper**: Converts between `QueryBreakdown` (SQL Tools) and `QueryBreakdownEntity` (EF Core)
- **QueryBreakdownMapper**: Default implementation of IQueryBreakdownMapper
- **IQueryBreakdownRepository**: Repository pattern interface for CRUD operations
- **QueryBreakdownRepository**: Default implementation using EF Core DbContext
### Extension Methods
The `DbContextExtensions` class provides helper methods for easy integration with existing DbContext instances.
## Integration Steps
### Step 1: Add DbSets to Your DbContext
```csharp
using Microsoft.EntityFrameworkCore;
using Strata.SqlTools.EFCore.Models;
public class YourDbContext : DbContext
{
// Existing DbSets...
// Add these new DbSets for QueryBreakdown support
public DbSet<QueryBreakdownEntity> QueryBreakdowns { get; set; }
public DbSet<QueryParameterEntity> QueryParameters { get; set; }
public DbSet<WithClauseEntity> WithClauses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure QueryBreakdown entities using the extension method
modelBuilder.ConfigureQueryBreakdownEntities();
// ... rest of your OnModelCreating configuration
}
}
```
### Step 2: Create and Apply Migrations
```bash
# Create a migration for the new entities
dotnet ef migrations add AddQueryBreakdownEntities
# Apply the migration to your database
dotnet ef database update
```
### Step 3: Register Services (if using Dependency Injection)
```csharp
// In your service configuration (e.g., Program.cs)
services.AddScoped<IQueryBreakdownMapper, QueryBreakdownMapper>();
services.AddScoped<IQueryBreakdownRepository>(
provider => new QueryBreakdownRepository(
provider.GetRequiredService<YourDbContext>(),
provider.GetRequiredService<IQueryBreakdownMapper>()
)
);
```
### Step 4: Use in Your Application
```csharp
public class QueryManagementService
{
private readonly IQueryBreakdownRepository _repository;
public QueryManagementService(IQueryBreakdownRepository repository)
{
_repository = repository;
}
public async Task<int> SaveQueryAsync(QueryBreakdown queryBreakdown)
{
return await _repository.AddAsync(queryBreakdown);
}
public async Task<QueryBreakdown?> GetQueryAsync(int id)
{
return await _repository.GetByIdAsync(id);
}
public async Task<List<QueryBreakdown>> GetAllQueriesAsync()
{
return await _repository.GetAllAsync();
}
public async Task UpdateQueryAsync(int id, QueryBreakdown queryBreakdown)
{
await _repository.UpdateAsync(id, queryBreakdown);
}
public async Task<bool> DeleteQueryAsync(int id)
{
return await _repository.DeleteAsync(id);
}
}
```
## Data Persistence
### Serialization Strategy
Complex properties are serialized as JSON for efficient storage:
- **SetupClausesJson**: List of setup clauses
- **FinishClausesJson**: ArrayList of finish clauses
- **ParametersJson**: Dictionary of parameter names and values
- **WithClause**: String representation of the WITH clause
This approach allows for:
- Efficient schema design with minimal tables
- Flexible handling of variable-length data
- Easy deserialization back to the original objects
## Database Schema
### Tables Created
#### QueryBreakdowns
```sql
CREATE TABLE [QueryBreakdowns] (
[Id] int NOT NULL IDENTITY,
[SelectClause] nvarchar(max),
[SelectClauseComment] nvarchar(max),
[FromClause] nvarchar(max),
[FromClauseComment] nvarchar(max),
[WhereClause] nvarchar(max),
[WhereClauseComment] nvarchar(max),
[GroupByClause] nvarchar(max),
[GroupByClauseComment] nvarchar(max),
[HavingClause] nvarchar(max),
[HavingClauseComment] nvarchar(max),
[OrderByClause] nvarchar(max),
[OrderByClauseComment] nvarchar(max),
[WithClause] nvarchar(max),
[RawSql] nvarchar(max),
[SetupClausesJson] nvarchar(max),
[FinishClausesJson] nvarchar(max),
[ParametersJson] nvarchar(max),
[CreatedAt] datetime2 DEFAULT GETUTCDATE(),
[UpdatedAt] datetime2 DEFAULT GETUTCDATE(),
CONSTRAINT [PK_QueryBreakdowns] PRIMARY KEY ([Id])
);
```
#### QueryParameters
```sql
CREATE TABLE [QueryParameters] (
[Id] int NOT NULL IDENTITY,
[QueryBreakdownEntityId] int NOT NULL,
[ParameterName] nvarchar(256) NOT NULL,
[ParameterValue] nvarchar(max),
[ParameterTypeName] nvarchar(256),
CONSTRAINT [PK_QueryParameters] PRIMARY KEY ([Id]),
CONSTRAINT [FK_QueryParameters_QueryBreakdowns] FOREIGN KEY ([QueryBreakdownEntityId]) REFERENCES [QueryBreakdowns] ([Id]) ON DELETE CASCADE,
CONSTRAINT [IX_QueryParameters_Unique] UNIQUE NONCLUSTERED ([QueryBreakdownEntityId], [ParameterName])
);
```
#### WithClauses
```sql
CREATE TABLE [WithClauses] (
[Id] int NOT NULL IDENTITY,
[QueryBreakdownEntityId] int NOT NULL,
[CteName] nvarchar(256) NOT NULL,
[ColumnList] nvarchar(max),
[CteDefinition] nvarchar(max) NOT NULL,
[OrderIndex] int NOT NULL,
CONSTRAINT [PK_WithClauses] PRIMARY KEY ([Id]),
CONSTRAINT [FK_WithClauses_QueryBreakdowns] FOREIGN KEY ([QueryBreakdownEntityId]) REFERENCES [QueryBreakdowns] ([Id]) ON DELETE CASCADE
);
```
## Advanced Usage
### Custom Entity Configuration
To customize the entity mappings, you can create your own configuration classes:
```csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Strata.SqlTools.EFCore.Models;
public class CustomQueryBreakdownConfiguration : IEntityTypeConfiguration<QueryBreakdownEntity>
{
public void Configure(EntityTypeBuilder<QueryBreakdownEntity> builder)
{
// Map to a specific schema
builder.ToTable("QueryBreakdowns", "queries");
// Add additional indexes
builder.HasIndex(e => e.CreatedAt).IsDescending();
// Change column types
builder.Property(e => e.RawSql)
.HasColumnType("varchar(max)");
}
}
// Then apply in your DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CustomQueryBreakdownConfiguration());
}
```
### Querying QueryBreakdowns
You can use LINQ queries to filter and search QueryBreakdowns:
```csharp
// Get all queries created in the last 7 days
var recentQueries = await _context.GetQueryBreakdowns()
.Where(q => q.CreatedAt >= DateTime.UtcNow.AddDays(-7))
.OrderByDescending(q => q.CreatedAt)
.ToListAsync();
// Find queries that select from a specific table
var userQueries = await _context.GetQueryBreakdowns()
.Where(q => q.FromClause != null && q.FromClause.Contains("Users"))
.ToListAsync();
// Get a query with its related parameters
var queryWithParams = await _context.GetQueryBreakdowns()
.Include(q => q.QueryBreakdownEntity) // Include navigation properties if configured
.FirstOrDefaultAsync(q => q.Id == queryId);
```
### Bulk Operations
For efficient bulk operations:
```csharp
var mapper = new QueryBreakdownMapper();
// Bulk insert
var queryBreakdowns = LoadQueriesFromSource();
var entities = queryBreakdowns.Select(q => mapper.MapToEntity(q)).ToList();
_context.Set<QueryBreakdownEntity>().AddRange(entities);
await _context.SaveChangesAsync();
// Bulk update
var existingQueries = await _context.GetQueryBreakdowns().ToListAsync();
foreach (var entity in existingQueries)
{
entity.UpdatedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
```
## Performance Considerations
### Indexing
The configuration includes indexes on:
- Primary keys (Id)
- Foreign keys (QueryBreakdownEntityId)
- Timestamp columns (CreatedAt, UpdatedAt)
- Unique combinations (QueryBreakdownEntityId + ParameterName)
- OrderIndex for WITH clauses
### Query Optimization
For best performance:
1. **Use LINQ projections** instead of loading full entities when possible
2. **Use .AsNoTracking()** for read-only queries
3. **Include related data** with `.Include()` only when needed
4. **Use pagination** for large result sets
5. **Create indexes** on frequently filtered columns
Examples:
```csharp
// Good: Projection for read-only access
var queryTexts = await _context.GetQueryBreakdowns()
.AsNoTracking()
.Select(q => new { q.Id, q.SelectClause, q.FromClause })
.ToListAsync();
// Good: Pagination
var page = await _context.GetQueryBreakdowns()
.AsNoTracking()
.OrderByDescending(q => q.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
```
## Migration Scenarios
### Existing DbContext with Query Tables
If you already have query tables in your database:
1. Create a custom configuration that maps to your existing tables
2. Adjust property names and column types as needed
3. Create a migration with appropriate mapping
```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<QueryBreakdownEntity>()
.ToTable("YourExistingQueryTable");
modelBuilder.Entity<QueryBreakdownEntity>()
.Property(e => e.SelectClause)
.HasColumnName("YourSelectColumn");
}
```
## Troubleshooting
### Issue: SqlException when creating entities
**Solution**: Ensure the migration has been applied: `dotnet ef database update`
### Issue: JSON deserialization errors
**Solution**: Verify that the JSON serialization format matches. The mapper uses `System.Text.Json.JsonSerializer`.
### Issue: Navigation properties are null
**Solution**: Use `.Include()` when querying to load related entities:
```csharp
var entity = await _context.GetQueryBreakdowns()
.Include(q => q.QueryBreakdownEntity)
.FirstOrDefaultAsync(q => q.Id == id);
```
### Issue: Foreign key constraint violations
**Solution**: Ensure that parent entities (QueryBreakdownEntity) are saved before child entities (QueryParameterEntity, WithClauseEntity). The repository handles this automatically.
## Best Practices
1. **Always use transactions** for operations that modify multiple entities
2. **Validate input** before saving to the database
3. **Use eager loading** (`.Include()`) sparingly to avoid performance issues
4. **Monitor database growth** as JSON columns can become large
5. **Implement archival policies** for old query breakdowns
6. **Use async/await** for all database operations
7. **Handle concurrency** using datetime stamps or EF Core's concurrency tokens
## Support and Documentation
- For detailed API documentation, see the README.md in the main EFCore project folder
- For examples of QueryBreakdown usage, see the Strata.SqlTools documentation
- For EF Core documentation, visit https://docs.microsoft.com/en-us/ef/core/