Last active
April 27, 2017 12:26
-
-
Save colltoaction/734bc9728db0b24a4795d7081c9ebad6 to your computer and use it in GitHub Desktop.
Presentación para el Meetup NetBaires del 26/04/2017
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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