Reducción de repositorios a raíces agregadas

83

Actualmente tengo un repositorio para casi todas las tablas en la base de datos y me gustaría alinearme más con DDD reduciéndolos a raíces agregadas solamente.

Supongamos que tengo las siguientes tablas Usery Phone. Cada usuario puede tener uno o más teléfonos. Sin la noción de raíz agregada, podría hacer algo como esto:

//assuming I have the userId in session for example and I want to update a phone number
List<Phone> phones = PhoneRepository.GetPhoneNumberByUserId(userId);
phones[0].Number = “911”;
PhoneRepository.Update(phones[0]);

El concepto de raíces agregadas es más fácil de entender en papel que en la práctica. Nunca tendré números de teléfono que no pertenezcan a un usuario, así que ¿tendría sentido eliminar el PhoneRepository e incorporar métodos relacionados con el teléfono en el UserRepository? Suponiendo que la respuesta sea sí, voy a reescribir el ejemplo de código anterior.

¿Puedo tener un método en UserRepository que devuelva números de teléfono? O debería devolver siempre una referencia a un Usuario y luego atravesar la relación a través del Usuario para llegar a los números de teléfono:

List<Phone> phones = UserRepository.GetPhoneNumbers(userId);
// Or
User user = UserRepository.GetUserWithPhoneNumbers(userId); //this method will join to Phone

Independientemente de la forma en que adquiera los teléfonos, suponiendo que modifique uno de ellos, ¿cómo hago para actualizarlos? Mi conocimiento limitado es que los objetos debajo de la raíz deben actualizarse a través de la raíz, lo que me llevaría hacia la opción # 1 a continuación. Aunque esto funcionará perfectamente bien con Entity Framework, parece extremadamente poco descriptivo, porque al leer el código no tengo idea de lo que estoy actualizando, aunque Entity Framework mantiene la pestaña de los objetos modificados dentro del gráfico.

UserRepository.Update(user);
// Or
UserRepository.UpdatePhone(phone);

Por último, en el supuesto tengo varias tablas de búsqueda que no son realmente vinculados a cualquier cosa, como por ejemplo CountryCodes, ColorsCodes, SomethingElseCodes. Podría usarlos para completar menús desplegables o por cualquier otra razón. ¿Son estos repositorios independientes? ¿Se pueden combinar en algún tipo de agrupación / repositorio lógico como CodesRepository? ¿O va en contra de las mejores prácticas?

e36M3
fuente
2
De hecho, una muy buena pregunta, que he estado luchando mucho conmigo mismo. Parece uno de esos puntos de compensación en los que no existe una solución "correcta". Si bien las respuestas disponibles en el momento en que escribo esto son buenas y cubren la mayoría de los problemas, no creo que brinden ninguna solución "final" .. :(
cwap
Te escucho, no hay límite para lo cerca que se puede llegar a la solución "correcta". Supongo que tenemos que cumplir con nuestro mejor
esfuerzo
+1 - Yo también estoy luchando con esto. Antes tenía un repositorio y una capa de servicio separados para cada tabla. Comencé a combinar estos donde tenía sentido, pero luego terminé con un repositorio y una capa de servicio con más de 1k líneas de código. En mi último segmento de aplicación, hice una pequeña copia de seguridad para poner solo conceptos estrechamente relacionados en la misma capa de repositorio / servicio, incluso si ese elemento es dependiente. Por ejemplo, para un blog, estaba agregando comentarios al agregado del repositorio de publicaciones, pero ahora los he separado para separar el repositorio / servicio de comentarios.
jpshook

Respuestas:

12

Se le permite tener cualquier método que desee en su repositorio :) En los dos casos que menciona, tiene sentido devolver al usuario con la lista de teléfonos completa. Normalmente, el objeto de usuario no se completará completamente con toda la información secundaria (por ejemplo, todas las direcciones, números de teléfono) y es posible que tengamos diferentes métodos para obtener el objeto de usuario con diferentes tipos de información. Esto se conoce como carga diferida.

User GetUserDetailsWithPhones()
{
    // Populate User along with Phones
}

Para la actualización, en este caso, se actualiza al usuario, no al número de teléfono en sí. El modelo de almacenamiento puede almacenar los teléfonos en una tabla diferente y de esa manera puede pensar que solo los teléfonos se están actualizando, pero ese no es el caso si lo piensa desde la perspectiva de DDD. En cuanto a la legibilidad, mientras que la línea

UserRepository.Update(user)

por sí solo no transmite lo que se está actualizando, el código anterior dejaría en claro lo que se está actualizando. Además, lo más probable es que sea parte de una llamada al método de interfaz que puede significar lo que se está actualizando.

Para las tablas de búsqueda, e incluso de otra manera, es útil tener GenericRepository y usarlo. El repositorio personalizado puede heredar del GenericRepository.

public class UserRepository : GenericRepository<User>
{
    IEnumerable<User> GetUserByCustomCriteria()
    {
    }

    User GetUserDetailsWithPhones()
    {
        // Populate User along with Phones
    }

    User GetUserDetailsWithAllSubInfo()
    {
        // Populate User along with all sub information e.g. phones, addresses etc.
    }
}

Busque Generic Repository Entity Framework y encontrará muchas buenas implementaciones. Utilice uno de esos o escriba el suyo propio.

amit_g
fuente
@amit_g, gracias por la información. Ya utilizo un repositorio genérico / básico del que heredan todos los demás. Mi idea para una agrupación lógica de tablas de "búsqueda" en un repositorio era simplemente ahorrar tiempo y reducir el número de repositorios. Entonces, en lugar de crear ColorCodeRepository y AnotherCodeRepository, simplemente crearía CodesRepository.GetColorCodes () y CodesRepository.GetAnotherCodes (). Pero no estoy seguro de si una agrupación lógica de entidades no relacionadas en un repositorio es una mala práctica.
e36M3
Además, luego está confirmando que, según las reglas de DDD, los métodos en un repositorio correspondiente a la raíz deberían devolver la raíz y no las entidades subyacentes dentro del gráfico. Entonces, en mi ejemplo, cualquier método en el UserRepository solo podría devolver el tipo de usuario, independientemente de cómo se vea el resto del gráfico (o la parte del gráfico que realmente me interesa, como Direcciones o Teléfonos).
e36M3
CodesRepository está bien, pero sería difícil mantener consistentemente lo que pertenece a eso. Lo mismo puede cumplirse simplemente con GenericRepository <ColorCodes> GetAll (). Dado que GenericRepository solo tendría métodos muy genéricos (GetAll, GetByID, etc.), funcionaría bien para las tablas de búsqueda.
amit_g
2
Desafortunadamente, esta respuesta es incorrecta. El repositorio debe tratarse como una colección de objetos en memoria y debe evitar la carga diferida. Aquí hay un buen artículo sobre ese besnikgeek.blogspot.com/2010/07/…
Rafał Łużyński
9

Su ejemplo en el repositorio Aggregate Root está perfectamente bien, es decir, cualquier entidad que no pueda existir razonablemente sin depender de otra no debería tener su propio repositorio (en su caso, Teléfono). Sin esta consideración, puede encontrarse rápidamente con una explosión de repositorios en una asignación 1-1 a tablas de base de datos.

Debería considerar el uso del patrón de Unidad de trabajo para los cambios de datos en lugar de los repositorios en sí, ya que creo que le están causando cierta confusión sobre la intención cuando se trata de persistir los cambios en la base de datos. En una solución EF, la Unidad de trabajo es esencialmente un contenedor de interfaz alrededor de su contexto EF.

Con respecto a su repositorio de datos de búsqueda, simplemente creamos un ReferenceDataRepository que se hace responsable de los datos que no pertenecen específicamente a una entidad de dominio (países, colores, etc.).

Darren Lewis
fuente
1
gracias. ¿No estoy seguro de cómo Unit of Work reemplaza al repositorio? Ya empleo UOW en el sentido de que habrá una única llamada SaveChanges () al contexto de Entity Framework al final de cada transacción comercial (final de la solicitud HTTP). Sin embargo, todavía paso por los repositorios (que albergan el contexto EF) para acceder a los datos. Como UserRepository.Delete (usuario) y UserRepository.Add (usuario).
e36M3
5

Si el teléfono no tiene sentido sin el usuario, es una entidad (si le importa su identidad) u objeto de valor y siempre debe modificarse a través del usuario y recuperarse / actualizarse juntos.

Piense en las raíces agregadas como definidores de contexto: dibujan contextos locales pero están en el contexto global (su aplicación) ellos mismos.

Si sigue un diseño basado en dominios, se supone que los repositorios son 1: 1 por raíces agregadas.
No hay excusas.

Apuesto a que estos son problemas que estás enfrentando:

  • Dificultades técnicas - desajuste de impedancia de relación de objeto. Está luchando con la persistencia de gráficos de objetos completos con facilidad y el tipo de marco de entidad a no ayuda.
  • El modelo de dominio está centrado en los datos (en oposición al comportamiento centrado). debido a eso - Pierdes el conocimiento sobre la jerarquía de objetos (contextos mencionados anteriormente) y mágicamente todo se convierte en una raíz agregada.

No estoy seguro de cómo solucionar el primer problema, pero he notado que solucionar el segundo soluciona el primero lo suficientemente bien. Para entender lo que quiero decir con centrado en el comportamiento, pruebe este artículo .

Ps Reducir el repositorio a raíz agregada no tiene sentido.
Pps Evitar "CodeRepositories". Eso conduce a un código de procedimiento centrado en datos.
Ppps Evite el patrón de unidad de trabajo. Las raíces agregadas deben definir los límites de las transacciones.

Arnis Lapsa
fuente
1
Como el enlace al documento ya no está activo, utilice este en su lugar: web.archive.org/web/20141021055503/http://www.objectmentor.com/…
JwJosefy
3

Esta es una pregunta antigua, pero se cree que vale la pena publicar una solución simple.

  1. EF Context ya le brinda tanto Unit of Work (seguimiento de cambios) como Repositories (referencia en memoria a cosas de DB). No es obligatorio realizar más abstracciones.
  2. Elimine el DBSet de su clase de contexto, ya que Phone no es una raíz agregada.
  3. En su lugar, utilice la propiedad de navegación 'Teléfonos' en Usuario.

static void updateNumber (int userId, string oldNumber, string newNumber)

static void updateNumber(int userId, string oldNumber, string newNumber)
    {
        using (MyContext uow = new MyContext()) // Unit of Work
        {
            DbSet<User> repo = uow.Users; // Repository
            User user = repo.Find(userId); 
            Phone oldPhone = user.Phones.Where(x => x.Number.Trim() == oldNumber).SingleOrDefault();
            oldPhone.Number = newNumber;
            uow.SaveChanges();
        }

    }
Gredoso
fuente
La abstracción no es obligatoria, pero se recomienda. Entity Framework sigue siendo solo un proveedor y parte de la infraestructura. Ni siquiera se trata solo de lo que sucedería si el proveedor cambiara, sino que en sistemas más grandes es posible que tenga varios tipos de proveedores que persisten diferentes conceptos de dominio en diferentes medios de persistencia. Este es el tipo de abstracción que es extremadamente fácil de hacer al principio, pero doloroso de refactorizar con el tiempo y la complejidad suficientes.
Joseph Ferris
1
Me ha resultado muy difícil retener los beneficios del ORM de EF (por ejemplo, carga diferida, consultables) cuando intento abstraerme de una interfaz de repositorio.
Chalky
Es una discusión interesante, sin duda. Dado que la carga diferida es muy específica de la implementación, encuentro que su valor se limita a la infraestructura (entrada y salida de objetos de dominio con traducción limitada por capas). Muchas de las implementaciones que he visto tienen problemas cuando se intenta una abstracción genérica. Tiendo a ir con una implementación explícita, porque los métodos genéricos tienen muy poco valor de dominio. EF hace que los consultables sean altamente utilizables, pero el problema se convierte en el rol del repositorio, es decir, los repositorios utilizados por los controladores pierden los beneficios de la abstracción.
Joseph Ferris
0

Si una entidad de teléfono solo tiene sentido junto con un usuario raíz agregado, entonces también creo que tiene sentido que la operación para agregar un nuevo registro de teléfono sea responsabilidad del objeto de dominio de usuario a través de un método específico (comportamiento DDD) y eso podría tiene perfectamente sentido por varias razones, la razón inmediata es que debemos verificar que el objeto Usuario existe, ya que la entidad Teléfono depende de su existencia y tal vez mantener un bloqueo de transacción mientras se realizan más verificaciones de validación para garantizar que ningún otro proceso haya eliminado el agregado raíz antes hemos terminado de validar la operación. En otros casos, con otros tipos de agregados raíz, es posible que desee agregar o calcular algún valor y mantenerlo en las propiedades de columna del agregado raíz para un procesamiento más eficiente por otras operaciones más adelante.

Además, si desea utilizar métodos que recuperen todos los teléfonos independientemente de los usuarios que los posean, aún puede hacerlo a través del repositorio de usuarios, solo necesita un método que devuelva todos los usuarios como IQueryable, luego puede asignarlos para obtener todos los teléfonos de los usuarios y hacer consulta con eso. Así que ni siquiera necesitas un PhoneRepository en este caso. Además, prefiero usar una clase con método de extensiones para IQueryable que pueda usar en cualquier lugar, no solo desde una clase de Repositorio, si quisiera abstraer consultas detrás de métodos.

Solo una advertencia para poder eliminar entidades de teléfono usando solo el objeto de dominio y no un repositorio de teléfono, debe asegurarse de que el UserId sea parte de la clave principal del teléfono o, en otras palabras, la clave principal de un registro de teléfono es una clave compuesta compuesto por UserId y alguna otra propiedad (sugiero una identidad generada automáticamente) en la entidad Phone. Esto tiene sentido intuitivamente ya que el registro del teléfono es "propiedad" del registro del usuario y su eliminación de la colección de navegación del usuario equivaldría a su eliminación completa de la base de datos.

Kaveh Hadjari
fuente