Pensé que me tomaría un buen rato responder mi propia pregunta. Lo que sigue es solo una forma de resolver los problemas 1-3 en mi pregunta original.
Descargo de responsabilidad: no siempre puedo usar los términos correctos al describir patrones o técnicas. Lo siento por eso.
Los objetivos:
- Cree un ejemplo completo de un controlador básico para ver y editar
Users
.
- Todo el código debe ser totalmente comprobable y simulable.
- El controlador no debe tener idea de dónde se almacenan los datos (lo que significa que se pueden cambiar).
- Ejemplo para mostrar una implementación de SQL (más común).
- Para obtener el máximo rendimiento, los controladores solo deben recibir los datos que necesitan, sin campos adicionales.
- La implementación debería aprovechar algún tipo de mapeador de datos para facilitar el desarrollo.
- La implementación debe tener la capacidad de realizar búsquedas complejas de datos.
La solución
Estoy dividiendo mi interacción de almacenamiento persistente (base de datos) en dos categorías: R (Leer) y CUD (Crear, Actualizar, Eliminar). Mi experiencia ha sido que las lecturas son realmente lo que hace que una aplicación se ralentice. Y aunque la manipulación de datos (CUD) es en realidad más lenta, ocurre con mucha menos frecuencia y, por lo tanto, es mucho menos preocupante.
CUD (Crear, Actualizar, Eliminar) es fácil. Esto implicará trabajar con modelos reales , que luego se pasan a mi Repositories
para persistencia. Tenga en cuenta que mis repositorios seguirán proporcionando un método de lectura, pero simplemente para la creación de objetos, no para mostrar. Más sobre eso más tarde.
R (Leer) no es tan fácil. No hay modelos aquí, solo objetos de valor . Use matrices si lo prefiere . Estos objetos pueden representar un modelo único o una combinación de muchos modelos, cualquier cosa realmente. No son muy interesantes por sí mismos, pero sí cómo se generan. Estoy usando lo que estoy llamando Query Objects
.
El código:
Modelo de usuario
Comencemos de manera simple con nuestro modelo de usuario básico. Tenga en cuenta que no hay extensión de ORM o material de base de datos en absoluto. Solo pura gloria modelo. Agregue sus captadores, establecedores, validación, lo que sea.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Interfaz de repositorio
Antes de crear mi repositorio de usuario, quiero crear mi interfaz de repositorio. Esto definirá el "contrato" que los repositorios deben seguir para que mi controlador pueda utilizarlos. Recuerde, mi controlador no sabrá dónde se almacenan realmente los datos.
Tenga en cuenta que mis repositorios solo contendrán estos tres métodos. El save()
método es responsable de crear y actualizar usuarios, simplemente dependiendo de si el objeto de usuario tiene o no un conjunto de identificación.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Implementación del repositorio de SQL
Ahora para crear mi implementación de la interfaz. Como se mencionó, mi ejemplo iba a ser con una base de datos SQL. Tenga en cuenta el uso de un mapeador de datos para evitar tener que escribir consultas SQL repetitivas.
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
Consultar interfaz de objeto
Ahora con CUD (Crear, Actualizar, Eliminar) atendido por nuestro repositorio, podemos centrarnos en la R (Leer). Los objetos de consulta son simplemente una encapsulación de algún tipo de lógica de búsqueda de datos. Son no generadores de consultas. Al abstraerlo como nuestro repositorio podemos cambiar su implementación y probarlo más fácilmente. Un ejemplo de un objeto de consulta podría ser un AllUsersQuery
o AllActiveUsersQuery
, o incluso MostCommonUserFirstNames
.
Puede estar pensando "¿no puedo simplemente crear métodos en mis repositorios para esas consultas?" Sí, pero aquí es por qué no estoy haciendo esto:
- Mis repositorios están diseñados para trabajar con objetos modelo. En una aplicación del mundo real, ¿por qué necesitaría obtener el
password
campo si estoy buscando una lista de todos mis usuarios?
- Los repositorios a menudo son específicos del modelo, pero las consultas a menudo involucran más de un modelo. Entonces, ¿en qué repositorio pones tu método?
- Esto mantiene mis repositorios muy simples, no una clase hinchada de métodos.
- Todas las consultas ahora están organizadas en sus propias clases.
- Realmente, en este punto, existen repositorios simplemente para abstraer mi capa de base de datos.
Para mi ejemplo, crearé un objeto de consulta para buscar "AllUsers". Aquí está la interfaz:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Implementación de objeto de consulta
Aquí es donde podemos usar un mapeador de datos nuevamente para ayudar a acelerar el desarrollo. Tenga en cuenta que estoy permitiendo un ajuste al conjunto de datos devuelto: los campos. Esto es lo más lejos que quiero llegar manipulando la consulta realizada. Recuerde, mis objetos de consulta no son constructores de consultas. Simplemente realizan una consulta específica. Sin embargo, como sé que probablemente usaré este mucho, en varias situaciones diferentes, me estoy dando la capacidad de especificar los campos. ¡Nunca quiero devolver campos que no necesito!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
Antes de pasar al controlador, quiero mostrar otro ejemplo para ilustrar cuán poderoso es esto. Tal vez tengo un motor de informes y necesito crear un informe para AllOverdueAccounts
. Esto podría ser complicado con mi mapeador de datos, y es posible que desee escribir algo real SQL
en esta situación. No hay problema, así es como podría verse este objeto de consulta:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
Esto mantiene muy bien toda mi lógica para este informe en una clase, y es fácil de probar. Puedo burlarme del contenido de mi corazón, o incluso usar una implementación completamente diferente.
El controlador
Ahora la parte divertida: unir todas las piezas. Tenga en cuenta que estoy usando inyección de dependencia. Por lo general, las dependencias se inyectan en el constructor, pero en realidad prefiero inyectarlas directamente en mis métodos de controlador (rutas). Esto minimiza el gráfico de objetos del controlador, y en realidad lo encuentro más legible. Tenga en cuenta que si no le gusta este enfoque, simplemente use el método de constructor tradicional.
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
Pensamientos finales:
Lo importante a tener en cuenta aquí es que cuando estoy modificando (creando, actualizando o eliminando) entidades, estoy trabajando con objetos de modelos reales y realizando la persistencia a través de mis repositorios.
Sin embargo, cuando estoy visualizando (seleccionando datos y enviándolos a las vistas) no estoy trabajando con objetos modelo, sino más bien con objetos de valor antiguo. Solo selecciono los campos que necesito y está diseñado para que pueda maximizar el rendimiento de mi búsqueda de datos.
Mis repositorios se mantienen muy limpios y, en cambio, este "desorden" se organiza en mis consultas modelo.
Utilizo un mapeador de datos para ayudar con el desarrollo, ya que es ridículo escribir SQL repetitivo para tareas comunes. Sin embargo, absolutamente puede escribir SQL donde sea necesario (consultas complicadas, informes, etc.). Y cuando lo haces, está bien escondido en una clase con el nombre adecuado.
¡Me encantaría escuchar tu opinión sobre mi enfoque!
Actualización de julio de 2015:
Me han preguntado en los comentarios dónde terminé con todo esto. Bueno, no tan lejos en realidad. A decir verdad, todavía no me gustan los repositorios. Los encuentro excesivos para búsquedas básicas (especialmente si ya está usando un ORM), y desordenados al trabajar con consultas más complicadas.
Por lo general, trabajo con un ORM de estilo ActiveRecord, por lo que a menudo solo haré referencia a esos modelos directamente en toda mi aplicación. Sin embargo, en situaciones donde tengo consultas más complejas, usaré objetos de consulta para hacerlos más reutilizables. También debo tener en cuenta que siempre inyecto mis modelos en mis métodos, lo que hace que sean más fáciles de burlar en mis pruebas.
new Query\ComplexUserLookup($username, $anotherCondition)
. O haga esto a través de métodos setter$query->setUsername($username);
. Realmente puede diseñar esto, sin embargo, tiene sentido para su aplicación particular, y creo que los objetos de consulta dejan mucha flexibilidad aquí.Según mi experiencia, aquí hay algunas respuestas a sus preguntas:
P: ¿Cómo nos ocupamos de recuperar los campos que no necesitamos?
R: Desde mi experiencia, esto realmente se reduce a tratar con entidades completas versus consultas ad-hoc.
Una entidad completa es algo así como un
User
objeto. Tiene propiedades y métodos, etc. Es un ciudadano de primera clase en su código base.Una consulta ad-hoc devuelve algunos datos, pero no sabemos nada más allá de eso. A medida que los datos pasan por la aplicación, se hacen sin contexto. Es un
User
? AUser
con algunaOrder
información adjunta? Realmente no lo sabemos.Prefiero trabajar con entidades completas.
Tiene razón en que a menudo traerá datos que no usará, pero puede abordar esto de varias maneras:
User
para el back-end y quizás unUserSmall
para llamadas AJAX. Uno podría tener 10 propiedades y uno tiene 3 propiedades.Las desventajas de trabajar con consultas ad-hoc:
User
, terminarás escribiendo esencialmente lo mismoselect *
para muchas llamadas. Una llamada obtendrá 8 de 10 campos, una obtendrá 5 de 10, una obtendrá 7 de 10. ¿Por qué no reemplazar todas con una llamada que obtenga 10 de 10? La razón por la que esto es malo es que es un asesinato re-factorizar / probar / burlarse.User
tan lento?" terminas rastreando consultas únicas y las correcciones de errores tienden a ser pequeñas y localizadas.P: Tendré demasiados métodos en mi repositorio.
R: Realmente no he visto otra solución que no sea consolidar llamadas. Las llamadas de método en su repositorio realmente se asignan a las características de su aplicación. Cuantas más funciones, más llamadas específicas de datos. Puede retrasar las funciones e intentar fusionar llamadas similares en una.
La complejidad al final del día tiene que existir en alguna parte. Con un patrón de repositorio lo hemos introducido en la interfaz del repositorio en lugar de tal vez hacer un montón de procedimientos almacenados.
A veces tengo que decirme a mí mismo: "¡Bueno, tenía que ceder en alguna parte! No hay balas de plata".
fuente
SELECT *
, sino que solo selecciono los campos que necesita. Por ejemplo, vea esta pregunta . En cuanto a todas esas consultas publicitarias de las que hablas, ciertamente entiendo de dónde vienes. Tengo una aplicación muy grande en este momento que tiene muchas de ellas. Ese fue mi "¡Bueno, tenía que ceder en alguna parte!" Por el momento, opté por el máximo rendimiento. Sin embargo, ahora estoy lidiando con MUCHAS consultas diferentes.reads
menudo surgen problemas de rendimiento, podría utilizar un enfoque de consulta más personalizado para ellos, que no se traduzca en objetos comerciales reales. Entonces, paracreate
,update
ydelete
, utilizar un ORM, que trabaja con objetos completos. ¿Alguna idea sobre ese enfoque?Yo uso las siguientes interfaces:
Repository
- carga, inserta, actualiza y elimina entidadesSelector
- encuentra entidades basadas en filtros, en un repositorioFilter
- encapsula la lógica de filtradoMi
Repository
es agnóstico de base de datos; de hecho no especifica ninguna persistencia; podría ser cualquier cosa: base de datos SQL, archivo xml, servicio remoto, un extraterrestre del espacio exterior, etc. Para las capacidades de búsqueda, lasRepository
construccionesSelector
se pueden filtrarLIMIT
, clasificar, clasificar y contar. Al final, el selector obtiene uno o másEntities
de la persistencia.Aquí hay un código de muestra:
Entonces, una implementación:
La idea es que los
Selector
usos genéricosFilter
pero losSqlSelector
usos de implementaciónSqlFilter
; el seSqlSelectorFilterAdapter
adapta un genéricoFilter
a un concretoSqlFilter
.El código del cliente crea
Filter
objetos (que son filtros genéricos) pero en la implementación concreta del selector esos filtros se transforman en filtros SQL.Otras implementaciones de selector, como
InMemorySelector
, transformar deFilter
queInMemoryFilter
el uso de su específicaInMemorySelectorFilterAdapter
; entonces, cada implementación de selector viene con su propio adaptador de filtro.Al usar esta estrategia, mi código de cliente (en la capa de negocios) no se preocupa por un repositorio específico o implementación de selector.
PD: esta es una simplificación de mi código real
fuente
Agregaré un poco sobre esto ya que actualmente estoy tratando de comprender todo esto yo mismo.
# 1 y 2
Este es un lugar perfecto para que su ORM haga el trabajo pesado. Si está utilizando un modelo que implementa algún tipo de ORM, puede usar sus métodos para encargarse de estas cosas. Haga su propio pedido Por funciones que implementan los métodos Eloquent si es necesario. Usando Eloquent por ejemplo:
Lo que parece estar buscando es un ORM. No hay razón para que su repositorio no pueda basarse en uno. Esto requeriría que el Usuario extienda elocuente, pero personalmente no lo veo como un problema.
Sin embargo, si desea evitar un ORM, tendrá que "rodar el suyo" para obtener lo que está buscando.
# 3
No se supone que las interfaces sean requisitos difíciles y rápidos. Algo puede implementar una interfaz y agregarle. Lo que no puede hacer es no implementar una función requerida de esa interfaz. También puede extender interfaces como clases para mantener las cosas SECAS.
Dicho esto, estoy empezando a comprender, pero estas realizaciones me han ayudado.
fuente
Solo puedo comentar sobre la forma en que (en mi empresa) tratamos esto. En primer lugar, el rendimiento no es un gran problema para nosotros, pero tener un código limpio / adecuado sí lo es.
En primer lugar, definimos modelos como un
UserModel
que usa un ORM para crearUserEntity
objetos. Cuando aUserEntity
se carga desde un modelo, se cargan todos los campos. Para los campos que hacen referencia a entidades foráneas, utilizamos el modelo foráneo apropiado para crear las entidades respectivas. Para esas entidades, los datos se cargarán a pedido. Ahora tu reacción inicial podría ser ... ??? ... !!! déjame darte un ejemplo un poco de ejemplo:En nuestro caso
$db
es un ORM que puede cargar entidades. El modelo indica al ORM que cargue un conjunto de entidades de un tipo específico. El ORM contiene una asignación y la usa para inyectar todos los campos de esa entidad en la entidad. Sin embargo, para campos foráneos solo se cargan los id de esos objetos. En este caso,OrderModel
creaOrderEntity
s con solo los id de los pedidos referenciados. CuandoPersistentEntity::getField
es llamado por laOrderEntity
entidad, le indica a su modelo que cargue lentamente todos los campos en elOrderEntity
s. Todos losOrderEntity
correos electrónicos asociados con una UserEntity se tratan como un conjunto de resultados y se cargarán a la vez.La magia aquí es que nuestro modelo y ORM inyectan todos los datos en las entidades y que las entidades simplemente proporcionan funciones envolventes para el
getField
método genérico suministrado porPersistentEntity
. Para resumir, siempre cargamos todos los campos, pero los campos que hacen referencia a una entidad extranjera se cargan cuando es necesario. Solo cargar un montón de campos no es realmente un problema de rendimiento. Cargar todas las entidades extranjeras posibles, sin embargo, sería una GRAN disminución del rendimiento.Ahora a cargar un conjunto específico de usuarios, basado en una cláusula where. Proporcionamos un paquete de clases orientado a objetos que le permite especificar expresiones simples que se pueden pegar. En el código de ejemplo lo llamé
GetOptions
. Es un contenedor para todas las opciones posibles para una consulta de selección. Contiene una colección de cláusulas where, un grupo por cláusula y todo lo demás. Nuestras cláusulas where son bastante complicadas, pero obviamente podrías hacer una versión más simple fácilmente.Una versión más simple de este sistema sería pasar la parte WHERE de la consulta como una cadena directamente al modelo.
Lamento esta respuesta bastante complicada. Traté de resumir nuestro marco lo más rápido y claro posible. Si tiene alguna pregunta adicional, no dude en hacerla y actualizaré mi respuesta.
EDITAR: además, si realmente no desea cargar algunos campos de inmediato, puede especificar una opción de carga diferida en su mapeo ORM. Debido a que todos los campos finalmente se cargan a través del
getField
método, podría cargar algunos campos en el último minuto cuando se llama a ese método. Este no es un problema muy grande en PHP, pero no lo recomendaría para otros sistemas.fuente
Estas son algunas soluciones diferentes que he visto. Hay pros y contras para cada uno de ellos, pero es decisión tuya.
Problema 1: demasiados campos
Este es un aspecto importante, especialmente cuando tiene en cuenta los escaneos de solo índice . Veo dos soluciones para lidiar con este problema. Puede actualizar sus funciones para incluir un parámetro de matriz opcional que contendría una lista de columnas para devolver. Si este parámetro está vacío, devolverá todas las columnas de la consulta. Esto puede ser un poco raro; basado en el parámetro, podría recuperar un objeto o una matriz. También podría duplicar todas sus funciones para tener dos funciones distintas que ejecuten la misma consulta, pero una devuelve una matriz de columnas y la otra devuelve un objeto.
Problema # 2: demasiados métodos
Trabajé brevemente con Propel ORM hace un año y esto se basa en lo que puedo recordar de esa experiencia. Propel tiene la opción de generar su estructura de clase basada en el esquema de base de datos existente. Crea dos objetos para cada tabla. El primer objeto es una larga lista de funciones de acceso similares a las que ha enumerado actualmente;
findByAttribute($attribute_value)
. El siguiente objeto hereda de este primer objeto. Puede actualizar este objeto secundario para incorporar sus funciones getter más complejas.Otra solución sería utilizar
__call()
para asignar funciones no definidas a algo procesable. Su__call
método sería capaz de analizar findById y findByName en diferentes consultas.Espero que esto ayude al menos algo.
fuente
Sugiero https://packagist.org/packages/prettus/l5-repository como proveedor para implementar repositorios / criterios, etc. en Laravel5: D
fuente
Estoy de acuerdo con @ ryan1234 en que debe pasar objetos completos dentro del código y usar métodos de consulta genéricos para obtener esos objetos.
Para uso externo / punto final, me gusta mucho el método GraphQL.
fuente
Mi instinto me dice que tal vez requiera una interfaz que implemente métodos optimizados de consulta junto con métodos genéricos. Las consultas sensibles al rendimiento deben tener métodos específicos, mientras que las consultas poco frecuentes o livianas son manejadas por un controlador genérico, tal vez a costa del controlador haciendo un poco más de malabarismo.
Los métodos genéricos permitirían implementar cualquier consulta y, por lo tanto, evitarían cambios importantes durante un período de transición. Los métodos específicos le permiten optimizar una llamada cuando tiene sentido y puede aplicarse a múltiples proveedores de servicios.
Este enfoque sería similar a las implementaciones de hardware que realizan tareas optimizadas específicas, mientras que las implementaciones de software hacen el trabajo ligero o la implementación flexible.
fuente
Creo que GraphQL es un buen candidato en este caso para proporcionar un lenguaje de consulta a gran escala sin aumentar la complejidad de los repositorios de datos.
Sin embargo, hay otra solución si no quieres utilizar GraphQL por ahora. Al usar un DTO donde se usa un objeto para transportar los datos entre procesos, en este caso entre el servicio / controlador y el repositorio.
Ya se proporcionó una respuesta elegante anteriormente, sin embargo, intentaré dar otro ejemplo que creo que es más simple y podría servir como punto de partida para un nuevo proyecto.
Como se muestra en el código, solo necesitaríamos 4 métodos para las operaciones CRUD. el
find
método se usaría para enumerar y leer pasando argumentos de objeto. Los servicios de back-end podrían construir el objeto de consulta definido en función de una cadena de consulta URL o en función de parámetros específicos.El objeto de consulta (
SomeQueryDto
) también podría implementar una interfaz específica si es necesario. y es fácil de extender más tarde sin agregar complejidad.Ejemplo de uso:
fuente