La pregunta "qué ORM debería usar" está realmente dirigida a la punta de un gran iceberg cuando se trata de la estrategia general de acceso a datos y la optimización del rendimiento en una aplicación a gran escala.
Diseño de bases de datos y mantenimiento
Este es, por un amplio margen, el determinante más importante del rendimiento de una aplicación o sitio web basado en datos y, a menudo, totalmente ignorado por los programadores.
Si no utiliza las técnicas de normalización adecuadas, su sitio está condenado. Si no tiene claves primarias, casi todas las consultas serán lentas. Si utiliza antipatrones bien conocidos, como el uso de tablas para pares de valores clave (AKA Entity-Attribute-Value) sin una buena razón, explotará la cantidad de lecturas y escrituras físicas.
Si no aprovecha las características que le brinda la base de datos, como la compresión de páginas, el FILESTREAM
almacenamiento (para datos binarios), las SPARSE
columnas, las hierarchyid
jerarquías, etc. (todos los ejemplos de SQL Server), no verá nada cerca del rendimiento que podrías estar viendo.
Debería comenzar a preocuparse por su estrategia de acceso a datos después de haber diseñado su base de datos y convencido de que es tan buena como sea posible, al menos por el momento.
Carga ansiosa vs. perezosa
La mayoría de los ORM usaban una técnica llamada carga diferida para las relaciones, lo que significa que, de manera predeterminada, cargará una entidad (fila de tabla) a la vez y realizará un viaje de ida y vuelta a la base de datos cada vez que necesite cargar uno o varios relacionados (externos clave) filas.
Esto no es algo bueno o malo, sino que depende de lo que realmente se va a hacer con los datos y de cuánto sabes por adelantado. A veces, la carga diferida es absolutamente lo que hay que hacer. NHibernate, por ejemplo, puede decidir no consultar nada y simplemente generar un proxy para una ID en particular. Si todo lo que necesita es la identificación, ¿por qué debería pedir más? Por otro lado, si está intentando imprimir un árbol de cada elemento individual en una jerarquía de 3 niveles, la carga diferida se convierte en una operación O (N²), lo que es extremadamente malo para el rendimiento.
Un beneficio interesante de usar "SQL puro" (es decir, consultas ADO.NET sin procesar / procedimientos almacenados) es que básicamente te obliga a pensar exactamente qué datos son necesarios para mostrar cualquier pantalla o página. ORM y características de carga perezosa no impiden que se haga esto, pero ellos no le dan la oportunidad de ser ... bueno, perezoso , y sin querer explotar el número de consultas que vaya haciendo. Por lo tanto, debe comprender las funciones de carga entusiasta de sus ORM y estar siempre atento a la cantidad de consultas que envía al servidor para cualquier solicitud de página determinada.
Almacenamiento en caché
Todos los ORM principales mantienen un caché de primer nivel, también conocido como "caché de identidad", lo que significa que si solicita la misma entidad dos veces por su ID, no requiere un segundo viaje de ida y vuelta (y si diseñó su base de datos correctamente) ) le ofrece la posibilidad de utilizar la simultaneidad optimista.
El caché L1 es bastante opaco en L2S y EF, tienes que confiar en que está funcionando. NHibernate es más explícito al respecto ( Get
/ Load
vs. Query
/ QueryOver
). Aún así, siempre que intente consultar por ID tanto como sea posible, debería estar bien aquí. Mucha gente se olvida del caché L1 y busca repetidamente la misma entidad una y otra vez por algo que no sea su ID (es decir, un campo de búsqueda). Si necesita hacer esto, debe guardar la ID o incluso la entidad completa para futuras búsquedas.
También hay un caché de nivel 2 ("caché de consultas"). NHibernate tiene esto incorporado. Linq to SQL y Entity Framework han compilado consultas , lo que puede ayudar a reducir bastante las cargas del servidor de aplicaciones compilando la expresión de consulta en sí, pero no almacena en caché los datos. Microsoft parece considerar esto una preocupación de la aplicación en lugar de una preocupación de acceso a datos, y este es un punto débil importante tanto de L2S como de EF. No hace falta decir que también es un punto débil de SQL "en bruto". Para obtener un rendimiento realmente bueno con básicamente cualquier ORM que no sea NHibernate, debe implementar su propia fachada de almacenamiento en caché.
También hay una "extensión" de caché L2 para EF4 que está bien , pero no es realmente un reemplazo total para un caché de nivel de aplicación.
Numero de consultas
Las bases de datos relacionales se basan en conjuntos de datos. Son muy buenos para producir grandes cantidades de datos en un corto período de tiempo, pero no son tan buenos en términos de latencia de consulta porque hay una cierta cantidad de sobrecarga involucrada en cada comando. Una aplicación bien diseñada debe aprovechar los puntos fuertes de este DBMS e intentar minimizar la cantidad de consultas y maximizar la cantidad de datos en cada una.
Ahora no estoy diciendo que consulte toda la base de datos cuando solo necesita una fila. Lo que estoy diciendo es, si usted necesita las Customer
, Address
, Phone
, CreditCard
, y Order
filas, todo al mismo tiempo con el fin de servir a una sola página, a continuación, usted debe preguntar por todos ellos al mismo tiempo, no ejecutar cada consulta por separado. A veces es peor que eso, verá un código que consulta el mismo Customer
registro 5 veces seguidas, primero para obtener el Id
, luego el Name
, luego el EmailAddress
, luego ... es ridículamente ineficiente.
Incluso si necesita ejecutar varias consultas que operan en conjuntos de datos completamente diferentes, generalmente es aún más eficiente enviarlo todo a la base de datos como un "script" único y hacer que devuelva múltiples conjuntos de resultados. Lo que le preocupa es la sobrecarga, no la cantidad total de datos.
Esto puede sonar a sentido común, pero a menudo es muy fácil perder el rastro de todas las consultas que se ejecutan en varias partes de la aplicación; su proveedor de membresía consulta las tablas de usuario / rol, su acción de encabezado consulta el carrito de compras, su acción de menú consulta la tabla de mapa del sitio, su acción de barra lateral consulta la lista de productos destacados y luego su página se divide en algunas áreas autónomas separadas que consulta las tablas Historial de pedidos, Recientemente visto, Categoría e Inventario por separado, y antes de que te des cuenta, estás ejecutando 20 consultas antes de que puedas comenzar a servir la página. Simplemente destruye completamente el rendimiento.
Algunos marcos, y estoy pensando principalmente en NHibernate aquí, son increíblemente inteligentes al respecto y le permiten usar algo llamado futuros que agrupa consultas completas e intenta ejecutarlas todas a la vez, en el último minuto posible. AFAIK, estás solo si quieres hacer esto con cualquiera de las tecnologías de Microsoft; tienes que construirlo en la lógica de tu aplicación.
Indización, predicados y proyecciones
Al menos el 50% de los desarrolladores con los que hablo e incluso algunos DBA parecen tener problemas con el concepto de índices de cobertura. Piensan: "bueno, la Customer.Name
columna está indexada, por lo que cada búsqueda que haga del nombre debe ser rápida". Excepto que no funciona de esa manera a menos que el Name
índice cubra la columna específica que está buscando. En SQL Server, eso se hace INCLUDE
en la CREATE INDEX
declaración.
Si ingenuamente lo usa en SELECT *
todas partes, y eso es más o menos lo que hará cada ORM a menos que especifique explícitamente lo contrario utilizando una proyección, entonces el DBMS puede elegir ignorar por completo sus índices porque contienen columnas no cubiertas. Una proyección significa que, por ejemplo, en lugar de hacer esto:
from c in db.Customers where c.Name == "John Doe" select c
Haces esto en su lugar:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
Y esta voluntad, para la mayoría de ORM modernas, instruir sólo para ir y consultar las Id
y Name
columnas que son presumiblemente cubiertos por el índice (pero no el Email
, LastActivityDate
o cualquier otra columnas que le pasó a pegarse allí).
También es muy fácil eliminar por completo cualquier beneficio de indexación mediante el uso de predicados inapropiados. Por ejemplo:
from c in db.Customers where c.Name.Contains("Doe")
... parece casi idéntico a nuestra consulta anterior, pero de hecho dará como resultado una tabla completa o exploración de índice porque se traduce en LIKE '%Doe%'
. Del mismo modo, otra consulta que parece sospechosamente simple es:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Suponiendo que tiene un índice BirthDate
, este predicado tiene una buena oportunidad de volverlo completamente inútil. Nuestro programador hipotético aquí obviamente ha intentado crear una especie de consulta dinámica ("solo filtra la fecha de nacimiento si se especificó ese parámetro"), pero esta no es la forma correcta de hacerlo. Escrito así en su lugar:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... ahora el motor de base de datos sabe cómo parametrizar esto y hacer una búsqueda de índice. Un cambio menor, aparentemente insignificante, en la expresión de la consulta puede afectar drásticamente el rendimiento.
Desafortunadamente, LINQ en general hace que sea demasiado fácil escribir consultas malas como esta porque a veces los proveedores pueden adivinar lo que estaba tratando de hacer y optimizar la consulta, y a veces no lo son. Por lo tanto, terminas con resultados frustrantemente inconsistentes que habrían sido cegadoramente obvios (para un DBA experimentado, de todos modos) si hubieras escrito un simple SQL antiguo.
Básicamente, todo se reduce al hecho de que realmente tienes que vigilar de cerca tanto el SQL generado como los planes de ejecución a los que conducen, y si no estás obteniendo los resultados que esperas, no temas pasar por alto Capa ORM de vez en cuando y codifique manualmente el SQL. Esto se aplica a cualquier ORM, no solo a EF.
Transacciones y Bloqueo
¿Necesita mostrar datos que estén actualizados hasta el milisegundo? Tal vez, depende, pero probablemente no. Lamentablemente, Entity Framework no te danolock
, solo puedes usarlo READ UNCOMMITTED
a nivel de transacción (no a nivel de tabla). De hecho, ninguno de los ORM es particularmente confiable al respecto; si desea hacer lecturas sucias, debe desplegarse al nivel SQL y escribir consultas ad-hoc o procedimientos almacenados. Entonces, de nuevo, todo se reduce a lo fácil que es hacerlo dentro del marco.
Entity Framework ha recorrido un largo camino en este sentido: la versión 1 de EF (en .NET 3.5) fue horrible, hizo increíblemente difícil romper la abstracción de "entidades", pero ahora tiene ExecuteStoreQuery y Translate , por lo que es realmente No está mal. Haz amigos con estos chicos porque los usarás mucho.
También está el problema del bloqueo de escritura y los puntos muertos y la práctica general de mantener los bloqueos en la base de datos durante el menor tiempo posible. A este respecto, la mayoría de los ORM (incluido Entity Framework) en realidad tienden a ser mejores que SQL sin formato porque encapsulan la unidad del patrón de trabajo , que en EF es SaveChanges . En otras palabras, puede "insertar" o "actualizar" o "eliminar" entidades al contenido de su corazón, siempre que lo desee, con la certeza de que no se introducirán cambios en la base de datos hasta que confirme la unidad de trabajo.
Tenga en cuenta que un UOW no es análogo a una transacción de larga duración. El UOW aún utiliza las características optimistas de concurrencia del ORM y rastrea todos los cambios en la memoria . No se emite una sola declaración DML hasta la confirmación final. Esto mantiene los tiempos de transacción lo más bajo posible. Si crea su aplicación utilizando SQL sin formato, es bastante difícil lograr este comportamiento diferido.
Lo que esto significa específicamente para EF: haga que sus unidades de trabajo sean lo más gruesas posible y no las comprometa hasta que sea absolutamente necesario. Haga esto y terminará con una contención de bloqueo mucho menor que la que usaría los comandos individuales de ADO.NET en momentos aleatorios.
EF está completamente bien para aplicaciones de alto tráfico / alto rendimiento, al igual que cualquier otro marco está bien para aplicaciones de alto tráfico / alto rendimiento. Lo que importa es cómo lo usas. Aquí hay una comparación rápida de los marcos más populares y las características que ofrecen en términos de rendimiento (leyenda: N = No compatible, P = Parcial, Y = sí / compatible):
Como puede ver, a EF4 (la versión actual) no le va demasiado mal, pero probablemente no sea el mejor si el rendimiento es su principal preocupación. NHibernate es mucho más maduro en esta área e incluso Linq to SQL proporciona algunas características que mejoran el rendimiento que EF todavía no ofrece. Raw ADO.NET a menudo va a ser más rápido para escenarios de acceso a datos muy específicos , pero, cuando reúne todas las piezas, realmente no ofrece muchos beneficios importantes que obtiene de los diversos marcos.