2010-01-11 10 views
11

Tenemos un montón de código que pasa sobre "Ids" de filas de datos; estos son en su mayoría ints o guids. Podría hacer que este código sea más seguro al crear una estructura diferente para el ID de cada tabla de base de datos. Luego, el verificador de tipos ayudará a encontrar casos cuando se pase la identificación incorrecta.¿Es una buena idea crear un tipo personalizado para la clave principal de cada tabla de datos?

Por ejemplo, la tabla persona tiene una columna llama PERSONID y tenemos un código como:

DeletePerson(int personId) 
DeleteCar(int carId) 

¿Sería mejor tener:

struct PersonId 
{ 
    private int id; 
    // GetHashCode etc.... 
} 

DeletePerson(PersionId persionId) 
DeleteCar(CarId carId) 
  • Alguien tiene experiencia de la vida real de dong esto?

  • ¿Valen la pena los gastos?

  • ¿O más dolor, entonces vale la pena?

(También haría más fácil para cambiar el tipo de datos en la base de datos de la clave primaria, es mi forma de pensar de este ideal en el primer lugar)


Por favor, no Digo usar un ORM otro cambio importante en el diseño del sistema, ya que sé que un ORM sería una mejor opción, pero eso no está bajo mi control en este momento. Sin embargo, puedo hacer pequeños cambios como los anteriores al módulo en el que estoy trabajando actualmente.

Actualización: cuenta que esto no es una aplicación web y los identificadores se mantiene en la memoria y se pasa alrededor con WCF, por lo que no hay conversión a/de cuerdas en el borde. No hay ninguna razón para que la interfaz WCF no pueda usar el tipo PersonId, etc. El tipo PersonsId, etc., podría incluso usarse en el código UI de WPF/Winforms.

El único bit "sin tipo" del sistema es la base de datos.


Esto parece estar abajo con el costo/beneficio de la escritura de código gasto de tiempo que el compilador puede verificar mejor, o pasar el tiempo escribiendo más pruebas unitarias. Estoy más del lado de pasar el tiempo en las pruebas, ya que me gustaría ver al menos algunas pruebas unitarias en la base de códigos.

+2

¿Cómo le ayudará el verificador de tipos a verificar si se pasa una identificación no válida? Si su código está intentando pasar un GUID donde se espera un int, debe bombardear de todos modos. No estoy seguro de a lo que intente llegar le dará más protección, pero innecesariamente desordenará su código. – Chris

+0

@Chris, lo siento, estoy tratando de atrapar el caso de un CarID que se transfiere a un PersonID cuando ambos están metidos. –

Respuesta

2

No haría una identificación especial para esto. Esto es principalmente un problema de prueba. Puedes probar el código y asegurarte de que haga lo que se supone que debe hacer.

Puede crear una forma estándar de hacer las cosas en su sistema que ayuda en el mantenimiento futuro (similar a lo que menciona) pasando todo el objeto a manipular. Por supuesto, si usted nombró su parámetro (int personID) y tenía documentación, cualquier programador no malicioso debería poder usar el código de manera efectiva al llamar a ese método. Pasar un objeto completo hará ese tipo de coincidencia que está buscando y que debería ser suficiente de una manera estandarizada.

Acabo de ver que tiene una estructura especial para evitar esto, ya que agrega más trabajo para poco beneficio.Incluso si hiciera esto, alguien podría venir y encontrar una manera conveniente de hacer un método de 'ayuda' y eludir cualquier estructura que ponga en su lugar de todos modos, así que realmente no es una garantía.

4

Es difícil ver cómo valdría la pena: Recomiendo hacerlo solo como último recurso y solo si las personas están realmente mezclando identificadores durante el desarrollo o si informan que tienen dificultades para mantenerlos en línea recta.

En aplicaciones web en particular, ni siquiera ofrecerá la seguridad que está esperando: normalmente convertirá cadenas en enteros de todos modos. Hay demasiados casos donde se encuentre escribiendo código tonto como esto:

int personId; 
if (Int32.TryParse(Request["personId"], out personId)) { 
    this.person = this.PersonRepository.Get(new PersonId(personId)); 
} 

Tratar con complejo estado en la memoria mejora ciertamente el caso para los ID de tipo fuerte, pero creo que Arthur's idea es aún mejor: a evitar confusiones, exigir una instancia de entidad en lugar de un identificador. En algunas situaciones, las consideraciones de rendimiento y memoria podrían hacerlo poco práctico, pero incluso esas deberían ser lo suficientemente raras como para que la revisión del código sea igual de efectiva sin los efectos secundarios negativos (¡todo lo contrario!).

He trabajado en un sistema que hizo esto, y en realidad no dio ningún valor. No teníamos ambigüedades como las que está describiendo, y en términos de protección contra el futuro, hizo que sea un poco más difícil implementar nuevas características sin ningún beneficio. (El tipo de datos de ID no cambió en dos años, de todos modos, podría pasar en algún momento, pero hasta donde yo sé, el rendimiento de la inversión es actualmente negativo.)

+0

esto no es una aplicación web y un montón de estado complejo en la memoria, incluidos los ID. Sin embargo, he visto mucho lo anterior en aplicaciones web, por lo tanto, +1 –

+0

. La conversión explícita requerida para el escenario web que das no es realmente un inconveniente, te obliga a documentar/pensar sobre el tipo del parámetro en su límite de entrada. . –

+0

@Frank - en el caso que describí, ya tiene 1) una clave de solicitud y 2) un nombre de variable local, que describen el contenido del parámetro en el límite de entrada. Simplemente no veo qué valor agrega la clase identificadora fuertemente tipada en ese escenario. –

1

No veo mucho valor en la comprobación personalizada en este caso. Es posible que desee para reforzar su suite de pruebas para comprobar que suceden dos cosas:

  1. Su código de acceso a datos siempre funciona según lo previsto (es decir, usted no está cargando la información clave incompatible en sus clases y hacer mal uso debido de eso).
  2. Que su código de "ida y vuelta" está funcionando como se esperaba (es decir, que cargar un registro, realizar un cambio y guardarlo de nuevo no está de alguna manera corrompiendo sus objetos de lógica de negocios).

Tener una capa de acceso a datos (y lógica de negocios) en la que pueda confiar es crucial para poder abordar los problemas de imágenes más grandes que encontrará al intentar implementar los requisitos comerciales reales. Si su capa de datos no es confiable, estará gastando mucho esfuerzo en el seguimiento (o peor, en el trabajo) de los problemas que surgen cuando coloca cargas en el subsistema.

Si, por el contrario, su código de acceso a datos es robusto ante un uso incorrecto (lo que su banco de pruebas debería probarle) puede relajarse un poco en los niveles superiores y confiar en que lanzarán excepciones (o como quiera lidiar con eso) cuando se abusa.

La razón por la que escuchan a las personas sugerir un ORM es que muchas de estas cuestiones son tratadas de manera confiable por dichas herramientas. Si su implementación es lo suficientemente larga como para que un cambio de este tipo resulte doloroso, solo tenga en cuenta que su capa de acceso a datos de bajo nivel necesita ser tan robusta como un buen ORM si realmente desea poder confiar (y así olvidarse de en cierta medida) su acceso a los datos.

En lugar de validación personalizada, su suite de pruebas podría inyectar código (a través de la inyección de dependencias) que hace pruebas sólidas de sus llaves (golpeando la base de datos para verificar cada cambio) como las pruebas se ejecutan y que inyecta código de producción que omite o restringe tales pruebas por razones de rendimiento. Su capa de datos arrojará errores en las claves que fallaron (si tiene allí configuradas sus claves externas) por lo que también debería poder manejar esas excepciones.

+0

Si solo ..., la mayor parte del código actual simplemente captura, luego registra la excepción y luego devuelve un resultado predeterminado a la siguiente capa. (Después de todo, es más importante hacer que el cliente piense que este sistema funciona bien, y luego hacer que el sistema funcione bien). –

+0

Ouch. Por el lado positivo, tiene un inicio de sesión que se puede utilizar para construir tales pruebas para evitar la recurrencia futura de dichos errores (¿Adivinaré que tales pruebas no se crean comúnmente ahora?) El código de la base de datos de prueba puede ser sencillo si tener la capacidad de establecer su DB de prueba en un estado conocido antes de cada ejecución de prueba. Usamos la solución de un "hombre pobre" para capturar nuestro estado de inicio y luego ejecutar el código de prueba * all * en transacciones que se retrotraen (falla de éxito * o *). De esta forma, cada prueba obtiene la prístina base de datos para trabajar (las pruebas más largas tienen una TRANSACCIÓN externa). – Godeke

1

Mi intuición dice que esto no vale la pena. Mi primera pregunta para usted sería si realmente ha encontrado errores en los que se transmitió el int incorrecto (una identificación de automóvil en lugar de una identificación de persona en su ejemplo). Si es así, probablemente se trate de una peor arquitectura general, ya que los objetos del Dominio tienen demasiado acoplamiento y están pasando demasiados argumentos en los parámetros del método en lugar de actuar sobre las variables internas.

2

Puede optar por los GUID, como usted mismo sugirió. Entonces, no tendrá que preocuparse por pasar una identificación de persona de "42" a DeleteCar() y eliminar accidentalmente el automóvil con una ID de 42. Los GUID son únicos; si pasa una persona GUID a DeleteCar en su código debido a un error de programación, ese GUID no será un PK de ningún automóvil en la base de datos.

+0

Para mí, parece la mejor respuesta de todas las disponibles. –

+0

Es una buena solución a este problema en particular, y podría ser apropiado, pero la decisión de usar GUID o entradas para una clave primaria en particular depende de muchos otros factores. Una búsqueda rápida de SO en "GUID Int Primary Key" ofrece varios buenos informes sobre ellos. –

2

Se puede crear una clase simple Id que puede ayudar a diferenciar en el código entre los dos:

public class Id<T> 
{ 
    private int RawValue 
    { 
     get; 
     set; 
    } 

    public Id(int value) 
    { 
     this.RawValue = value; 
    } 

    public static explicit operator int (Id<T> id) { return id.RawValue; } 

    // this cast is optional and can be excluded for further strictness 
    public static implicit operator Id<T> (int value) { return new Id(value); } 
} 

Usado de esta manera:

class SomeClass 
{ 
    public Id<Person> PersonId { get; set; } 
    public Id<Car> CarId { get; set; } 
} 

Suponiendo sus valores sólo se recupera de la base de datos, a menos que explícitamente arrojes el valor a un entero, no es posible usar los dos en el lugar del otro.

+0

Seguí haciendo algo como esto con Linq-to-SQL, desafortunadamente no me dejaba usar mi tipo personalizado con la columna ID. Por lo tanto, es posible que tenga problemas con otras capas de ORM que pueda tener. Al final no lo hice, pero creo que es algo razonable de hacer. Sin embargo, sería inusual. –

+0

Creo que tiene la mezcla implícita/explícita sin embargo. El tipo Id <> debe ser implícitamente convertible a un int, mientras que un int solo debe ser explícitamente convertible a un Id <>. –

+0

Lo prefiero de esta manera, ya que los moldes que se alejan del tipo son fácilmente visibles. Prefiero que nunca se vea una ID de base de datos como usuario int. A cada cual lo suyo.Tenemos un sistema ORM que utiliza estas clases de Id (bueno, algo similar) con buenos resultados. – user7116

Cuestiones relacionadas