2011-11-05 8 views
7

Primero, me disculpo si esto se ha preguntado antes, de hecho estoy seguro de que lo ha hecho, pero no puedo encontrarlo/no puedo resolver qué buscar para encontrarloCómo obtener la próxima identificación alfanumérica basada en el valor existente de MySQL

Necesito generar identificaciones de referencia rápida únicas, basadas en el nombre de una compañía. Así, por ejemplo:

 
Company Name    Reference 
Smiths Joinery    smit0001 
Smith and Jones Consulting smit0002 
Smithsons Carpets   smit0003 

Todos ellos se almacenan en una columna VARCHAR en una tabla de MySQL. Los datos serán recopilados, escapados e insertados como 'HTML -> PHP -> MySQL'. Los ID deben estar en el formato que se muestra arriba, cuatro letras, luego cuatro números (inicialmente al menos, cuando llego al smit9999 simplemente se derraman en 5 dígitos).

Puedo encargarme de generar las 4 letras del nombre de la empresa, simplemente voy a recorrer el nombre hasta que haya reunido 4 caracteres alfabéticos, y strtolower(), pero luego necesito obtener el siguiente número disponible.

¿Cuál es la mejor/más fácil manera de hacer esto, por lo que se elimina la posibilidad de duplicados?

Por el momento estoy pensando:

$fourLetters = 'smit'; 
$query = "SELECT `company_ref` 
      FROM `companies` 
      WHERE 
      `company_ref` LIKE '$fourLetters%' 
      ORDER BY `company_ref` DESC 
      LIMIT 1"; 
$last = mysqli_fetch_assoc(mysqli_query($link, $query)); 
$newNum = ((int) ltrim(substr($last['company_ref'],4),'0')) + 1; 
$newRef = $fourLetters.str_pad($newNum, 4, '0', STR_PAD_LEFT); 

pero puedo ver que esta causando un problema si dos usuarios intentan entrar en nombres de empresas que resultarían en el mismo ID al mismo tiempo. Utilizaré un índice único en la columna, por lo que no generará duplicados en la base de datos, pero aún así causará un problema.

¿Alguien puede pensar en una manera de hacer que MySQL me explique esto cuando hago la inserción, en lugar de calcularla en PHP de antemano?

Tenga en cuenta que el código real será OO y manejará los errores, etc. - Estoy buscando ideas sobre si hay una mejor manera de hacer esta tarea específica, se trata más de SQL que cualquier otra cosa.

EDITAR

creo que @ sugerencia de utilizar un disparador de MySQL de EmmanuelN puede ser la manera de manejar esto, pero:

  • No soy lo suficientemente bueno con MySQL, en particular los factores desencadenantes, a hacer que esto funcione, y me gustaría un ejemplo paso a paso para crear, agregar y usar un disparador.
  • Todavía no estoy seguro de si esto eliminará la posibilidad de que se generen dos identificaciones idénticas. Ver what happens if two rows are inserted at the same time that result in the trigger running simultaneously, and produce the same reference? Is there any way to lock the trigger (or a UDF) in such a way that it can only have one concurrent instance?.

O estaría abierto a cualquier otro enfoque sugerido para este problema.

+0

¿Ha pensado en usar el disparador y generar esa identificación dentro de un disparador? –

+0

No, pero SQL es definitivamente mi tema más débil en términos de este tipo de proyecto, realmente no sé cómo usar activadores de manera efectiva. ¿Podría darme un resumen básico de cómo se haría esto? Si pudiera dirigirme hacia algún material de lectura relevante, estaría feliz :-) – DaveRandom

+1

[Este artículo] (http://www.sugarcrm.com/forums/f6/developed-auto-generate-ids-guids -within-mysql-2895 /) habla sobre generación automática en GUIDs usando mysql, creo que le dará un lugar donde comenzar –

Respuesta

7

Si está utilizando MyISAM, puede crear una clave primaria compuesta en un campo de texto + campo de incremento automático. MySQL se encargará de incrementar el número automáticamente. Son campos separados, pero puede obtener el mismo efecto.

CREATE TABLE example (
company_name varchar(100), 
key_prefix char(4) not null, 
key_increment int unsigned auto_increment, 
primary key co_key (key_prefix,key_increment) 
) ENGINE=MYISAM; 

Al hacer una inserción en la tabla, el campo key_increment se incrementará en función del valor más alto basado en key_prefix. Así inserte con key_prefix "Smit" se iniciará con 1 en key_inrement, key_prefix "Jone" se iniciará con 1 en key_inrement, etc.

Pros:

  • Usted no tiene que hacer nada con los números de cálculo .

Contras:

  • Usted tiene una división clave a través de 2 columnas.
  • No funciona con InnoDB.
+0

Esto parece ser una buena opción y un buen candidato para la respuesta aceptada - presumiblemente haciendo esto Acabo de 'INSERT' las alfas en la columna' key_prefix', y 'key_increment' genera una parte numérica. Hará que la lógica 'SELECCIONAR * ligeramente * sea más complicada cuando busque por una referencia de entrada de usuario, pero aparte de ese punto menor, cumple con todos los requisitos ... ¡muchas gracias! – DaveRandom

+1

Mi motor de almacenamiento es InnoDB actual (aunque no estoy particularmente apegado a esto) - por interés, ¿por qué solo puedes hacer esto con MyISAM? – DaveRandom

+0

Me lo pregunté también: del manual de mysql en Innodb Limits "Para una columna AUTO_INCREMENT, siempre debe definir un índice para la tabla, y ese índice debe contener solo la columna AUTO_INCREMENT. En las tablas MyISAM, la columna AUTO_INCREMENT puede ser parte de un índice de columnas múltiples ". –

1
  1. Asegúrese de tener una restricción única en la columna de Referencia.
  2. Obtenga la referencia secuencial máxima actual de la misma forma que lo hace en su código de muestra. En realidad, no es necesario recortar los ceros antes de convertir a (int), '0001' es un número entero válido.
  3. Haz un lazo y coloca tu inserción dentro.
  4. Compruebe las filas afectadas después de la inserción. También puede verificar el estado de SQL para un error de clave duplicado, pero tener cero filas afectadas es una buena indicación de que su inserción falló debido a la inserción de un valor de Referencia existente.
  5. Si tiene cero filas afectadas, incremente el número secuencial y enrolle nuevamente el ciclo. Si tiene filas afectadas que no sean cero, ha terminado y tiene un identificador único insertado.
+0

Este enfoque es ligeramente ineficaz, ya que volverá a intentar insertos hasta que finalmente logre evitar el conflicto. Aún así, está comenzando desde el valor máximo de referencia actual +1, y a menos que tenga MUCHOS insertos, es muy probable que tenga éxito en el primer intento, y prácticamente garantizado que tendrá éxito en el segundo intento. – lanzz

+0

Esto fue lo que pensé que terminaría hasta la respuesta de Brent Baisley, solo que no me gusta debido a la muy remota posibilidad de un ciclo largo tratando de encontrar la próxima identificación disponible en caso de un gran volumen de información independiente inserciones, como mencionas en tu comentario. Sé que esto es una programación en torno a una situación que * nunca * debería ocurrir, pero aún me gustaría evitarla si es posible. – DaveRandom

+0

La solución de Brent es realmente mejor que la mía, de hecho no sabía que myisam rastrea la autoincrementación para la clave primaria completa en lugar de la columna real. – lanzz

0

La manera más fácil de evitar los valores duplicados para la columna de referencia es agregar una restricción única. Entonces, si múltiples procesos intentan establecer el mismo valor, MySQL rechazará el segundo intento y arrojará un error.

ALTER TABLE table_name ADD UNIQUE KEY (`company_ref`); 

Si me encontré con su situación, me gustaría manejar la generación de ID de referencia de empresa dentro de la capa de aplicación, los disparadores pueden causar problemas si no se ha configurado correctamente.

3

¿Qué hay de esta solución con un gatillo y una tabla para contener el company_ref es único. Se realizó una corrección: la tabla de referencia debe ser MyISAM si desea que la numeración comience en 1 para cada secuencia única de 4char.

DROP TABLE IF EXISTS company; 
CREATE TABLE company (
    company_name varchar(100) DEFAULT NULL, 
    company_ref char(8) DEFAULT NULL 
) ENGINE=InnoDB 

DELIMITER ;; 
CREATE TRIGGER company_reference BEFORE INSERT ON company 
FOR EACH ROW BEGIN 
    INSERT INTO reference SET company_ref=SUBSTRING(LOWER(NEW.company_name), 1, 4), numeric_ref=NULL; 
    SET NEW.company_ref=CONCAT(SUBSTRING(LOWER(NEW.company_name), 1, 4), LPAD(CAST(LAST_INSERT_ID() AS CHAR(10)), 4, '0')); 
END ;; 
DELIMITER ; 

DROP TABLE IF EXISTS reference; 
CREATE TABLE reference (
company_ref char(4) NOT NULL DEFAULT '', 
numeric_ref int(11) NOT NULL AUTO_INCREMENT, 
PRIMARY KEY (company_ref, numeric_ref) 
) ENGINE=MyISAM; 

está completo y si aquí es un disparador que va a crear una nueva referencia si se altera el nombre de la empresa.

DROP TRIGGER IF EXISTS company_reference_up; 
DELIMITER ;; 
CREATE TRIGGER company_reference_up BEFORE UPDATE ON company 
FOR EACH ROW BEGIN 
    IF NEW.company_name <> OLD.company_name THEN 
     DELETE FROM reference WHERE company_ref=SUBSTRING(LOWER(OLD.company_ref), 1, 4) AND numeric_ref=SUBSTRING(OLD.company_ref, 5, 4); 
     INSERT INTO reference SET company_ref=SUBSTRING(LOWER(NEW.company_name), 1, 4), numeric_ref=NULL; 
     SET NEW.company_ref=CONCAT(SUBSTRING(LOWER(NEW.company_name), 1, 4), LPAD(CAST(LAST_INSERT_ID() AS CHAR(10)), 4, '0')); 
    END IF; 
END; 
;; 
DELIMITER ; 
+0

Voy a aceptar la respuesta de Brent ya que me ha proporcionado una buena explicación de por qué este es el enfoque correcto, pero si Podría aceptar 2 respuestas que recibiría la otra, ya que es una combinación de sus respuestas lo que me ha ayudado a encontrar la solución adecuada a mi problema, ya que proporcionó un buen ejemplo de la creación del desencadenante.El problema que estaba teniendo es el cambio de 'DELIMITER' mientras ejecutaba la consulta de creación; de alguna manera, me lo perdí cuando inicialmente intenté hacerlo. Definitivamente se merece (al menos) el +1 que le di ... – DaveRandom

+0

@Dave :-) bien - me alegro de que lo haya ayudado –

3

Dado que está utilizando InnoDB, ¿por qué no utiliza una transacción explícita para obtener un bloqueo de fila exclusivo y evitar que otra conexión lea la misma fila antes de terminar de configurar una nueva ID basada en él?

(Naturalmente, haciendo el cálculo en un disparador llevaría a cabo el bloqueo por menos tiempo.)

mysqli_query($link, "BEGIN TRANSACTION"); 
$query = "SELECT `company_ref` 
      FROM `companies` 
      WHERE 
      `company_ref` LIKE '$fourLetters%' 
      ORDER BY `company_ref` DESC 
      LIMIT 1 
      FOR UPDATE"; 
$last = mysqli_fetch_assoc(mysqli_query($link, $query)); 
$newNum = ((int) ltrim(substr($last['company_ref'],4),'0')) + 1; 
$newRef = $fourLetters.str_pad($newNum, 4, '0', STR_PAD_LEFT); 
mysqli_query($link, "INSERT INTO companies . . . (new row using $newref)"); 
mysqli_commit($link); 

Editar: Sólo para estar 100% seguro de que hizo una prueba con la mano para confirmar que la segunda transacción devuelve la fila recién insertada después de esperar en lugar de la fila bloqueada original.

Edit2: También probé el caso donde no se devolvió una fila inicial (donde pensaría que no hay una fila inicial para poner un candado) y eso también funciona.

0

Una versión hacky que también funciona para InnoDB.

Sustituir el inserto a companies con dos insertos en una transacción:

INSERT INTO __keys 
     VALUES (LEFT(LOWER('Smiths Joinery'),4), LAST_INSERT_ID(1)) 
    ON DUPLICATE KEY UPDATE 
     num = LAST_INSERT_ID(num+1); 

    INSERT INTO __companies (comp_name, reference) 
    VALUES ('Smiths Joinery', 
      CONCAT(LEFT(LOWER(comp_name),4), LPAD(LAST_INSERT_ID(), 4, '0'))); 

donde:

CREATE TABLE `__keys` (
     `prefix` char(4) NOT NULL, 
     `num` smallint(5) unsigned NOT NULL, 
     PRIMARY KEY (`prefix`) 
    ) ENGINE=InnoDB COLLATE latin1_general_ci; 

    CREATE TABLE `__companies` (
     `comp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
     `comp_name` varchar(45) NOT NULL, 
     `reference` char(8) NOT NULL, 
     PRIMARY KEY (`comp_id`) 
    ) ENGINE=InnoDB COLLATE latin1_general_ci; 

Aviso:

  • latin1_general_ci puede ser reemplazado con utf8_general_ci,
  • LEFT(LOWER('Smiths Joinery'),4) sería mejor que se convierta en una función en PHP
Cuestiones relacionadas