Release Notes: Delphi and C++Builder 2010 Updates 4, 5 (Database Pack), and Boost Library

diciembre 16, 2009 en Advertencia, Ayuda, Consejo, Delphi, Entrada Diario, Noticias, Noticias Delphi

Han llegado los primeros comentarios de la disponibilidad de los últimos updates para Delphi 2010, C++ Builder 2010 y Rad Studio. Yo lo he podido leer por el comentario de Andreano Lanusse desde el grupo Twibes Hispano y tal y como nos dice, podeis acceder facilmente al mismo tan solo con la opción de chequear actualizaciones (Check for Updates) existente en el item creado por el entorno en el menu de inicio.

Logicamente, tambien podeis acceder a la descarga a través del registro en la zona de descargas de Embarcadero.

Es muy importante que leais las notas de instalación antes de efectuar la actulización. Estas notas las podeis leer en la entrada de Kris Houser

http://edn.embarcadero.com/article/40174#14KnownIssues

 No dejeis de leerla, ya que existen algunos prerrequisitos que afectan al orden de instalación, tanto de los updates que contiene (segun el producto) como respecto a los anteriores, que puedan estar pendientes de instalación.

En mi caso concreto, parece ser que finalizó con exito… pero he podido ver durante la actualización de la instalación del update 5, numerosas advertencias similares a la que se muestra en la imagen:

Advertencia instalacion databasepack (Update 5)

Que permitieron seguir la instalación y como comento, aparentemente finalizar con exito…

Ni idea… :-(

El problema de estas advertencias es que no sabes tras aceptar, si han sido “resueltas” por el mismo instalador o ha quedado algo incorrecto.  :-)   

En el menú de ayuda del entorno, se visualizan los updates pero supongo que tendré que buscar la forma de asegurarme de que realmente la instalación fue correcta. Lo peor de todo es que en las notas de instalación, se comenta que la desistalación de estos updates puede ser problematica… Por eso os comento que leais con atención las notas de instalación.

__Uninstalling Update 4
If you need to uninstall Update 4, you must uninstall the entire product and reinstall one of the following:
  • Delphi 2010, C++Builder 2010, or RAD Studio 2010 (RTM)
  • Update 1 to Delphi 2010, C++Builder 2010, or RAD Studio 2010

    Uninstalling Update 5 (Database Pack)

If you need to uninstall the update, select “Embarcadero Delphi and C++Builder 2010 Database Pack” in Add/Remove Programs, and click Remove. The IDE has dependencies on the Database Pack, so you should not uninstall the DataBase Pack if you will continue to use the IDE.

If you have all three personalities of RAD Studio 2010 installed (including Delphi Prism), uninstalling Update 5 (Database Pack) or uninstalling Delphi Prism will mean that you must restart the Blackfish SQL server.

    Uninstalling the Boost Library

The C++ Standard Library has dependencies on Boost, so you should not uninstall Boost if you will continue to use the C++ compiler.
 

Y por ultimo, quizás dejar el comentario que me ha causado extrañeza que tras finalizar la actualización, fuera necesario registrar de nuevo el producto.

En fin… dejo el comentario por si quereis añadir algo. Ya sabeis que sois libres de añadir lo que gusteis.

Experimentos, reflexiones y otros artefactos (IV)

junio 7, 2009 en Advertencia, Artículos, Ayuda, Código, Delphi, Entrada Diario

Hace una tarde calurosa y bastante veraniega. En una tarde así, no se puede estar en casa escribiendo una entrada como esta… :-) El sol cae a plomo a las 6 de la tarde, un día de verano, y parece que todo invita a salir a disfrutar de un rato de deporte. Así que con con vuestro permiso :-) inicio la entrada ahora, hasta donde llegue, y la prosigo esta noche si quedara algo por decir.

¿Por donde empiezo? Ummmm…

Lo habíamos dejado en la parte en la que haciamos un pequeño boceto de las dos clases que ibamos a considerar. La clase TGenerador y la clase TAtributo. Eso nos permitía ver como se podían relacionar y a modo de prueba, imaginábamos un método para permitirnos alterar el orden de los mismos, en caso de ser necesario.

Durante esta semana he podido sacer algunos minutos para adelantarme y darle forma a esas dos ideas. Pero no teneis que perder de vista, por favor, que lo importante no es el código en si mismo, que no es mas que un ejemplo cualquiera y puede contener alguna que otra errata al analizarlo con profundidad, sino el hecho mismo de solicitar de cada uno, un ejercicio de análisis previo y reflexión, intentando razonar en términos de clases, en la medida que nos sea posible.

Una imagen vale mas que mil palabras. Este es el ejemplo que preparé para probar la implementación de ambas clases ya con una funcionalidad mayor.

exercicio2

La imagen muestra en la parte superior, 4 atributos que representan los tipos posibles que hemos adminitido. Un atributo combinable (Atributo 2), capaz de seleccionar multiples valores. Un atributo dependiente (Atributo 3), tambien combinable pero que va a permitirnos descartar alguna combinaciones en funcion de una relación imaginaria. Es decir, el caso de que un tipo de atributo solo pueda combinarse con determinados valores de otro atributo. Decirlo es mas complicado que razonarlo.  En el ejemplo, el color ’18′ solo puede ser combinado con los tipos ‘AA’ y ‘AB’.

Nos queda el atributo dependiente, representado en el Atributo 1, que toma siempre el mismo valor. Y el Atributo rellenable de, que toma valores respecto a otro atributo cualquiera combinable y no rellenable. Es el Atributo 4.

Así que al pulsar el boton “Solución”, nos presenta en los dos componentes TMemo, las combinaciones correctas y las descartadas. En este ejemplo, se aceptan 8 combinaciones y se descartan 16, del total de 24 combinaciones posibles. Las ventanas superiores unicamente existen con el motivo de permitirnos visualizar la información que contiene el generador y los atributos de cara a seguir mejor el ejemplo.

Ahora mismo, el desarrollo del ejemplo se va alejando progresivamente de los bocetos de las dos primeras entradas, representadas en el bucle anidado for, intimamente ligado al interfaz.

Podemos ver la declaración de tipos del módulo que contiene las clases protagonistas:


type

  TEstructura = Class(TObject)
    FIndice: Integer;
    FSiguiente: TObject;
  private
    procedure SetIndice(const Value: Integer);
    procedure SetSiguiente(const Value: TObject);
  published
  public
  constructor Create;
  destructor Destroy; override;
  function Situar(AIndice: Integer): TEstructura;
  property Indice: Integer read FIndice write SetIndice;
  property Siguiente: TObject read FSiguiente write SetSiguiente;
  end;

  TAtributo = class;

  TGenerador = Class(TObject)
  private
    FAtributos: TList;
    FCombinaciones: TStringList;
    function GetAtributos(Indice: Integer): TAtributo;
    function GetDescartado(Indice: Integer): Boolean;
    procedure SetDescartado(Indice: Integer; const Value: Boolean);
    function GetCombinaciones(Indice: Integer): String;
   protected
    procedure DoCombinar; virtual;
    procedure DoLimpiarCombinaciones; virtual;
    procedure AssignCombinaciones; virtual;
  public
    constructor Create;
    destructor Destroy; override;
    function Count: Integer;
    procedure SetUpPosition(AAtributo: TAtributo);
    procedure SetDownPosition(AAtributo: TAtributo);
    function GetPosicion(AAtributo: TAtributo): Integer;
    function GetMultiplicador(AAtributo: TAtributo): Integer;
    function GetTotalCombinaciones: Integer;
    function AddAtributo: TAtributo;
    procedure Combinar; virtual;
    procedure VerificarDependencias;
    property Atributos[Indice: Integer]: TAtributo read GetAtributos;
    property Descartado[Indice: Integer]: Boolean read GetDescartado write SetDescartado;
    property Combinaciones[Indice: Integer]: String read GetCombinaciones;
  end;

  TAtributo = class(TObject)
  private
    FGenerador: TGenerador;
    FListaValores: TStrings;
    FCombinaciones: TStrings;
    FCombinable: Boolean;
    FCaption: String;
    FRellenableDe: TAtributo;
    FDependienteDe: TAtributo;
    procedure SetCombinable(const Value: Boolean);
    procedure SetCaption(const Value: String);
    function GetValues(Indice: Integer): String;
    procedure SetValues(Indice: Integer; const Value: String);
    function GetCombinacion(Indice: Integer): String;
    procedure SetRellenableDe(const Value: TAtributo);
    procedure SetDependienteDe(const Value: TAtributo);
    function GetDependencia(Indice: Integer): TEstructura;
    function GetPosicion: Integer;
    { Private declarations }
  protected
    function DoGetMultiplicador: Integer; virtual;
  public
    { Public declarations }
    procedure LimpiarCombinaciones;
    procedure Combinar;
    function GetMultiplicador: Integer;
    function ValuesCount: Integer;
    procedure UpPosicion;
    procedure DownPosicion;
    constructor Create(AGenerador: TGenerador); virtual;
    destructor Destroy; override;
    function AddValue(const AValue: String): Integer; overload;
    function AddValue(const AValue, ADependencia: String): Integer; overload;
    procedure DeleteValue(const AValor: String);
    procedure VerificarDependencias;
    property Combinable: Boolean read FCombinable write SetCombinable;
    property Posicion: Integer read GetPosicion;
    property Caption: String read FCaption write SetCaption;
    property Values[Indice: Integer]: String read GetValues write SetValues;
    property Dependencia[Indice: Integer]: TEstructura read GetDependencia;
    property Combinacion[Indice: Integer]: String read GetCombinacion;
    property RellenableDe: TAtributo read FRellenableDe write SetRellenableDe;
    property DependienteDe: TAtributo read FDependienteDe write SetDependienteDe;
  end;

Descargar

No nos es posible en una entrada explicar todas las relaciones, por varios motivos razonables, entre los que considero el tiempo y el espacio. Así que lo mejor es que os descargueis el codigo fuente y le deis un vistazo.

Se me ocurren algunas ideas que pueden ser interesantes:

* A pesar de funcionar correctamente, o al menos eso creo, el hecho mismo de declarar las 3 clases en la parte pública del interfaz, hacen que queden disponibles para el uso, métodos que por ejemplo pertenen a TAtributo y que tan solo deberían estar disponibles para la instancia de TGenerador, puesto que va a ser esta quien quede intimamente ligada a ella, dado que conoce como deben ser ejecutados. De hecho, para ser formalmente correcto, el “usuario”, que en este caso es el formulario, debería batallar tanto con TGenerador como con la clase TAtributo, pero parece claro que nunca debería poder ejecutar por ejemplo el metodo VerificarDependencias en el atributo.  Y sin embargo, la disposición de las clases y la publicación de los métodos lo facilitan.

* En ocasiones, al pensar en clases aparecen puntos de vista que inicialmente no considerabamos. De hecho, lo que inicialmente era un bucle for anidado, ha sido sustituido por otro razonamiento. ¿Es posible que la clase Generador le pueda decir a cada atributo: Oye majo, que quiero que te combines? Ese es el motivo de que exista un metodo virtual DoCombinar, que se encarga de que cada atributo ejecute su propia expansión de combinaciones…

A eso me refería al comentar que a veces surgen nuevos puntos de vista, mejores o peores, pero distintos. Se me ocurrío razonar al estilo de la cuenta de la vieja. Y generar una lista de las disposiciones individuales de cada uno de los atributos en las combinaciones.

Este es el razonamiento de la vieja.

Supongamos los siguentes pares (Atributos, valores)

Atributo1  > A, B          Atributo2 >  C, D           Atributo3 > E, F

Podemos seguir un patron para combinarlos (el mismo que seguiría el bucle for)

ACE, ACF, ADE, ADF, BCE, BCF, BDE, BDF

Nos permite individualmente obtener una lista de valores

Atributo1 > A,A,A,A,B,B,B,B     Atributo2 > C,C,D,D,C,C,D,D   Atributo3 > E,F,E,F,E,F,E,F

Por eso aparece por algun lado el multiplicador, que no es mas que las veces que tiene que repetirse el valor en la cadena. En este ejemplo en concreto, el Atributo1 tiene un multiplicador de 4, el Atributo2, un multiplicador de 2, y el atributo3 un multiplicador de 1. Y dichos valores, son factibles de ser implementados mediante una sencilla función.

function TGenerador.GetMultiplicador(AAtributo: TAtributo): Integer;
var
  i,j: Integer;
begin
   if (AAtributo.Combinable) and (not Assigned(AAtributo.RellenableDe)) then
   begin
      Result:= 1;
      i:= FAtributos.IndexOf(AAtributo);
      if  (i < 0) then Result:= -1
      else begin
         for j := i+1 to FAtributos.Count - 1 do
            if (TAtributo(FAtributos[j]).ValuesCount = 0) or
                            (not TAtributo(FAtributos[j]).Combinable)    then
               Result:= Result * 1
            else Result:= Result * TAtributo(FAtributos[j]).ValuesCount;
      end;
   end
   else begin
      Result:= 1;
   end;
end;

* La tercera reflexión que ya se desprende de todo lo comentado, es el tiempo que dedicamos a implementar lo mismo. Visto de la primera forma, como razonabamos en las dos primeras entradas, la ganancia de tiempo podría ser considerable (menor tiempo). Este módulo, el que vemos en la entrada actual, tiene mayor trabajo para hacer exactamente lo mismo y quizás fuera una buena razón para considerar que perdemos el tiempo. Eso que deciamos de: -acaba como sea pero acaba (El jefe dixit) suele ser pan para hoy y hambre para mañana. Así que de cuando en cuando, deberíais dar un buen puñetazo en la mesa :-) y poner una silla trabando la entrada de vuestro despacho de forma que sea imposible que nadie os distraiga. ;-)  Por paradojas de la vida, esa misma persona que hoy os pide que acabeis ya el desarrollo como sea, es la misma que mañana os atará de los pulgares en el mástil mayor, si dentro de 6 meses se complica el desarrollo.

Vale… es una exageración bastante malvada pero yo creo que se me entiende. Y el peor enemigo que puede tener vuestro trabajo es que seais vuestro propio jefe… :-)

* Otra cosa que puede extrañar en el codigo, es que aparezca un tercero en discordia (la clase TEstructura). Es algo bastante habitual y ya  lo referia en alguna entrada anterior al hablar de aquel componente Buscador, que dejabamos atras al hablar de los hilos de ejecución. En aquel caso, el tercero en discorida era una implementación del patron iterador, que permitía recorrer la estructura y avanzar. En este caso concreto, me encontré que para poder trabajar con la dependencias, siendo posible que existiera una relación múltiple entre los valores: un color podia pertenecer a varios tipos, pensé que me era necesaria una forma sencilla de recorrerla. En eso me ayuda la clase TEstructura, que apunta a otra Estructura con el valor de relación. De esa forma, puedo buscar si un color pertenece a un tipo y saber si la combinación debe ser descartada o no.

* Y la ultima reflexión que cierra la entrada: la reutilización de nuestro codigo. Ligarnos a nuestros interfaces nos puede permitir reutilizarlos en forma de herencia y en eso ya nos ayuda el IDE de Delphi, al habilitar nuestros formularios en el repositorio de objetos. Sin embargo, al hacerlo así nos obliga a arrastrar no solo las ideas sino las apariencias y eso no es demasiado bueno en todos los casos.

Creo que os lo puedo demostrar… Dadme unos minutos… Pongamos por caso que dos días despues, vamos a formar una peña con el gabinete de programadores para jugar a las quinielas y poder hacernos ricos y dejar de trabajar. :-) WOWWWWW…  Y como somos previsores recordamos que nuestro programa hacía algo similar… De haberme ligado al interfaz, me hubiera sido imposible en este caso concreto reutilizar esa idea de forma sencilla. Sin embargo, nuestro nuevo planteamiento sí me permite mediante controles distintos y buscando una finalidad que no tiene porque coincidir con la original, reutilizar el código escrito.

Esta es la imagen de nuestro nuevo interfaz de ejemplo:

Dadle un vistazo:

azar1x2

Este es el código que ha permitido ampliar la funcionalidad original (por si teneis curiosidad)

:-)

unit main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ButtonGroup, ExtCtrls, StdCtrls, CheckLst, uAtributos;
const

RestriccionFicticia = 100000;

type
  TForm1 = class(TForm)
    chbPartidos: TCheckListBox;
    chb1: TCheckListBox;
    chbx: TCheckListBox;
    chb2: TCheckListBox;
    Label1: TLabel;
    btnCombinar: TButton;
    chbCombinables: TCheckListBox;
    lbxResultados: TListBox;
    Label2: TLabel;
    labCombinaciones: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure chbPartidosClickCheck(Sender: TObject);
    procedure btnCombinarClick(Sender: TObject);
    procedure chb1ClickCheck(Sender: TObject);
    procedure chbCombinablesClickCheck(Sender: TObject);
  private
    { Private declarations }
    FGenerador: TGenerador;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btnCombinarClick(Sender: TObject);
var
  i: Integer;
begin
  fGenerador.Combinar;
  lbxResultados.Clear;
  for i:= 0 to fGenerador.GetTotalCombinaciones-1 do begin
    if not fGenerador.Descartado[i] then
      lbxResultados.Items.Add(fGenerador.Combinaciones[i]);
  end;
  labCombinaciones.Caption:= ' Total: ' + IntToStr(lbxResultados.Items.Count);
end;

procedure TForm1.chb1ClickCheck(Sender: TObject);
begin

  case (Sender as TCheckListBox).Tag of
    1: begin
             if (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex] then
                FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].AddValue('1')
             else FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('1');

          if not FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].Combinable then begin
             chbx.Checked[(Sender as TCheckListBox).ItemIndex]:= False;
             chb2.Checked[(Sender as TCheckListBox).ItemIndex]:= False;
          end;
       end;
    2: begin
          if (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex] then
             FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].AddValue('X')
          else FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('X');

          if not FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].Combinable then begin
             chb1.Checked[(Sender as TCheckListBox).ItemIndex]:= False;
             chb2.Checked[(Sender as TCheckListBox).ItemIndex]:= False;
          end;
       end;
    3: begin
             if (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex] then
                FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].AddValue('2')
             else FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('2');

          if not FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].Combinable then begin
             chb1.Checked[(Sender as TCheckListBox).ItemIndex]:= False;
             chbX.Checked[(Sender as TCheckListBox).ItemIndex]:= False;
          end;
       end;
  end;

  if fGenerador.GetTotalCombinaciones > RestriccionFicticia then begin
    (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex]:= False;
    case (Sender as TCheckListBox).Tag of
       1: FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('1');
       2: FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('X');
       3: FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('2');
    end;

    Raise Exception.Create('No se puede exceder de '+
                           IntToStr(RestriccionFicticia)+' combinaciones...');
  end;

  labCombinaciones.Caption:= 'Total combinaciones ' +
                             IntToStr(fGenerador.GetTotalCombinaciones);

end;

procedure TForm1.chbCombinablesClickCheck(Sender: TObject);
begin
   if not chbCombinables.Checked[chbCombinables.ItemIndex] then begin
     if chb1.Checked[chbCombinables.ItemIndex] then begin
        chb1.Checked[chbCombinables.ItemIndex]:= False;
        FGenerador.Atributos[chbCombinables.ItemIndex].DeleteValue('1');
     end;

     if chbX.Checked[chbCombinables.ItemIndex] then begin
       chbX.Checked[chbCombinables.ItemIndex]:= False;
       FGenerador.Atributos[chbCombinables.ItemIndex].DeleteValue('X');
     end;

     if chb2.Checked[chbCombinables.ItemIndex] then begin
       chb2.Checked[chbCombinables.ItemIndex]:= False;
       FGenerador.Atributos[chbCombinables.ItemIndex].DeleteValue('2');
     end;
   end;

  FGenerador.Atributos[chbCombinables.ItemIndex].Combinable:=
                               chbCombinables.Checked[chbCombinables.ItemIndex];
  labCombinaciones.Caption:= 'Total combinaciones ' +
                              IntToStr(fGenerador.GetTotalCombinaciones);
end;

procedure TForm1.chbPartidosClickCheck(Sender: TObject);
begin
  chbPartidos.Checked[chbPartidos.ItemIndex]:= True;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
  fAtrib: TAtributo;
begin
  FGenerador:= TGenerador.Create;

  for i := 0 to chbPartidos.Count - 1 do begin
     chbPartidos.Checked[i]:= True;
     fAtrib:= FGenerador.AddAtributo;
     fAtrib.Caption:= IntToStr(i);
     fAtrib.Combinable:= True;
  end;

  for i := 0 to chbCombinables.Count - 1 do begin
     chbCombinables.Checked[i]:= True;
  end;

end;

end.

Descargar

Creo que por hoy ya esta bien.

Hace un día estupendo. Y también hay que sacar tiempo para el deporte. No todo puede ser trabajo, ¿no? Además, no se si alguna vez comenté que suelo correr con frecuencia, y a lo largo de la temporada intento participar en alguna que otra carrera popular, casi siempre en la distancia Media Maratón. Creo que es importante el equilibrio personal y que debemos buscarlo en nuestro interior.

Quedan más ideas en el tintero. Las compartiremos en próximas entradas.

Cero contra quinientos sesenta ( Anexo)

marzo 26, 2009 en Ado Express & DataSnap, Advertencia, Artículos, Ayuda, ¿Sabías que...?, ¿Sabías que...? [Delphi], Código, Consejo, Delphi, Entrada Diario

No iba a escribir esta entrada pero me ha pasado algo similar a lo que me ocurrió con los comentarios sobre JediVCL, donde despues de haber acabado el segundo artículo, sentía que quedaban cosas que comentar y redacté un pequeño anexo con el que dejaba lo que me parecía mas importante zanjado. El resto de funciones se podían ver sobre la marcha. Es más cuestión del día a día, a medida que te van surgiendo las dudas. Con este tema, siento que pasa algo similar.

Hemos compartido dos entradas donde comentabamos como se generaban, y en base a que criterios, las sentencias sql que el proveedor (TDataSetProvider) finalmente “ejecuta” (o manda ejecutar). Bueno. Ya visteis que no él, sino que se apoya en una clase que crea a demanda, que se llama TSQLResolver. Concretamente, y para entrar un poco más en detalle se iniciaba todo desde la función CreateResolver

function CreateResolver: TCustomResolver; override;

Las llamadas tanto a CheckResolver que en último extremo pueden crear la clase, como a FreeResolver que se encarga de liberar el objeto instanciado, se lanzan desde la clase ascendente de nuestro TDataSetProvider.

procedure TBaseProvider.CheckResolver;
begin
  if not Assigned(FResolver) then
         FResolver := CreateResolver;
end;

procedure TBaseProvider.FreeResolver;
begin
  FResolver.Free;
  FResolver := nil;
end;

Así que esa pieza clave en la generación de la sentencia SQL, queda habilmente oculta tras nuestro TDataSetProvider, que se alza como un muro casi infranqueable (*). Atrás quedan algunos comentarios compartidos con otros compañeros donde se dejaba caer que en ocasiones podría ser necesario sobrescribir la sentencia SQL, o que les gustaría ver la cadena que se ha generado, etc….

(*) Eso de infranqueable no es del todo cierto ya que es un poco una expresión, sobretodo desde el momento en el que podemos redefinir el método protegido

function CreateResolver: TCustomResolver; override;

Podriamos obtener una instancia de dicha clase y buscar la forma de, en un descendiente de la misma, acceder al punto que deseamos. Todo es posible dedicando horas y paciencia.

Y yo desde luego, no soy demasiado partidario de tocar el codigo fuente, más que nada porque luego, las posibles actualizaciones pueden sobrescribir los cambios que podamos hacer. En este caso, y para mi uso y disfrute, he modificado unas lineas del módulo Provider. El riesgo también puede ser el de generar errores adicionales a los ya existentes (que seguro los hay). Además, no siempre recordamos pasados meses y meses desde que se hizo el cambio, exactamente las lineas que estaban implicadas por lo que cualquier actualización puede hacer que el código fuente empiece a crear problemas.

Bueno… prometo que no lo volveré a hacer. :-)

Os comenté la idea de duplicar un par de procedimientos y reservarlos para el borrado. Concretamente yo en casa me he duplicado UseFieldInWhere y GenWhereSQL y les he añadido Delete para distinguirlos pero es una solución casera.

function UseFieldInWhereDelete(Field: TField;
Mode: TUpdateMode): Boolean; virtual;

procedure GenWhereSQLDelete(Tree: TUpdateTree; SQL: TWideStrings;
 Params: TParams;  GenUpdateMode: TUpdateMode; Alias: string); virtual;

Lo correcto quizás sería cambiar la firma de esos metodos y añadir sucesivamente el parametro UpdateKind (TUpdateKind) que nos hace falta para distinguir desde UseFieldInWhere cuando se está borrando de cuando se está modificando un registro. Yo siempre digo en casos así que cada uno haga de su capa un sayo, que en cristiano viene a decir algo así como cada cual se la pele como bien pueda. ;-)

Sea como sea, entraríamos en una discusión cuasi filosófica entre los que abogarían por coger siempre en el borrado (en el where) los campos de las claves primarias y aquellos que tienen valor, para compararlos (o si prefiere… todos), como forma mas segura de saber que nada ha pasado desde el momento que cargamos en la cache de nuestro TClientDataSet el registro y fue borrado, hasta que se han aplicado los cambios. Si en ese transcurso de tiempo indeterminado el registro es modificado por otro usuario, el borrado fallaría, garantizando que el usuario que ha borrado dicho registro lo hará con el conocimiento de que se ha intentado modificar.

Ahora bien, siempre existirá tambien el que piense que no vale la pena semejante alboroto para tan pocas nueces. Si me basta conocer las claves primarias que intervienen para localizar el registro, qué necesidad puede haber de utlizar en el where de la consulta todos, o parte de los campos, en el borrado de un registro. Todo depende un poco del cristal desde el que observemos y analicemos la realidad. Si nuestra vida dependiera de que un alcaide modificara el campo perdonado para que no se ejecutase nuestra sentencia al ser borrado el registro, sin duda escogeriamos un borrado seguro, pero si fuera el caso que la información a manejar fuera intrascendente prefeririamos ir al grano y olvidarnos de historias.

Yo me he dado la oportunidad de tener lo mejor de ambos mundos por si cambio de opinión. :-)

En primer lugar he creado la propiedad en la parte publica del TDataSetProvider:

property BorradoSeguro: Boolean read FBorradoSeguro write SetBorradoSeguro;

Y puesto que he hecho cambios en el modulo provider ¡que mas me da hacer unos pocos mas!

Veamos…

procedure TSQLResolver.GenDeleteSQL(Tree: TUpdateTree; SQL: TWideStrings;
  Params: TParams; Alias: string);
begin
  with PSQLInfo(Tree.Data)^ do
  begin
    SQL.Clear;
    if Tree.IsNested then
    begin
      Alias := NestAlias;
      SQL.Add(WideFormat('delete the (select %s FROM %s %s',[QuoteFullName(Tree.Name, QuoteChar),
        PSQLInfo(Tree.Parent.Data).QuotedTable, DefAlias]));       { Do not localize }
      GenWhereSQL(Tree.Parent, SQL, Params, upWhereKeyOnly, DefAlias);
      SQL.Add(WideFormat(') %s',[Alias]));
    end else
      SQL.Add(WideFormat('delete from %s %s', [QuotedTable, Alias]));  { Do not localize }

    if GetProvider.BorradoSeguro then
      GenWhereSQL(Tree, SQL, Params, Provider.UpdateMode, Alias)
    else GenWhereSQLDelete(Tree, SQL, Params, Provider.UpdateMode, Alias);

  end;
end;

La condición ( if GetProvider.BorradoSeguro then ) y la asignación previa de la propiedad BorradoSeguro me ayudará en esta tarea.

Puestos a cambiar, también se me ocurrió sobre la marcha, incluir un evento en el TDataSetProvider que nos permita acceder a la sentencia SQL. Veamos las lineas añadidas:

  TSobrescribeSQLEvent = procedure(Sender: TObject; var SQL: TWideStringList;Params: TParams;
UpdateKind: TUpdateKind; const ATablename, AliasTablename: String) of Object;

   ...

  TDataSetProvider = class(TBaseProvider)
    ...
  private
    FOnNeedChangeSQLCommand: TSobrescribeSQLEvent;
     ...
  published
    ...
    property BorradoSeguro: Boolean read FBorradoSeguro write SetBorradoSeguro;
    ...
   property OnNeedChangeSQLCommand: TSobrescribeSQLEvent read FOnNeedChangeSQLCommand
write FOnNeedChangeSQLCommand;
  end;

Solo nos quedaría localizar donde se va a ejecutar y previamente a esa instrucción, dejar que se transmita el evento al proveedor, para que el usuario pueda acceder al mismo.

procedure TSQLResolver.InternalDoUpdate(Tree: TUpdateTree; UpdateKind: TUpdateKind);
var
  Alias: string;
  FTableName: string;
  PS2 : IProviderSupport2;
begin
  if (Supports(Tree.Source, IProviderSupport2, PS2) and
 (not (PS2.PSUpdateRecord(UpdateKind, Tree.Delta)))) or 
   (not (Tree.Source as IProviderSUpport).PSUpdateRecord(UpdateKind, Tree.Delta)) then
  begin
    if (PSQLInfo(Tree.Data)^.QuotedTable = '') and not Tree.IsNested then
      DatabaseError(SNoTableName);
    if PSQLInfo(Tree.Data)^.HasObjects then Alias := DefAlias else Alias := '';
    FSQL.Clear;
    FParams.Clear;
    case UpdateKind of
      ukModify: GenUpdateSQL(Tree, FSQL, FParams, Alias);
      ukInsert: GenInsertSQL(Tree, FSQL, FParams);
      ukDelete: GenDeleteSQL(Tree, FSQL, FParams, Alias);
    end;

    if FSQL.Text <> '' then begin
      FTableName:= PSQLInfo(Tree.Data)^.QuotedTable;
      DoSobrescribeSQL(FSQL, FParams, UpdateKind, FTableName, Alias);

      DoExecSQL(FSQL, FParams);
    end;
  end;
end;

Las lineas añadidas son:

      FTableName:= PSQLInfo(Tree.Data)^.QuotedTable;
      DoSobrescribeSQL(FSQL, FParams, UpdateKind, FTableName, Alias);

Yo he usado el método virtual y protegido DoSobrescribeSQL, con la idea de poder sobrescribirlo si me hiciera falta, pero en realidad solo encubre el disparo del evento.

procedure TSQLResolver.DoSobrescribeSQL(var SQL: TWideStringList; Params: TParams;
UpdateKind: TUpdateKind; const ATablename, AliasTablename: String);
begin
  if Assigned(GetProvider.FOnNeedChangeSQLCommand) then
     GetProvider.FOnNeedChangeSQLCommand(Self,
                                          SQL,
                                          Params,
                                          UpdateKind,
                                          ATablename,
                                          AliasTablename);
end;

En cualquier caso, el evento haría recaer sobre el programador la responsabilidad de la manipulación de la cadena, sea para el uso que sea. Incluso puede haber a quien se le ocurra algo similar para elaborar un log de movimientos reales de flujo de datos, por sesión de usuario. ¡Qué se yo lo que puede haber dentro de la cabecita de un programador!, ¡Nada bueno! :-)

Al final, cogí un pequeño ejemplo y puse en práctica el artefacto, haciendo un borrado en la tabla vArticulos, que es la tabla que me servía de muestra en las entradas anteriores.

Podeis ver una imagen de cada ejecución (una con borrado seguro y la otra sin borrado seguro):

Borrado seguro

Borrado con KeysOnly

Y estas son las pocas lineas de código que las han generado:

unit USQLDatos;

interface

uses
  SysUtils, Classes, DB, ADODB, DBClient, TConnect, Provider, WideStrings;

type
  TsqlDatos = class(TDataModule)
    dspArticulos: TDataSetProvider;
    qArticulos: TADOQuery;
    Local: TLocalConnection;
    conexion: TADOConnection;
    qArticulosIdArticulo: TIntegerField;
    qArticulosCodigoAlternativo: TStringField;
    qArticulosDescripcion: TStringField;
    qArticulosCoste: TBCDField;
    qArticulosPorcentajeBeneficio: TFloatField;
    qArticulosPrecioVenta: TBCDField;
    qArticulosFechaUltimaVenta: TDateTimeField;
    procedure DataModuleCreate(Sender: TObject);
  private
    { Private declarations }
public
    { Public declarations }
     procedure OnNeedChangeSQL(Sender: TObject; var SQL: TWideStringList;
Params: TParams; UpdateKind: TUpdateKind; const ATablename, AliasTablename: String);

  end;

var
  sqlDatos: TsqlDatos;

implementation
{$R *.dfm}

uses Dialogs;

procedure TsqlDatos.DataModuleCreate(Sender: TObject);
begin
   dspArticulos.OnNeedChangeSQLCommand:= OnNeedChangeSQL;
   dspArticulos.BorradoSeguro:= True;
end;

procedure TsqlDatos.OnNeedChangeSQL(Sender: TObject; var SQL: TWideStringList; Params: TParams;
  UpdateKind: TUpdateKind; const ATablename, AliasTablename: String);
var
  i: Integer;
  sValores: String;
begin

   for i := 0 to params.Count - 1 do
    if Not Params[i].IsNull then
       sValores:= sValores + #13#10 +
                              IntToStr(params[i].Index) + '=' + Params[i].AsString;

   case UpdateKind of
     ukModify: ;
     ukInsert: ;
     ukDelete: begin

                  if ATablename = 'vArticulos' then
                       ShowMessage(SQL.Text + #13#10 + sValores);
                //     Otra posibilidad...
                //     SQL.Clear;
                //     SQL.Add('Delete from vArticulos where IDArticulo = 1');

               end;
   end;
end;
end.

Así que despues de probarlo y de comprobar que efectivamente puedo sobrescribir la sentencia SQL me quedo más tranquilo.

Me vienen a la mente numerosas ocasiones que pensé en la necesidad de poder acceder a la cadena SQL. Cuando por ejemplo he necesitado transformar una sentencia de borrado en una actualizacion (marcando por ejemplo un campo que identifique el registro como borrado) y no ejecutar un borrado físico del registro. Con ese pequeño cambio que he hecho me podría valer. Pero también podría valerme para simplemente visualizar en una depuración la sentencia y el valor de los parámetros que realmente estan enviandose a la base de datos, sin necesidad de hacer una traza en una herramienta adicional, que puede no existir.

Espero que os puedan ayudar estos comentarios.

Cero contra quinientos sesenta. (Parte 1)

marzo 21, 2009 en Ado Express & DataSnap, Artículos, Ayuda, ¿Sabías que...?, ¿Sabías que...? [Delphi], Código, Consejo, Delphi, Entrada Diario

Parece un título un tanto extraño pero harto de buscar el más apropiado, o al menos el que me parecía mas ingenioso, he acabado escogiendo el mas evidente en la historia que narra esta entrada.

Este es el protagonista:

Error TDatasetProvider

Como podeis ver, se trata una notificacion de error, que es lo que se puede desprender de la imagen.

Un dato mas: la aplicación utiliza AdoExpress y DataSnap, accediendo a un servidor de SQL Server.

Con ese supuesto, quizás la intuición nos llevaría a suponer que el problema surge en una actualizacion, es decir, en un Update (más que nada por la caché de datos y el sistema de conciliacion). O quizás a causa de un trigger, un disparador del gestor de datos, que ha podido alterar el contenido del registro (se supone que lanzado desde otro proceso). O la mas probable, a causa de la interacción de otro usuario, que ha decidido caprichosamente modificar la misma ficha de un cliente o de un artículo, suponiendo que fuera este el cambio. Son ideas que a uno le pueden venir a la mente intentando encontrar un sentido al mensaje de error.

El problema es que no era una actualización sino un borrado (ese dato ya lo aporto ahora) y eso ya es un poco mas dificil de digerir. Manuel trataba de borrar la ficha de un cliente, en una de esas tardes en las que parte del tiempo se nos va en la depuración del codigo que hemos escrito. Y la aplicación siempre se obstinaba en enviarnos un mensaje que tenía poco sentido o ninguno. Porque entre otras cosas, solo quedabamos en la empresa a esas horas el y yo, y no había nada que pudiera afectar al registro desde el momento en el que el presionaba el ok del formulario hasta que se rascaba la cabeza.

Bueno. Cierto es que tengo que admitir que tenía poco sentido porque pensabamos, lo habiamos leído y eramos capaces de perjurar que era así, que si optabamos por un modo de actualización upWhereChanged en nuestro proveedor TDataSetProvider, toda vez que en una sentiencia de actualizacion iban a compararse los campos marcados como clave primaria y aquellos que habían sido modificados, en una sentencia de borrado tan solo iban a compararse los que contenian el flag pfInKey, o lo que es lo mismo, los que el usuario había marcado como clave primaria.

Ese es el problema… ¡uno da por supuesto cosas que no siempre son verdad!…

Cuenta una leyenta urbana que en el presidio de Ula-Ula los presos esperaban con paciencia en el corredor de la muerte. Uno de ellos, el protagonista de esta historia se llamaba Palomo Confiado. Y cuentan además que habían pasado los años y desde su llegada al penal, ya nunca habían podido ejecutar a nadie. El director del presidio tenía una regla inmutable: Para que una ejecución se llevara a efecto, tenía que eliminar la ficha del recluso de su aplicación. Así que cada vez que tenía que cumplir con su cargo y enviar un recluso al otro mundo, ejecutaba su aplicación y tras localizar la ficha del reo, simplemente bastaba pulsar el botón de suprimir y se ponía en marcha el mecanismo que iniciaba el tramite de la ejecución.

Hasta la llegada de Palomo al presidio todo funcionaba bien. Los presos iban desfilando por el corredor de la muerte, animando las aburridas tardes de la ciudad. Pero… siempre hay un pero. Palomo Confiado cambió todo de la noche a la mañana porque había nacido con buena estrella.

Un día le dijo Palomo al programador del presidio, que por cierto, también programaba en Delphi con AdoExresss y DataSnap:

-Para que quieres pasar trabajo asignando la fecha de alta cada vez que ingresa un nuevo preso. No. No te preocupes que de eso ya se encarga tu gestor de datos. Olvidate de asignar el valor desde Delphi y limítate a poner como valor por defecto el retorno de la función GetDate(), en el campo FECHAALTA de tu tabla de PRESOS.

Tabla de presos

Aquella tarde el programador siguió el consejo de Palomo y desde aquel momento ya no pudieron ejecutar a nadie más… ;-)

JEDI Version Control System (Anexo)

marzo 2, 2009 en Advertencia, Artículos, Ayuda, Consejo, Delphi, Entrada Diario, Recordatorio, VCS (Control versiones)

Suelo releer las entradas al día siguiente de escribirlas y casi siempre acabo corrigiendo algo de lo que escribí el día anterior. En realidad, es casi una pequeña manía mía, porque suelo llevar el máximo cuidado tanto a nivel de contenido como de la presentación del texto. No pasa nada pero creo que ese detalle de cuidar también la forma en la que nos expresamos, al final resulta agradecida por las personas que leen estas entradas. No…, no me refiero a que tengamos que hablar como Quevedo o Góngora sino que no es incompatible expresarnos correctamente con la valoración del contenido. Tambien existen otras razones, como que pueda existir algún párrafo que no me acabe de convencer o incluso otros motivos que tienen relación directa con el contenido, como puede ser alguna palabra mal escrita al teclear, algún acento que se escapa o simplemente que se han omitido las comas necesarias que hacen la lectura mas agradable.

En esta ocasión, he hecho una excepción ya que despues de releer la entrada anterior, he recordado un punto al hilo de lo comentado, que finalmente olvidé mencionar, y se prestaba a reabrir la entrada para ampliar esa pequeña nota. En su lugar, he preferido abrir otra entrada y calificarla como Anexo.

Es una tontería y nada mas empezar a trabajar con JediVCS se os hubiera planteado. No obstante no está de mas comentarlo.

Veamos…

Supongamos que dos usuarios estan compartiendo el proyecto y uno de ellos ha añadido un nuevo módulo. Como comentabamos en la entrada anterior, dicho módulo solo existiría en su repositorio local, que es donde nuestro programador está desarrollando. Así que de alguna forma, se le plantea en ese momento cual es el mecanismo que debe seguir para dar de alta el nuevo formulario dentro del sistema de control de versiones (supongamos que sea un formulario). Una opción posible podría ser abrir el cliente de JediVCS y añadirlo directamente (existe un boton en una de las barras superiores que permite añadir modulos al proyecto), de forma que al sincronizar los archivos su compañero, se detectaría y sería añadido también localmente. El problema es que, aunque puede compilar el usuario B, el hecho de que tenga el modulo añadido en su repositorio local no implica que exista en su archivo de proyecto, lo cual obligaría a añadir de forma manual en Delphi los nuevos módulos al proyecto.

En este caso concreto, creo que lo mas sencillo es que el usuario A , antes de añadir el nuevo módulo, haga un CheckOut de los archivos del proyecto (dpr, dproj), para que al finalizar la tarea de añadirlo, al ser devueltos también se sincronicen en el usuario B. Recordad que una vez añadido en el dpr y este archivo de proyecto exista en JediVCS, automaticamente se detectarían las nuevas unidades y se añadirían de forma automática al usuario B.

Como véis, se desprende de la mecánica misma de trabajo de nuestro servicio JediVCS y tarde o temprano hubiera surgido la necesidad de ampliar el proyecto. Así que conocerlo previamente no está de mas.