Skip to content

Instantly share code, notes, and snippets.

@colltoaction
Last active April 27, 2017 12:26
Show Gist options
  • Save colltoaction/734bc9728db0b24a4795d7081c9ebad6 to your computer and use it in GitHub Desktop.
Save colltoaction/734bc9728db0b24a4795d7081c9ebad6 to your computer and use it in GitHub Desktop.
Presentación para el Meetup NetBaires del 26/04/2017
<!DOCTYPE html>
<html>
<head>
<title>Qué se viene en EF Core 2</title>
<meta charset="utf-8">
<style>
@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
@import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
@import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
body { font-family: 'Droid Serif'; }
h1, h2, h3 {
font-family: 'Yanone Kaffeesatz';
font-weight: normal;
}
table, th, td {
border-collapse: collapse;
border: 1px solid black;
padding: 4px;
}
.remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
</style>
</head>
<body>
<textarea id="source">
class: center, middle
# Qué se viene en EF Core 2
## Una presentación de las nuevas features
---
# Schedule
https://github.com/aspnet/Home/wiki/Roadmap
|Release|Time frame|
|-------|----------|
|1.0.3 |Dec 2016 |
|1.0.4 |Feb 2017 |
|2.0 |Q3 2017 |
---
# Features
## Roadmap:
https://github.com/aspnet/EntityFramework/wiki/Roadmap#ef-core-20
## Issues:
https://github.com/aspnet/EntityFramework/issues?q=is%3Aissue+milestone%3A2.0.0
---
# Temas
* Query translation
* DbContext pooling
* Compiled queries
* Owned entity types
* Seed data
???
Esta es una charla más o menos avanzada sobre EF.
Vamos a hablar sobre nuevas features en EF Core 2, así que si ya usaron EF van a sacarle más jugo.
Si no, no se preocupen, que elegí cosas que son técnicamente interesantes.
---
# Query translation - Client evaluation - 1
Anuncio de interés público: activar warnings.
```csharp
public partial class MyContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.ConfigureWarnings(w =>
{
w.Throw(RelationalEventId.QueryClientEvaluationWarning);
});
base.OnConfiguring(optionsBuilder);
}
}
```
???
Si bien EF Core tiene la capacidad de ejecutar en memoria todas las operaciones que no hayan podido ser traducidas a SQL,
esto puede resultar en una pérdida de performance y/o un elevado uso de recursos.
---
# Query translation - Client evaluation - 2
## EF Core 1.X
```csharp
ctx.OrderDetails
.Where(od => od.DateCreated.Year == 2017)
.Where(od => Math.Sign(od.Discount) == 1)); // equivalente a od.Discount > 0
```
```sql
SELECT *
FROM OrderDetail
WHERE YEAR(DateCreated) = 2017 -- Todas las órdenes del 2017!!
```
+
```csharp
return GetFromSql(sql) // pseudocódigo que llama a la BD
.Where(od => Math.Sign(od.Discount) == 1));
```
---
# Query translation - Client evaluation - 3
## EF Core 2.0
Aporte de la comunidad: https://github.com/aspnet/EntityFramework/pull/7672
```csharp
ctx.OrderDetails
.Where(od => od.DateCreated.Year == 2017)
.Where(od => Math.Sign(od.Discount) == 1)); // equivalente a od.Discount > 0
```
```sql
SELECT *
FROM OrderDetail
WHERE YEAR(DateCreated) = 2017
AND SIGN(Discount) = 1
```
+
```csharp
return GetFromSql(sql); // pseudocódigo que llama a la BD
```
???
Menos transferencia de datos + aprovechamiento de las características de la BD
---
# Query translation - Mejoras - 1
## Nuevo include compiler
* Más eficiente
* Más escenarios soportados
* Ahora con 100% más recursión™
???
Una de las cosas que uno espera de un ORM es que nuestras queries sean traducidas eficientemente a código SQL.
LINQ es tan potente que muchas queries son difíciles de traducir eficientemente, y se intenta mejorar constantemente.
---
# Query translation - Mejoras - 2
## EF Core 1.X
```csharp
ctx.Customers
.FromSql(@"SELECT * FROM [Customers]")
.Include(c => c.Orders);
```
```sql
SELECT *
FROM [Orders] AS [o]
WHERE EXISTS (
SELECT 1
FROM (
SELECT * FROM [Customers]
) AS [c]
WHERE [o].[CustomerID] = [c].[CustomerID])
```
???
Vemos una característica potente, que es el uso de SQL escrito a mano.
Esto permite optimizar queries complicadas, pero también genera código poco eficiente como WHERE EXISTS.
---
# Query translation - Mejoras - 3
## EF Core 2.0
```csharp
ctx.Customers
.FromSql(@"SELECT * FROM [Customers]")
.Include(c => c.Orders);
```
```sql
SELECT *
FROM [Orders] AS [o]
INNER JOIN (
SELECT * FROM [Customers]
) AS [c0]
ON [o].[CustomerID] = [c0].[CustomerID]
```
???
El nuevo compilador optimiza incluso casos donde se use custom SQL!
---
# DbContext pooling
### DbContext:
* Contiene toda la información del modelo de datos
* En una aplicación ASP.NET, se crea uno por cada request
* Es pesado y costoso de crear
### Posibles mejoras:
* Hacerlo más rápido y liviano (no es factible)
* **Hacer un pool de DbContexts**
### Pooling:
* Los contextos se reusan
* Quedan en memoria y son re-inicializados
* Se evita que el DI cree nuevamente todo el grafo de objetos
???
Una demo para esto o bien sería muy complicada.
Se hicieron benchmarks y se descubrió que crear un DbContext es más pesado de lo que debería,
y esto afecta en particular a las aplicaciones web que cada vez que se hace un request instancian uno nuevo.
La solución es sencillamente hacer un pool de DbContext que son reutilizados a lo largo de la vida del programa.
Gracias a esto, es posible disminuir el tiempo de respuesta ya que el DI framework no tiene que hacer tanto trabajo.
---
# Compiled queries - 1
https://github.com/aspnet/EntityFramework/issues/7009
> “Adds `EF.CompileQuery` and `EF.CompileAsyncQuery` which allow for the creation of user managed
> compiled query delegates. Such queries are more efficient to execute because they bypass the
> `CompiledQueryCache` (avoiding query hashing and parameterization).
>
> Typical gains are around 3-5%”
### Notas
* Se evita hacer trabajo de reflection múltiples veces
* Es 100% controlada por el programador
* No es adecuada para queries dinámicas (sí parametrizadas)
---
# Compiled queries - 2
Ejemplo sencillo tomado de los benchmarks de ASP.NET Core.
https://github.com/aspnet/benchmarks/pull/179
## EF Core 1.X
```csharp
public Task<World> LoadSingleQueryRow()
{
var id = _random.Next(1, 10001);
return _dbContext.World.FirstAsync(w => w.Id == id);
}
```
---
# Compiled queries - 3
Ejemplo sencillo tomado de los benchmarks de ASP.NET Core.
https://github.com/aspnet/benchmarks/pull/179
## EF Core 2.0 - ~50% mejora
```csharp
private static readonly Func<ApplicationDbContext, int, Task<World>>
_firstWorldQuery = EF.CompileAsyncQuery(
(ApplicationDbContext ctx, int id) => ctx.World.First(w => w.Id == id));
public Task<World> LoadSingleQueryRow()
{
var id = _random.Next(1, 10001);
return _firstWorldQuery(_dbContext, id);
}
```
???
En este escenario en particular, la query precompilada da entre 50% y 90% de mejora de performance,
lo cual se puede explicar porque LoadSingleQueryRow es llamada multiples veces,
y en cada una se recompila la query en cuestión.
Detalle: Si bien se cambió a la versión sync (First vs FirstAsync), la query es traducida eficientemente a async.
---
# Owned Entity Types - 1
https://github.com/aspnet/EntityFramework/pull/7552
* Una entidad depende 100% de otra
* La FK es la PK => la relación es sí o sí 1-1
* Posibilita escenarios que antes usaban Complex Types
### Esquema de ejemplo
Customer
|Id |Name|
|---|----|
|...|... |
Address
|CustomerId|Street|City|
|----------|------|----|
|... |... |... |
???
Tiene sentido usar esta feature cuando una entidad solo tiene sentido como parte de otra,
y posibilita escenarios como los que soporta Complex Types.
Cuando consulté, me comentaron el ejemplo más clásico donde un cliente tiene una dirección,
y esta es única y exclusiva del cliente.
---
# Owned Entity Types - 2
Modelo que mapea a esa tabla
```csharp
class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address BillingAddress { get; set; }
}
class Address
{
public string Street { get; set; }
public string City { get; set; }
}
// ....
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.BillingAddress);
```
---
# Owned Entity Types - 3
Eventualmente, cuando se implemente la configuración de table splitting
```csharp
// ....
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.BillingAddress)
.SameTable()); // pseudocódigo
```
### Esquema resultante
Customer
|Id |Name|BillingAddress_Street|BillingAddress_City|
|---|----|---------------------|-------------------|
|...|... |... |... |
---
# Seed data - 1
https://github.com/aspnet/EntityFramework/issues/629#issuecomment-282368487
Este fue mi proyecto de pasante en Microsoft :)
* Está en EF6, no en EF Core 1.X
* Diseño distinto a EF 6
* Cubre nuevos casos de uso
* Se basa en migraciones
* Es lo más
???
Cuando me plantearon hacer esto, yo ya llevaba alrededor de un mes en Microsoft.
A simple vista parece sencillo, pero cuando empezamos a discutir el diseño
me pareció una locura para los dos meses que me quedaban.
No solo íbamos a implementar algo que cubriera lo que teníamos antes,
sino que nos decidimos por un diseño totalmente nuevo que funcionaba fundamentalmente distinto
y además cubría nuevos casos de uso.
---
# Seed data - 2
### EF Core 1.X: Migrations
* Analiza el modelo
* Calcula cambios (diff)
* Genera scripts para actualizar el modelo
Algunas operaciones:
* DropTable
* RenameColumn
* CreateColumn
* CreateIndex
---
# Seed data - 3
### EF Core 2.0: Migrations **+ Seed data**
* Analiza el modelo
* Calcula cambios (diff)
* Genera scripts para actualizar el modelo
* **Analiza los datos**
* **Calcula cambios en datos (diff)**
* **Genera scripts para actualizar los datos**
Algunas operaciones:
* DropTable
* RenameColumn
* CreateColumn
* CreateIndex
* **Insert (row)**
* **Delete (row)**
* **Update (row)**
???
El nuevo diseño se integra directamente con las migraciones,
que es una característica que genera scripts para cambiar el esquema de una base de datos cuando uno cambia su modelo.
Al ser parte de las migraciones, uno puede agregar seed data en cualquier momento del desarrollo
y generar una nueva migración: al igual que con el esquema,
logramos que el motor del ORM generara operaciones de migración para datos.
---
# Seed data - 4
### Ejemplo 1 - https://github.com/aspnet/EntityFramework/issues/629#issuecomment-290511112
Modelo
```csharp
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
```
---
# Seed data - 5
Configuración por defecto + seed data
```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// #1 - InitialMigration
modelBuilder.Entity<Post>().SeedData(
new Post { PostId = 545, Title = "Title of the seed post", BlogId = 12 });
modelBuilder.Entity<Blog>().SeedData(
new Blog { Url = "theseedblog.com", BlogId = 12 });
}
```
---
# Seed data - 6
Migration
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable("Blogs", /*...*/);
migrationBuilder.CreateTable("Posts", /*...*/);
migrationBuilder.Insert(
table: "Blogs",
columns: new[] { "BlogId", "Url" },
values: new object[] { 12, "theseedblog.com" });
migrationBuilder.Insert(
table: "Posts",
columns: new[] { "PostId", "BlogId", "Content", "Title" },
values: new object[] { 545, 12, null, "Title of the seed post" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Posts");
migrationBuilder.DropTable(
name: "Blogs");
}
```
???
Podemos ver por ejemplo que los inserts se hacen en el orden correcto
para no tener conflictos de foreign keys.
También podemos ver que el método Down es eficiente, y no ejecuta DELETE innecesarios.
---
# Seed data - 7
### Ejemplo 2
```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// #2 - ChangeTitleAndColumnName
modelBuilder.Entity<Post>(e => {
e.Property(p => p.Title).HasColumnName("The Title");
e.SeedData(new Post { PostId = 545, Title = "The new and updated title", BlogId = 12 });
});
modelBuilder.Entity<Blog>().SeedData(
new Blog { Url = "theseedblog.com", BlogId = 12 });
}
```
???
En este caso hicimos un cambio de esquema y al mismo tiempo actualizamos el título del post.
---
# Seed data - 8
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Title",
table: "Posts",
newName: "The Title");
migrationBuilder.Update(
table: "Posts",
keyColumns: new[] { "PostId" },
keyValues: new object[] { 545 },
columns: new[] { "The Title" },
values: new object[] { "The new and updated title" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "The Title",
table: "Posts",
newName: "Title");
migrationBuilder.Update(
table: "Posts",
keyColumns: new[] { "PostId" },
keyValues: new object[] { 545 },
columns: new[] { "Title" },
values: new object[] { "Title of the seed post" });
}
```
???
Vemos como la migración es correctamente generada, usando los nombres de columna correctos.
---
# Seed data - 8
### Conclusiones
* Feature compleja en poco tiempo
* Muy buena calidad de código existente
* Alta reutilización
* Responsabilidades bien definidas
* Equipo muy conocedor de su codebase
* Características muy útiles como Shadow Entities
???
Mi trabajo habría tomado años si no hubiera sido por la calidad del código,
donde cada componente tiene responsabilidades muy bien definidas y es muy fácilmente reutilizable.
---
# Quedaron afuera
* SQLite custom builds
* .NET Standard 2.0
* .NET Native
* ApplicationInsights
* DI Improvements (prevent API changes)
* Better logging
* Better tooling (`dotnet ef`)
---
class: center, middle
# Muchas gracias!
</textarea>
<script src="https://remarkjs.com/downloads/remark-latest.min.js">
</script>
<script>
var slideshow = remark.create();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment