Estrategias para evitar SQL en sus Controladores ... ¿o cuántos métodos debería tener en mis Modelos?

17

Entonces, una situación con la que me encuentro razonablemente a menudo es una en la que mis modelos comienzan a:

  • Conviértete en monstruos con toneladas y toneladas de métodos.

O

  • Permitirle pasar fragmentos de SQL para que sean lo suficientemente flexibles como para no requerir un millón de métodos diferentes

Por ejemplo, supongamos que tenemos un modelo de "widget". Comenzamos con algunos métodos básicos:

  • obtener ($ id)
  • insertar ($ registro)
  • actualización ($ id, $ record)
  • eliminar ($ id)
  • getList () // obtiene una lista de widgets

Eso está muy bien, pero luego necesitamos algunos informes:

  • listCreatedBetween ($ fecha_inicio, $ fecha_final)
  • listPurchasedBetween ($ fecha_inicio, $ fecha_final)
  • listOfPending ()

Y luego los informes comienzan a complicarse:

  • listPendingCreatedBetween ($ fecha_inicio, $ fecha_final)
  • listForCustomer ($ customer_id)
  • listPendingCreatedBetweenForCustomer ($ customer_id, $ start_date, $ end_date)

Puedes ver dónde está creciendo ... eventualmente tenemos tantos requisitos de consulta específicos que necesito implementar toneladas y toneladas de métodos, o algún tipo de objeto de "consulta" que puedo pasar a una sola consulta> consulta método $ query) ...

... o simplemente muerde la bala y comienza a hacer algo como esto:

  • list = MyModel-> query ("fecha_inicio> X Y fecha_final <Y Y pendiente = 1 Y cliente_id = Z")

Hay un cierto atractivo para tener un método como ese en lugar de 50 millones de otros métodos más específicos ... pero a veces se siente "mal" meter un montón de lo que es básicamente SQL en el controlador.

¿Hay una forma "correcta" de manejar situaciones como esta? ¿Parece aceptable incluir consultas como esa en un método genérico -> query ()?

¿Hay mejores estrategias?

Keith Palmer Jr.
fuente
Estoy pasando por este mismo problema ahora mismo en un proyecto que no es MVC. La pregunta sigue surgiendo si la capa de acceso a datos extrae todos los procedimientos almacenados y deja la base de datos de la capa de lógica empresarial independiente, o la capa de acceso a datos debe ser genérica, a costa de que la capa de negocios sepa algo sobre la base de datos subyacente. Quizás una solución intermedia es tener algo como ExecuteSP (string spName, parámetros params object []), luego incluir todos los nombres de SP en un archivo de configuración para que los lea la capa empresarial. Sin embargo, realmente no tengo una muy buena respuesta a esto.
Greg Jackson

Respuestas:

10

Los patrones de arquitectura de aplicaciones empresariales de Martin Fowler describen una serie de patrones relacionados con ORM, incluido el uso del objeto de consulta, que es lo que sugeriría.

Los objetos de consulta le permiten seguir el principio de Responsabilidad única, al separar la lógica de cada consulta en objetos de estrategia administrados y mantenidos individualmente. O bien su controlador puede administrar su uso directamente o delegarlo a un controlador secundario u objeto auxiliar.

¿Tendrás muchos de ellos? Ciertamente. ¿Se pueden agrupar algunos en consultas genéricas? Si de nuevo.

¿Se puede usar la inyección de dependencia para crear los objetos a partir de metadatos? Eso es lo que hacen la mayoría de las herramientas ORM.

Matthew Flynn
fuente
4

No hay una forma correcta de hacer esto. Muchas personas usan ORM para abstraer toda la complejidad. Algunos de los ORM más avanzados traducen expresiones de código en declaraciones SQL complicadas. Los ORM también tienen sus desventajas, sin embargo, para muchas aplicaciones, los beneficios superan los costos.

Si no está trabajando con un conjunto de datos masivo, lo más simple es seleccionar toda la tabla en la memoria y filtrar el código.

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

Para las aplicaciones de informes internos, este enfoque probablemente esté bien. Si el conjunto de datos es realmente grande, comenzará a necesitar muchos métodos personalizados, así como índices adecuados en su tabla.

dan
fuente
1
+ 1 para "No hay una forma correcta de hacer esto"
ozz
1
Desafortunadamente, filtrar fuera del conjunto de datos no es realmente una opción, incluso con los conjuntos de datos más pequeños con los que trabajamos, es demasiado lento. :-( Es bueno saber que otros se encuentran con mi mismo problema. :-)
Keith Palmer Jr.
@KeithPalmer por curiosidad, ¿qué tamaño tienen sus mesas?
dan
Cientos de miles de filas, si no más. Demasiados para filtrar con un rendimiento aceptable fuera de la base de datos, ESPECIALMENTE con una arquitectura distribuida donde las bases de datos no están en la misma máquina que la aplicación.
Keith Palmer Jr.
-1 para "No hay una forma correcta de hacer esto". Hay varias formas correctas. Duplicar el número de métodos cuando agrega una característica como lo estaba haciendo el OP es un enfoque sin escala, y la alternativa sugerida aquí es igualmente sin escala, solo con respecto al tamaño de la base de datos en lugar del número de características de consulta. Existen enfoques escalables, vea las otras respuestas.
Theodore Murdock
4

Algunos ORM le permiten construir consultas complejas a partir de métodos básicos. Por ejemplo

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

es una consulta perfectamente válida en el Django ORM .

La idea es que tenga algún generador de consultas (en este caso Purchase.objects ) cuyo estado interno represente información sobre una consulta. Métodos como get, filter, exclude, order_byson válidos y devuelven un nuevo generador de consultas con un estado actualizado. Estos objetos implementan una interfaz iterable, de modo que cuando itera sobre ellos, se realiza la consulta y obtiene los resultados de la consulta construida hasta ahora. Aunque este ejemplo está tomado de Django, verá la misma estructura en muchos otros ORM.

Andrea
fuente
No veo qué ventaja tiene esto sobre algo como old_purchases = Purchases.query ("date> date.today () AND type = Purchase.PRESENT AND status! = Purchase.REJECTED"); No está reduciendo la complejidad ni abstrayendo nada simplemente convirtiendo los AND y OR de SQL en métodos AND y OR, solo está cambiando la representación de los AND y OR, ¿verdad?
Keith Palmer Jr.
44
En realidad no. Está abstrayendo el SQL, lo que le brinda muchas ventajas. Primero, evitas la inyección. Luego, puede cambiar la base de datos subyacente sin preocuparse por versiones ligeramente diferentes del dialecto SQL, ya que el ORM lo maneja por usted. En muchos casos, también puede poner un backend NoSQL sin darse cuenta. En tercer lugar, estos creadores de consultas son objetos que puede pasar como cualquier otra cosa. Esto significa que su modelo puede construir la mitad de la consulta (por ejemplo, podría tener algunos métodos para los casos más comunes) y luego se puede refinar en el controlador para manejar el ..
Andrea
2
... casos más específicos. Un ejemplo típico es definir un orden predeterminado para modelos en Django. Todos los resultados de la consulta seguirán ese orden a menos que especifique lo contrario. Cuarto, si alguna vez necesita desnormalizar sus datos por razones de rendimiento, solo tiene que ajustar el ORM en lugar de reescribir todas sus consultas.
Andrea
+1 Para lenguajes de consulta dinámicos como el mencionado y LINQ.
Evan Plaice
2

Hay un tercer enfoque.

Su ejemplo específico exhibe un crecimiento exponencial en la cantidad de métodos necesarios a medida que crece la cantidad de características requeridas: queremos la capacidad de ofrecer consultas avanzadas, combinando cada función de consulta ... si lo hacemos agregando métodos, tenemos un método para un consulta básica, dos si agregamos una función opcional, cuatro si agregamos dos, ocho si agregamos tres, 2 ^ n si agregamos n funciones.

Obviamente, eso no se puede mantener más allá de tres o cuatro características, y hay un mal olor de muchos códigos estrechamente relacionados que casi se pegan entre los métodos.

Puede evitar eso agregando un objeto de datos para contener los parámetros y tener un único método que construya la consulta en función del conjunto de parámetros proporcionados (o no proporcionados). En ese caso, agregar una nueva característica como un rango de fechas es tan simple como agregar setters y getters para el rango de fechas a su objeto de datos, y luego agregar un poco de código donde se construye la consulta parametrizada:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

... y donde se agregan los parámetros a la consulta:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

Este enfoque permite el crecimiento lineal del código a medida que se agregan características, sin tener que permitir consultas arbitrarias y no parametrizadas.

Theodore Murdock
fuente
0

Creo que el consenso general es mantener el mayor acceso posible a los datos en sus modelos en MVC. Uno de los otros principios de diseño es mover algunas de sus consultas más genéricas (las que no están directamente relacionadas con su modelo) a un nivel más alto y más abstracto donde puede permitir que otros modelos también lo utilicen. (En RoR, tenemos algo llamado marco) También hay otra cosa que debe tener en cuenta y es la capacidad de mantenimiento de su código. A medida que su proyecto crezca, si tiene acceso a los datos en los controladores, será cada vez más difícil rastrearlo (actualmente estamos enfrentando este problema en un gran proyecto) Los modelos, aunque repletos de métodos, proporcionan un único punto de contacto para cualquier controlador que podría terminar haciendo preguntas desde las tablas. (Esto también puede conducir a una reutilización del código que a su vez es beneficioso)

Ricketyship
fuente
1
Ejemplo de lo que estás hablando ...?
Keith Palmer Jr.
0

La interfaz de la capa de servicio puede tener muchos métodos, pero la llamada a la base de datos puede tener solo uno.

Una base de datos tiene 4 operaciones principales

  • Insertar
  • Actualizar
  • Eliminar
  • Consulta

Otro método opcional puede ser ejecutar alguna operación de base de datos que no se encuentre dentro de las operaciones básicas de DB. Llamemos a eso Ejecutar.

Insertar y Actualizaciones se pueden combinar en una sola operación, llamada Guardar.

Muchos de sus métodos son de consulta. Por lo tanto, puede crear una interfaz genérica para satisfacer la mayoría de las necesidades inmediatas. Aquí hay una interfaz genérica de muestra:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

El objeto de transferencia de datos es genérico y contendría todos sus filtros, parámetros, ordenación, etc. La capa de datos sería responsable de analizar y extraer esto y configurar la operación en la base de datos a través de procedimientos almacenados, sql parametrizado, linq, etc. Por lo tanto, SQL no se pasa entre capas. Esto suele ser lo que hace un ORM, pero puede rodar el suyo y tener su propio mapeo.

Entonces, en tu caso tienes Widgets. Los widgets implementarían la interfaz IPOCO.

Entonces, en su modelo de capa de servicio tendría getList().

Necesitaría una capa de mapeo para manejar la getListtransformación en

Search<Widget>(DataTransferObject<Widget> Dto)

y viceversa. Como otros han mencionado, en algún momento esto se hace a través de un ORM, pero finalmente terminas con un montón de código repetitivo, especialmente si tienes cientos de tablas. El ORM crea mágicamente SQL parametizado y lo ejecuta contra la base de datos. Si realiza el suyo propio, adicionalmente en la capa de datos, se necesitarían mapeadores para configurar el SP, linq, etc. (Básicamente, el sql va a la base de datos).

Como se mencionó anteriormente, el DTO es un objeto compuesto por composición. Quizás uno de los objetos que contiene es un objeto llamado QueryParameters. Estos serían todos los parámetros para la consulta que la consulta configuraría y utilizaría. Otro objeto sería una Lista de objetos devueltos de consultas, actualizaciones, ext. Esta es la carga útil. En este caso, la carga útil sería una Lista de widgets.

Entonces, la estrategia básica es:

  • Llamadas de capa de servicio
  • Transforme la llamada de la capa de servicio a la base de datos utilizando algún tipo de repositorio / mapeo
  • Llamada de base de datos

En su caso, creo que el modelo podría tener muchos métodos, pero de manera óptima desea que la llamada a la base de datos sea genérica. Todavía terminas con un montón de código de mapeo repetitivo (especialmente con SP) o código ORM mágico que está creando dinámicamente el SQL parametizado para ti.

Jon Raynor
fuente