108 votos

Diseño del modelo de repositorio adecuado en PHP?

Prefacio: voy a intentar usar el repositorio de patrones en una arquitectura MVC con bases de datos relacionales.

Recientemente he empezado a aprender TDD en PHP, y me estoy dando cuenta de que mi base de datos se acopla demasiado estrecha colaboración con el resto de mi solicitud. He leído acerca de los repositorios, y el uso de un contenedor de IoC "inyectar" en mi controladores. Muy interesante. Pero ahora tenemos algunas preguntas prácticas sobre el diseño del repositorio. Considere el siguiente ejemplo.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Problema #1: Demasiados campos

Todos estos métodos de búsqueda utiliza un seleccionar todos los campos (SELECT *). Sin embargo, en mis aplicaciones siempre estoy tratando de limitar el número de campos que puedo obtener, ya que esto a menudo implica una sobrecarga y ralentiza las cosas. Para aquellos que utilizan este patrón, ¿cómo lidiar con esto?

Problema #2: Demasiados métodos

Mientras esta clase se ve bien ahora, yo sé que en un mundo real de la aplicación necesito mucho más métodos. Por ejemplo:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Etc.

Como se puede ver, no puede ser muy, muy larga lista de métodos posibles. Y, a continuación, si se añade en el campo de selección de la edición anterior, el problema se agrava. En el pasado yo normalmente sólo hay que poner todo esto la derecha de la lógica en mi controlador:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')->byCountry('Canada')->orderBy('name')->rows()

        return View::make('users', array('users' => $users))
    }

}

Con mi repositorio de enfoque, yo no quiero terminar con esto:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Problema #3: Imposible para que coincida con una interfaz

Veo el beneficio en el uso de interfaces para los repositorios, así que puedo cambiar mi aplicación (para propósitos de prueba u otros). Mi comprensión de interfaces que definen un contrato por el que una aplicación debe seguir. Esto es genial hasta que comience a agregar métodos adicionales a los repositorios como findAllInCountry(). Ahora necesito actualizar mi interfaz también de disponer de este método, de lo contrario otras implementaciones no la puede tener, y que podría romper mi solicitud. Por esto se siente loco...un caso de la cola que menea al perro.

Especificación De Patrón?

Esto me lleva a creer que el repositorio sólo debe tener un número fijo de métodos (como save(), remove(), find(), findAll(), etc.). Pero entonces ¿cómo puedo ejecutar determinadas búsquedas? He escuchado acerca de la Especificación de Patrón, pero a mí me parece que esto sólo reduce todo un conjunto de registros (via IsSatisfiedBy()), que claramente tiene problemas importantes de rendimiento si usted está tirando de una base de datos.

Ayuda?

Es evidente que la necesidad de repensar un poco las cosas cuando se trabaja con los repositorios. ¿Alguien puede iluminar sobre cómo se manejan mejor?

87voto

Jonathan Puntos 2381

Pensé en tomar una grieta en responder a mi propia pregunta. Lo que sigue es sólo una manera de resolver los problemas 1-3 en mi pregunta original.

Descargo de responsabilidad: yo no puede utilizar siempre el derecho de los términos para la descripción de patrones o técnicas. Lo siento por eso.

Los Objetivos:

  • Crear un ejemplo completo de un controlador básico para la visualización y edición de Users.
  • Todo el código debe estar totalmente comprobables y mockable.
  • El controlador debe tener ni idea de dónde se almacenan los datos (lo que significa que puede ser cambiado).
  • Ejemplo para mostrar una implementación de SQL (la más común).
  • Para obtener el máximo rendimiento, los controladores deben recibir únicamente los datos que necesita, ni de los campos adicionales.
  • La aplicación debe aprovechar algún tipo de datos del asignador para facilitar su desarrollo.
  • La aplicación debe tener la capacidad de llevar a cabo complejas de datos de búsquedas.

La Solución

Estoy dividiendo mi persistente de almacenamiento (base de datos) la interacción en dos categorías: R (Lectura) y de la COALICIÓN (Crear, Actualizar, Eliminar). Mi experiencia ha sido que lee son realmente lo que hace que una aplicación para frenar. Y mientras manipulación de datos (CUD) es realmente más lento, esto ocurre con mucha menos frecuencia, y es por lo tanto mucho menos de una preocupación.

CUD (Crear, Actualizar, Eliminar) es muy fácil. Esto implica trabajar con los modelos, que están a continuación, se pasa a mis Repositories de persistencia. Tenga en cuenta que mi repositorios proporcionará un método de Lectura, sino, simplemente, para la creación de objetos, no de la pantalla. Más sobre esto más adelante.

R (Read) no es tan fácil. No hay modelos de aquí, sólo objetos de valor. El uso de matrices si lo prefiere. Estos objetos pueden representar un modelo o una mezcla de muchos modelos, cualquier cosa realmente. Estos no son muy interesantes por su propia cuenta, sino cómo se generan. Estoy usando lo que yo estoy llamando Query Objects.

El Código:

Modelo De Usuario

Vamos a comenzar por lo más fácil con nuestra base de modelo de usuario. Nota que no es un ORM, la ampliación o la base de datos de cosas en todos. Sólo pura modelo de gloria. Agregue sus getters, setters, validación, lo que sea.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

Repositorio De Interfaz

Antes de crear mi repositorio de usuarios, quiero crear mi repositorio de interfaz. Esto va a definir el "contrato" que los repositorios deben seguir para ser utilizado por mi controlador. Recuerde, mi controlador no se sabe donde los datos se almacenan realmente.

Tenga en cuenta que mi repositorios sólo cada contienen estos tres métodos. La save() método es responsable de la creación y actualización de los usuarios, simplemente dependiendo de si o no el objeto de usuario tiene un id de conjunto.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

SQL Repositorio de la Aplicación

Ahora a crear mi implementación de la interfaz. Como se ha mencionado, mi ejemplo de que iba a ser con una base de datos SQL. Nota el uso de un data mapper para evitar tener que escribir repetitivo de consultas SQL.

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');
    }
}

Consulta De Objetos De Interfaz

Ahora con la CUD (Crear, Actualizar, Eliminar) al cuidado de nuestro repositorio, podemos centrarnos en el R (Read). Consulta los objetos son simplemente una encapsulación de algún tipo de búsqueda de datos lógica. Ellos son los que no se consulta a los constructores. Mediante la abstracción como nuestro repositorio podemos cambiar es la implementación y prueba de ello es más fácil. Un ejemplo de un Objeto de Consulta podría ser un AllUsersQuery o AllActiveUsersQuery, o incluso, MostCommonUserFirstNames.

Usted puede estar pensando "no puedo crear métodos en mis repositorios para esas consultas?" Sí, pero aquí es ¿por qué no voy a hacer esto:

  • Mi repositorios están diseñados para trabajar con los objetos del modelo. En un mundo real de la aplicación, ¿por qué yo nunca la necesidad de obtener el password campo si estoy buscando a la lista de todos mis usuarios?
  • Los repositorios son a menudo de un modelo específico, sin embargo, las consultas a menudo involucran a más de un modelo. Entonces, ¿qué repositorio ¿su método?
  • Esto mantiene mi repositorios muy simple-no es una hinchada clase de métodos.
  • Todas las consultas están ahora organizados en sus propias clases.
  • Realmente, en este punto, los repositorios existen simplemente para abstraer mi capa de base de datos.

Para mi ejemplo voy a crear un objeto de consulta para la búsqueda de "Todos". Aquí está la interfaz:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

Consulta La Implementación De Objeto

Aquí es donde podemos usar los datos del asignador de nuevo para ayudar a acelerar el desarrollo. Aviso que estoy permitiendo un tweak para el conjunto de datos devuelto-los campos. Esto es lo que quiero ir con la manipulación de la consulta. Recuerde, mi consulta objetos no son generadores de consultas. Ellos simplemente realizar una consulta específica. Sin embargo, ya sé que probablemente voy a utilizar mucho, en un número de situaciones diferentes, se la voy a dar yo la capacidad de especificar los campos. Nunca quiero volver campos 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 a la controladora, quiero mostrar otro ejemplo para ilustrar cuán poderosa es. Tal vez tengo un motor de generación de informes y la necesidad de crear un informe para AllOverdueAccounts. Esto puede ser difícil con mis datos mapper, y yo podría escribir algunas real SQL en esta situación. No hay problema, aquí, es lo que este objeto de consulta podría parecerse a:

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...";
    }
}

Esta bien mantiene toda mi lógica para este informe en una clase, y es fácil de probar. Me puede burlarse de que el contenido de mi corazón, o incluso el uso de una implementación diferente por completo.

El Controlador De

Ahora la parte divertida-traer todas las piezas juntas. Tenga en cuenta que yo soy el uso de la inyección de dependencia. Normalmente las dependencias se inyecta en el constructor, pero yo en realidad prefiero que se inyectan directamente en los métodos de controlador (rutas). Esto minimiza el controlador del objeto gráfico, y de hecho, me parece más legible. Nota, si no te gusta este enfoque, sólo tiene que utilizar el tradicional método constructor.

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 destacar aquí que cuando estoy modificando (crear, actualizar o eliminar) las entidades que, estoy trabajando con el modelo real de los objetos, y la realización de la persistencia a través de mi repositorios.

Sin embargo, cuando estoy mostrar (selección de los datos y su envío a los puntos de vista), no estoy trabajando con el modelo de objetos, sino el viejo y simple objetos de valor. Yo sólo seleccionar los campos que necesitamos, y está diseñado de manera que puedo máximo mi búsqueda de datos de rendimiento.

Mi repositorios estancia muy limpio, y en lugar de este "lío" está organizado en mi modelo de las consultas.

Yo uso un data mapper para ayudar con el desarrollo, como es simplemente ridículo para escribir repetitivo SQL para tareas comunes. Sin embargo, usted absolutamente puede escribir SQL donde sea necesario (complicado consultas, informes, etc.). Y cuando se hace, muy bien escondido a un nombre de la clase.

Me encantaría escuchar su opinión sobre mi enfoque!

18voto

ryan1234 Puntos 4884

Basado en mi experiencia, aquí están algunas respuestas a tus preguntas:

P: ¿Cómo lidiar con traer de vuelta a los campos que no necesitamos?

R: Desde mi experiencia, esto realmente se reduce a tratar con entidades completas, frente a las 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. Como los datos se pasa alrededor de la aplicación, se realiza sin contexto. Es una User? Un User con algunos Order información adjunta? En realidad no sabemos.

Prefiero trabajar con total entidades.

Tienes razón que se suele traer de vuelta datos que no uso, pero se puede abordar de diferentes maneras:

  1. Agresivamente caché de las entidades, por lo que usted sólo paga la lectura de precio de una vez de la base de datos.
  2. Pasar más tiempo en el modelado de sus entidades, por lo que tener buena distinciones entre ellos. (Considere la posibilidad de dividir una entidad grande en dos entidades más pequeñas, etc.)
  3. Considere la posibilidad de tener varias versiones de entidades. Usted puede tener un User para la parte final y tal vez un UserSmall para las llamadas AJAX. Uno podría tener 10 propiedades y uno tiene 3 propiedades.

Las desventajas de trabajar con consultas ad-hoc:

  1. Se termina con prácticamente los mismos datos a través de muchas consultas. Por ejemplo, con un User, que terminará de escribir esencialmente el mismo select * para el número de llamadas. Una llamada lo recibirá 8 de 10 campos, uno se consigue un 5 de 10, uno va a conseguir 7 de 10. ¿Por qué no reemplazar todo con una llamada que recibe 10 fuera de 10? La razón de esto es malo es que es un asesinato a re-factor/test/mock.
  2. Se hace muy difícil para la razón en un de alto nivel sobre el código a lo largo del tiempo. En lugar de afirmaciones tales como "¿por Qué la User tan lento?" que terminan seguimiento de consultas únicas y así, las correcciones de errores tienden a ser pequeñas y localizadas.
  3. Es muy difícil reemplazar la tecnología subyacente. Si se guarda todo en MySQL y ahora quieren pasar a MongoDB, es mucho más difícil para sustituir 100 ad-hoc de las llamadas que se trata de un puñado de entidades.

P: voy a tener demasiados métodos en mi repositorio.

R: realmente no he visto ninguna manera alrededor de este distinta a la consolidación de las llamadas. El método de llamadas en su repositorio realmente mapa con las características en su aplicación. Las más características, más datos específicos de llamadas. Usted puede empujar hacia atrás en cuenta y tratar de fusionar llamadas similares en uno.

La complejidad al final del día tiene que existir en algún lugar. Con un modelo de repositorio hemos impulsado en el repositorio de interfaz en vez de hacer un montón de procedimientos almacenados.

A veces tengo que decirme a mí mismo, "Bueno, es que había que dar en algún lugar! No hay balas de plata."

1voto

TFennis Puntos 477

Sólo puedo comentar sobre la forma en que (en mi empresa) lidiar con esto. Primero de todo, el rendimiento no es demasiado de un problema para nosotros, pero tener limpia/código apropiado.

Primero vamos a definir los Modelos, tales como UserModel que utiliza un ORM para crear UserEntity objetos. Cuando un UserEntity está cargado de un modelo de todos los campos de la carga. Para los campos de referencia a las entidades extranjeras, debemos utilizar el correspondiente modelo extranjero para crear las respectivas entidades. Para aquellas entidades que los datos se cargan bajo demanda. Ahora su primera reacción podría ser ...???...!!! permítanme darles un ejemplo un poco de ejemplo:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

En nuestro caso, $db es un ORM que es capaz de cargar las entidades. El modelo indica el ORM para cargar un conjunto de entidades de un tipo específico. El ORM contiene una asignación y la utiliza para inyectar todos los campos para que la entidad en la entidad. Para los campos extranjeros, sin embargo, sólo la identificación de los objetos cargados. En este caso, la OrderModel crea OrderEntitys con sólo la identificación de la referencia a las órdenes. Cuando PersistentEntity::getField se llama mediante la OrderEntity la entidad le indica que el modelo de carga perezoso todos los campos del OrderEntitys. Todos los OrderEntitys asociados con una UserEntity se tratan como un conjunto de resultados y serán cargados a la vez.

La magia aquí, es que nuestro modelo y ORM inyectar todos los datos a las entidades y que las entidades que se limita a ofrecer funciones de contenedor para el genérico getField método suministrado por PersistentEntity. Para resumir siempre nos carga a todos los campos, pero los campos de la referencia a una entidad extranjera, se carga cuando sea necesario. Sólo cargar un montón de campos no es realmente un problema de rendimiento. Carga todas las posibles entidades extranjeras sin embargo, sería una GRAN disminución del rendimiento.

Ahora a la carga de un conjunto específico de usuarios, basado en una cláusula where. Ofrecemos un orientada a objetos paquete de clases que permiten especificar expresión simple que puede ser pegadas. En el código de ejemplo que me llamó GetOptions. Es un contenedor de todas las opciones posibles para una consulta de selección. Contiene una colección de cláusulas where, una cláusula group by y todo lo demás. Nuestro donde cláusulas son bastante complicadas, pero que obviamente podría hacer una versión más simple de fácil.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

Una versión más sencilla de este sistema sería para pasar el DONDE parte de la consulta como una cadena de caracteres directamente en el modelo.

Lo siento por esta bastante complicada respuesta. Traté de resumir nuestro marco de trabajo tan pronto y tan claro como sea posible. Si tiene cualquier otra duda no dude en preguntar y yo voy a actualizar mi respuesta.

EDIT: Además, si usted realmente no quiere cargar algunos campos de inmediato puede especificar una carga diferida opción en su ORM, mapeo. Debido a que todos los campos son finalmente se cargan a través de la getField método que podría cargar algunos campos de último minuto al que se llama al método. Esto no es un problema muy grande en PHP, pero yo no lo recomendaría para otros sistemas.

1voto

WMeldon Puntos 345

Voy a añadir un poco sobre esto ya que actualmente estoy tratando de entender todo esto a mí mismo.

#1 y 2

Este es un lugar perfecto para sus ORM para hacer el trabajo pesado. Si usted está usando un modelo que implementa algún tipo de ORM, sólo se puede utilizar métodos para cuidar de estas cosas. Hacer su propia orderBy funciones que implementan el Elocuente métodos si es necesario. Utilizando Elocuente por ejemplo:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

Lo que parece estar buscando es un ORM. Ninguna razón de su Repositorio no puede ser en torno a uno. Esto requeriría de Usuario extender elocuente, pero yo personalmente no lo veo como un problema.

Si, no obstante, quieren evitar un ORM, que luego sería "el rollo de su propia" para conseguir lo que usted está buscando.

#3

Las Interfaces no se supone que ser duro y rápido requisitos. Algo puede implementar una interfaz y agregar a ella. Lo que no se puede hacer es no implementar una función necesaria de la interfaz. También puede extender interfaces como las clases para mantener tus cosas SECAS.

Dicho esto, estoy empezando a tener una idea, pero estas realizaciones han ayudado a mí.

0voto

Logan Bailey Puntos 748

Estas son algunas de las soluciones que he visto. Hay pros y contras para cada uno de ellos, pero es para que usted decida.

Problema #1: Demasiados campos

Este es un aspecto importante especialmente cuando se toma en cuenta el Índice Sólo analiza. Yo veo dos soluciones para lidiar con este problema. Usted puede actualizar sus funciones a tomar en un array opcional parámetro que contendría una lista de columnas a devolver. Si este parámetro está vacío usted desea devolver todas las columnas en la consulta. Esto puede ser un poco raro; basado en el parámetro que podría recuperar un objeto o un array. Usted también podría duplicar todas sus funciones de modo que usted tiene dos funciones distintas que ejecuta la misma consulta, pero uno devuelve una matriz de columnas y el otro devuelve un objeto.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

Problema #2: Demasiados métodos

Brevemente he trabajado con Propel como ORM hace un año y este se basa en lo que yo recuerdo 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 acceder a la función similar a lo que tengo actualmente en la lista; findByAttribute($attribute_value). El siguiente objeto hereda de este primer objeto. Usted puede actualizar este objeto secundario para construir en su más complejas funciones de captador.

Otra solución sería utilizar __call() a mapa no funciones definidas a algo punible. Su __call método sería sería capaz de analizar la findById y findByName en las distintas consultas.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

Espero que esto ayude, al menos, algunos lo.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X