blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

lundi 7 décembre 2009

Les formulaires de mise à jour en ASP.NET MVC

J'avais prévu de finir la sixième partie du tutoriel NerdDinner la semaine dernière et ça n'a pas marché. Par rapport aux autres parties, je ne peux pas dire que ça soit beaucoup plus compliqué à suivre, c'est juste que c'est un peu plus long. Sans compter que les conditions de travail étaient particulièrement bruyantes cette semaine (quoique ?).

Ce qui fait que j'ai préféré me disperser plutôt que de rester des heures sans pouvoir réellement avancer :

Ces petits écarts ont quand même réussi à me maintenir à peu près d'attaque pour venir à bout du tutoriel. Mais par contre, ça n'a pas été suffisant pour progresser autant sur la traduction.

Le problème, c'est que si dans la journée je patauge pour suivre le tutoriel, ça fait qu'en soirée j'ai beaucoup de mal à avancer sur la traduction puisque c'est des trucs que je n'ai pas encore vu ou suffisamment assimilés... C'est pour cela que malgré mes prévisions pourtant très mesurées, je n'ai pour l'instant réussi à traduire que les 12 premières pages du tutoriel, consacrées majoritairement à la partie Edit de l'application NerdDinner.

Et pour la suite, je n'ai qu'un tout petit plus de la moitié des pages restantes prêtes pour la relecture (et malheureusement, comme j'ai fait ça dans le désordre, je ne suis pas sûr de pouvoir mettre en ligne les pages consacrées au ModelState avant mercredi).

Alors pour me reposer de ma semaine, je me suis laissé aller à programmer pour de vrai. Ca m'a permis de pas mal améliorer le système de rechercher / remplacer qui fait passer le fichier de dans lequel j'enregistre la traduction du format Word au format Wiki de Dotclear. Pour cela, je suis parti de l'application CleanWordHtml de Jeff Atwood. Maintenant, je n'ai plus qu'à exporter la traduction au format HTML filtré et à lancer ma petite application et le résultat obtenu est quasiment prêt à coller sous Dotclear.

Et grâce à ça, j'ai découvert l'existence de deux fonctions trop pratiques : File.ReadAllText() et File.WriteAllText(). C'est des petits trucs comme ça qui devraient suffire à tout le monde pour admettre qu'il y a encore beaucoup de travail avant de pouvoir réaliser proprement une vraie application en ASP.NET 3.5 (MVC ou non). Ou alors, on se contente simplement de refaire plus ou moins bien en .NET 3.5 ou 4.0 ce qu'on a l'habitude de faire en .NET 1.1...

mercredi 25 novembre 2009

VS 2008, SQL Server 2008 Express et Windows 7 64 bits

Depuis que j'ai mon nouveau portable, je galère comme pas possible pour m'y sentir comme chez moi et laisser tomber mon ancien PC. La principale cause de tout ces problèmes (mais pas la seule), c'est que je suis maintenant sous Windows 7 en version 64 bits (alors que j'aurais très bien pu m'en passer vu que je reste scotché à 4 GO de mémoire à cause d'un tranchement entre fromage ou dessert).

Le dernier problème en date c'est SQL Server Express 2008 (avec Service Pack 1 s'il vous plait). J'ai commencé par passer un temps fou à installer / désinstaller / réinstaller / combiner / décombiner / recombiner Visual Studio 2008 Professional Edition, SQL Server Express 2005 et SQL Server Express 2008 pour réussir en début d'après-midi à faire fonctionner mes quelques essais d'applications ASP.NET MVC que j'avais créées sur mon vieux PC sous XP.

Ce matin, j'avais encore le message d'erreur suivant :

The database 'BLABLA.MDF' cannot be opened because it is version 655. This server supports version 612 and earlier. A downgrade path is not supported.

(en fait, mon message d'erreur à moi était en français mais je l'ai perdu, si c'est pas malheureux...)

Je bataille tant que je peux pour désinstaller complètement SQL Server Express 2005 puis installer SQL Server Express 2008 à partir du Web Platform Installer et ça le fait presque : je n'ai plus d'erreur quand je lance mes projets ContactManager et NerdDinner !!!

Mais il reste encore un problème : pas possible de créer une nouvelle base de données. Je vais sur App_Data, je clique-droit, je fais "Ajouter", je fais "Nouvel élément...", je choisis "Base de données SQL Server", je laisse le nom "Database1.mdf" par défaut, je clique sur le bouton "Ajouter" et patatras :

SQL Server Express 2005 doit fonctionner correctement pour permettre les connexions aux fichiers SQL Server (*.mdf). Vérifiez l'installation du composant ou téléchargez-le à partir de l'URL suivante : http://go.microsoft.com/fwlink/?LinkId=49251

ou en anglais pour faciliter les recherches sous Google :

Connections to SQL Server files (*.mdf) require SQL Server Express 2005 to function properly. Please verify the installation of the component of download from the URL: http://go.microsoft.com/fwlink/?LinkId=49251 (http://go.microsoft.com/fwlink/?LinkId=49251)

Et en fait c'est bigrement compliqué pour trouver la bonne réponse sous Google, mais après des tas de liens, je réussi à tomber sur LA solution : c'est une fonctionnalité bien connue (en tout cas depuis janvier 2009) de Visual Studio 2008 SP1 quand on se connecte à une instance 64 bits de SQL Server Express 2008. Y'a même un article n° 957944 tout exprès pour ça dans la Knowledge Base de Microsoft : FIX: Error message when you connect to a 64-bit instance of SQL Server Express 2008 by using Visual Studio 2008 Service Pack 1 (SP1): "Connections to SQL Server files (*.mdf) require SQL Server Express 2005 to function properly".

Comme je suis quelqu'un de très conciliant, je fait tout ce qu'on me demande et j'appelle le support Microsoft. Je me retrouve je ne sais trop où (hello la verte Erin ?) et après quelques interludes musicaux y'a un monsieur qui vérifie une dernière fois que je j'ai bien un problème puis me demande mon adresse mél pour m'envoyer le correctif.

J'aurais lui donner mon adresse GMail parce que ça fait une demi-heure et je ne vois toujours rien venir...

Mise à jour 24 heures après

Toujours pas de nouvelle de Microsoft. Et pourtant, j'ai aussi mis sur le coup un ex-collègue qui a travaillé à Dublin dans un centre d'appel à destination des développeurs MS. Comme j'en ai eu marre d'attendre, j'ai lancé une recherche sur "hotfix 957944" et j'ai eu droit à tout plein de réponses, dont une sur StackOverflow que je me suis empressé de consulter en premier. Et là, ils expliquent même comment télécharger le fameux hotfix 957944 en direct...

Je fais la manipulation indiquée => ça me demande mon adresse mail. Ce coup-ci je donne mon compte gmail et même pas 5 secondes après j'ai un nouveau message dans mon Inbox avec un lien pour aller récupérer ce satané hotfix. Je télécharge, j'installe, ça décompacte un VS90SP1-KB957944-x86.exe que je lance, ça mouline et c'est fini ! Reste plus qu'à lancer VS 2008 pour vérifier que je peux enfin créer une base de données et ça marche !!!

La meilleure façon d'obtenir quelque chose de Microsoft, c'est de demander à Google d'aller rechercher sur StackOverflow :)

vendredi 13 novembre 2009

Portage du tutoriel Contact Manager sous LINQ to SQL

Encouragé par un premier succès pour faire passer le tutoriel ContactManager en tout jQuery, j'ai décidé de continuer ma formation ASP.NET MVC en modifiant l'application ContactManager pour remplacer Entity Framework par LINQ to SQL.

Création du modèle LINQ to SQL

Après un ou deux essais, j'ai vu que le plus pratique était de commencer par exclure ou supprimer du projet tout ce qui touchait à Entity Framework, à savoir ContactManagerModel.edmx et EntityContactManagerRepository.cs. Il devient alors possible d'ajouter un modèle LINQ to SQL au projet pour représenter la base de données, sans que cela provoque des définitions en double avec les objets générés par Entity Framework.

L'ajout d'un modèle LINQ to SQL est plutôt simple :

  • se placer dans le dossier Models de l'explorateur de solution
  • faire un clic droit pour sélectionner la commande "New items..."
  • dans la catégorie "Data", choisir le modèle "LINQ to SQL Classes"
  • le nommer "ContactManagerModel.dbml" et cliquer sur le bouton "Add" pour le créer

Le concepteur LINQ to SQL est alors automatiquement ouvert et il est possible d'y faire glisser les deux tables "Contacts" et "Groups" depuis l'explorateur de base de données, ce qui a pour effet de :

  • créer les classes "Contact" et "Group" basées sur ces deux tables
  • ajouter une propriété "Group" à la classe "Contact" pour refléter le côté "1" de la relation entre les deux tables
  • ajouter une propriété "Contacts" à la classe "Group" pour refléter le côté "N" de la relation entre les deux tables

Cela génère aussi une classe ContactManagerModelDataContext.cs qui fourni deux propriétés "Contacts" et "Groups" permettant de gérer des objets "Contact" et "Group" issus de la base de données.

Concrètement, cette première étape a eu pour résultat de substituer ContactManagerModel.dbml à ContactManagerModel.edmx.

Création d'un Repository basé sur LINQ to SQL

Cette seconde étape va consister à remplacer EntityContactManagerRepository.cs par une classe L2SContactManagerRepository.cs qui implémente elle aussi l'interface IContactManagerRepository. Et au lieu d'utiliser un objet ContactManagerDBEntities, cette nouvelle classe va utiliser un objet ContactManagerModelDataContext.

using System.Collections.Generic;
using System.Linq;

namespace ContactManager.Models
{
    public class L2SContactManagerRepository : IContactManagerRepository
    {
        private ContactManagerModelDataContext _context = new ContactManagerModelDataContext();

        ...
    }
}

Il reste alors à implémenter les différentes méthodes déclarées dans l'interface IContactManagerRepository, mais à la sauce LINQ to SQL. Par rapport au repository basé sur Entity Framework, les différences sont finalement assez mineures.

  • Les objets _entities.ContactSet et _entities.GroupSet deviennent des objets _context.Contacts et _context.Groups :
// ---------- Entity Framework
public IEnumerable<Group> ListGroups()
{
    return _entities.GroupSet.OrderBy(o => o.Name).ToList();
}

// ---------- LINQ to SQL
public IEnumerable<Group> ListGroups()
{
    return _context.Groups.OrderBy(o => o.Name).ToList();
}
  • Les méthodes pour la mise à jour des données ont des noms différents : AddToXxxxxxSet() devient InsertOnSubmit(), DeleteObject() devient DeleteOnSubmit() et SaveChanges() devient SubmitChanges() :
// ---------- Entity Framework
public Group CreateGroup(Group groupToCreate)
{
    _entities.AddToGroupSet(groupToCreate);
    _entities.SaveChanges();
    return groupToCreate;
}

// ---------- LINQ to SQL
public Group CreateGroup(Group groupToCreate)
{
    _context.Groups.InsertOnSubmit(groupToCreate);
    _context.SubmitChanges();
    return groupToCreate;
}
  • La méthode ApplyPropertyChanges() de Entity Framework n'ayant apparemment pas d'équivalent en LINQ to SQL, et le helper UpdateModel() étant lié à la classe Controller, je me suis pour l'instant contenté d'une mise à jour des propriétés à la main pour retrouver la même fonctionnalité :
// ---------- Entity Framework
public Contact EditContact(int groupId, Contact contactToEdit)
{
    // Get original contact
    var originalContact = GetContact(contactToEdit.Id);
    
    // Update with new group
    originalContact.Group = GetGroup(groupId);
    
    // Save changes
    _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
    _entities.SaveChanges();
    return contactToEdit;
}

// ---------- LINQ to SQL
public Contact EditContact(int groupId, Contact contactToEdit)
{
    // Get original contact
    var originalContact = GetContact(contactToEdit.Id);

    // Update with new group
    originalContact.Group = GetGroup(groupId);

    // Update properties
    originalContact.FirstName = contactToEdit.FirstName;
    originalContact.LastName = contactToEdit.LastName;
    originalContact.Phone = contactToEdit.Phone;
    originalContact.Email = contactToEdit.Email;

    // Save changes
    _context.SubmitChanges();
    return contactToEdit;
}

Malgré tout, il y a une autre "grosse" différence dans la mesure où avec Entity Framework on utilise la méthode Include(relation) dans la requête LINQ pour retrouver les objets associés à l'élément sur lequel on effectue la requête.

Concrètement, cela a pour effet de générer une jointure lors de l'exécution de la requête LINQ.

  • Sans le Include(relation) :
return (from c in _entities.ContactSet
        where c.Id == id
        select c).FirstOrDefault();

=>  SELECT 1 AS [C1], 
                [Extent1].[Id] AS [Id], 
                [Extent1].[FirstName] AS [FirstName], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[Phone] AS [Phone], 
                [Extent1].[Email] AS [Email], 
                [Extent1].[groupId] AS [groupId]
        FROM [dbo].[Contacts] AS [Extent1]
        WHERE [Extent1].[Id] = @p__linq__1
  • Avec le Include(relation) :
return (from c in _entities.ContactSet.Include("Group")
        where c.Id == id
        select c).FirstOrDefault();

=> SELECT 1 AS [C1], 
                [Extent1].[Id] AS [Id], 
                [Extent1].[FirstName] AS [FirstName], 
                [Extent1].[LastName] AS [LastName], 
                [Extent1].[Phone] AS [Phone], 
                [Extent1].[Email] AS [Email], 
                [Extent2].[Id] AS [Id1], 
                [Extent2].[Name] AS [Name]
        FROM  [dbo].[Contacts] AS [Extent1]
        LEFT OUTER JOIN [dbo].[Groups] AS [Extent2] ON [Extent1].[groupId] = [Extent2].[Id]
        WHERE [Extent1].[Id] = @p__linq__1

Là aussi, je n'ai pas réussi à trouver d'équivalent pour LINQ to SQL, d'autant que dans ce cas je n'ai pas cherché avec beaucoup d'acharnement puisque l'application fonctionne parfaitement même si on s'en passe...

// ---------- Entity Framework
public Contact GetContact(int id)
{
    return (from c in _entities.ContactSet.Include("Group")
            where c.Id == id
            select c).FirstOrDefault();
}

// ---------- LINQ to SQL
public Contact GetContact(int id)
{
    return (from c in _context.Contacts
            where c.Id == id
            select c).FirstOrDefault();
}

Isoler les tests unitaires de l'ORM

Une fois la classe Repository complètement ré-écrite pour s'appuyer sur LINQ to SQL et plus Entity Framework, le projet Contact manager compile sans problème. Encore faut-il penser à modifier la couche de service pour instancier un objet IContactManagerRepository à partir de L2SContactManagerRepository :

public ContactManagerService(IValidationDictionary validationDictionary)
    : this(validationDictionary, new L2SContactManagerRepository())
{
}

Par contre, la compilation de la solution provoque une erreur parce que les tests unitaires font appel à la méthode CreateContact() de la classe Contact. Apparemment, Entity Framework génère automatiquement une méthode CreateContact() pour ses objets, alors que LINQ to SQL n'en fait rien !

Cela met en évidence deux problèmes :

  • chez Microsoft, ils sont si peu opiniated qu'ils partent dans tous les sens
  • les tests unitaires du Contact Manager sont fortement liés à l'ORM Entity Framework et ça, c'est pas bon du tout

Dans un premier temps, j'ai tout simplement piraté la méthode CreateContact() générée par Entity Framework et ajouté une classe partielle Contact :

namespace ContactManager.Models
{
    public partial class Contact
    {
        /// <summary>
        /// Create a new Contact object.
        /// </summary>
        /// <param name="id">Initial value of Id.</param>
        /// <param name="firstName">Initial value of FirstName.</param>
        /// <param name="lastName">Initial value of LastName.</param>
        /// <param name="phone">Initial value of Phone.</param>
        /// <param name="email">Initial value of Email.</param>
        public static Contact CreateContact(int id, string firstName, string lastName, string phone, string email)
        {
            Contact contact = new Contact();
            contact.Id = id;
            contact.FirstName = firstName;
            contact.LastName = lastName;
            contact.Phone = phone;
            contact.Email = email;
            return contact;
        }
    }
}

Mais j'ai trouvé ça un peu trop spécifique pour LINQ to SQL (puisque c'est redondant avec le code généré par Entity Framework) et j'ai finalement préféré ajouter une méthode CreateContact() utilisant la syntaxe "Object initializers" à la classe ContactManagerServiceTest des tests unitaires :

private Contact CreateContact(int id, string firstName, string lastName, string phone, string email)
{
    Contact contact = new Contact() { Id = id, FirstName = firstName, LastName = lastName, Phone = phone, Email = email };
    return contact;
}

Puis remplacer les appels à Contact.CreateContact() dans ContactManagerServiceTest.cs par de simples appels à CreateContact().

Une fois cette dernière modification réalisée, la solution compile sans erreur, les tests unitaires continuent à être OK et l'application fonctionne sans problème. Je peux :

  • lister les contacts rattachés à un groupe,
  • ajouter, modifier ou supprimer un contact,
  • ajouter ou supprimer un groupe de contacts.

Conclusion

Suite à mes différents essais avec les tutoriels ContactManager et NerdDinner, j'avais eu le pressentiment que cette notion de repository et de service pouvait rendre une application indépendante de la façon d'implémenter l'accès à la base de données.

La réalisation de ce petit exercice m'a permis de vérifier cette impression puisque hormis une ligne de la couche service, seule la classe Repository a été mise à jour. Par conséquent, si on fait les choses comme il faut, on ne se retrouve pas lié corps et âme à un ORM mais au contraire complètement libre d'en changer dans le cas où il ne serait pas satisfaisant.

Dans la pratique, cela veut dire qu'il ne faut pas se focaliser sur des détails techniques mais sur des problèmes réels. Et notamment décider si on conserve une base de données pourrie pléthorique et mal conçue ou si on fait table rase des errements du passé pour construire une vrai base de données et de vrais objets adaptés uniquement à nos besoins actuels.

jeudi 8 octobre 2009

Tuer du code pour réussir

Dans les mois à venir, je vais avoir à participer à la re-écriture en ASP.NET MVC d'une application existante développée depuis des années en ASP.NET 1.1.

Après de longues tergiversations, il a bien fallu reconnaitre qu'une simple migration en ASP.NET 3.5 et de simples rustines n'étaient ni suffisantes ni envisageables (même pas en rêve mon ami !).

Le problème avec cette application, c'est qu'elle est le résultat d'un empilement continuel de nouvelles fonctionnalités (une fuite en avant perpétuelle ?) et qu'à aucun moment il a été accepté de prendre des pauses pour réaliser les consolidations nécessaires si on voulait que l'ensemble puisse évoluer (voire fonctionner) sereinement.

Ce qui fait qu'aujourd'hui, on a la chance d'avoir une grosse ... avec dans le tas des trucs certainement utiles mais aussi des trucs complètements stupides, des trucs réalisées pour faire plaisir à des gens qui n'ont jamais mis les pieds dans l'application, des trucs développés et toujours pas passés en production 1 an près, des trucs pour que "ça se fait tout seul y a rien à faire", des trucs pour contourner d'autres trucs qui ne marchent pas comme on veut qui faudrait que ça marche, des trucs qu'on est pas sûr que ça serve mais yaka les laisser, des trucs qui prennent une éternité pour faire 3 fois rien, des trucs qui ont été demandés parce qu'on sait jamais ça pourrait toujours servir un jour ou l'autre...

Pour résumer ma pensée, il est plus qu'indispensable de commencer par un énorme travail de réflexion sur le contenu de ce ramassis avant même de penser à démarrer un Visual Studio. Et par réfléchir j'entends supprimer ce qui ne sert pas, supprimer ce qui ne marche pas, supprimer ce qui n’est pas indispensable, supprimer ce qui n’est pas faisable, supprimer ce qui prendra trop de temps à faire, etc… En bref éviter de re-développer des fonctionnalités en pagaille et prendre le risque de re-venir à notre point de départ...

Mon souci c'est que le spectre d'une migration iso-fonctionnelle flotte encore dans l'air : on part de tout ça et au final on veut toujours tout ça (et un peu plus) mais en mieux. Et comme on est très conciliants, on pourrait à la rigueur accepter de sacrifier quelques écrans à condition qu'on retrouve leurs fonctionnalités dans d'autres écrans...

Voilà donc un de mes stress du moment. Mais ce matin, qu'est-ce que je découvre dans mon Bloglines ? Un article des créateurs de Scout qui explique qu'en supprimant des tonnes de code (y compris des fonctionnalités), ils ont amélioré leur application tant d'un point de vue performance (jusqu'à x 10 sur les traitements les plus couteux) que satisfaction de leurs clients. Et en plus ils nous nous livrent les deux leçons que l'on peut en retirer :

1. Arrêter de vouloir faire simple pour l'utilisateur final au prix d'une tarabiscotisation dans le code, mais chercher la simplicité dans la façon de faire (et perso, je pense que si c'est simple à faire, c'est évident à utiliser).

It turns out that simplicity and elegance for the end user can mean some awfully complicated stuff behind the scenes to make it work.

2. Arrêter de perdre son temps sur l'ajout de nouvelles fonctionnalités soi-disant incontournables qui vont cannibaliser notre temps et notre énergie juste pour les mettre au point

If you’re running a web application and the majority of your work is spent delivering the service you advertised to your customers, you’re probably in bad shape.

Ca, ça me plait !

vendredi 5 juin 2009

Ouvrir un document en dehors du navigateur

Quand on vient comme moi de changer de PC et que son nouveau Microsoft Internet Explorer a la facheuse habitude d'ouvrir les fichiers Word dans le navigateur plutôt que de lancer le Microsoft Word qui va bien, il ne faut pas hésiter à aller trifouiller dans le paramètres de l'explorateur de fichiers :

  • cliquer sur le menu "Outils"
  • choisr le sous-menu "Option des dossiers..."
  • aller sur l'onglet "Types de fichiers"
  • rechercher la ligne "DOC - Document Microsoft Word" (c'est dans l'ordre alphabétique)
  • cliquer sur le bouton "Avancé"
  • décocher "Parcourir dans une même fenêtre"
  • valider en cliquent sur le bouton "OK"
  • fermer la fenêtre "Option des dossiers" en cliquant sur le bouton "Fermer"

Pour faire bonne mesure et parce qu'on a rien à perdre, le mieux c'est ensuite de fermer les fenêtres de toutes les applications en cours, d'arrêter l'ordinateur puis de le redémarrer après avoir compté jusqu'à 5 (un, deux, trois, quatre et cinq).

Ou bien alors, avant d'en arriver à cette extrémité, on peut aussi en profiter pour répéter l'opération pour les fichiers Microsoft Excel, Adobe PDF, etcétéra...

jeudi 13 novembre 2008

Installer le client ODBC pour Sybase SQL Anywhere 9

Pour mon super état des marges ultra prioritaire, je dois intégrer des données en provenance de la paye. Le problème c'est que le logiciel de paie utilise une base de données SQL Anywhere 9 et qu'il faut donc que je trouve comment m'y connecter. Et ben c'est pas si évident que ça pour trouver de l'information sur comment faire et encore moins pour arriver à récupérer un client pour Sql Anywhere 9 qui fasse l'affaire. Ou alors il faut être prêt à se tartiner des kilomètres de documentation et installer à tire larigot jusqu'à tomber sur le truc qui va bien.

Mais quand on a autant de temps que moi et qu'on ne veut pas encombrer son PC de tout un tas de trucs inutiles, il faut savoir se débrouiller (et aussi se contenter d'ODBC).

Pour commencer, il faut aller dans le dossier C:\Program Files\Sybase\SQL Anywhere 9\win32 d'une machine où SQL Anywhere 9 est déjà installé.

Une fois là, récupérer les 3 fichiers suivants :

  • dbodbc9.dll
  • dblgen9.dll
  • dbcon9.dll

Eventuellement, on peut aussi prendre les 2 fichiers suivants :

  • dblgfr9.dll : dblgen9.dll en français
  • dbcoen9.chm : fichier d'aide

Créer un dossier "C:\SQL Anywhere 9" et y copier les fichiers.

On dispose maintenant d'un répertoire contenant tous les fichiers nécessaires pour le bon fonctionnement du pilote ODBC de Sybase SQL Anywhere 9. Il reste simplement à configurer la base de registre pour qu'il soit possible de se connecter à Sybase via le pilote ODBC.

On continue et on lance regedit.exe pour ouvrir la base de registre

Aller dans HKEY_LOCAL_MACHINE \ SOFTWARE \ ODBC \ ODBCINST.INI. Là il faut se placer sur la clé "ODBC Drivers" et demander à y créer une nouvelle valeur chaine que l'on appellera "SQL Anywhere 9". Une fois cette valeur créée, double-cliquer dessus pour y enregistrer la valeur "Installed".

Rester dans HKEY_LOCAL_MACHINE \ SOFTWARE \ ODBC \ ODBCINST.INI et y créer une nouvelle clé "SQL Anywhere 9" (le même nom que la chaine créée auparavant). Se placer ensuite sur la clé que l'on vient de créer et ajouter les 2 valeurs chaines suivantes :

  • Driver
  • Setup

Puis pour chacune de ces deux chaines, enregistrer la valeur "C:\SQL Anywhere 9\dbodbc9.dll".

Ca y est, c'est quasiment fini.

On peut maintenant lancer l'administrateur de sources de données ODBC, cliquer sur le bouton "Ajouter" dans l'onglet "Sources de données système" et choisir le pilote "SQL Anywhere 9" pour obtenir la boite de dialogue permettant de configurer la connexion ODBC au serveur SQL Anywhere 9 :

odbc-sql-anywhere-9.png

A partir de là, il suffit de renseigner les différents informations qui vont bien, soit dans mon cas à peu près :

  • Onglet ODBC - Nom de source de données : BasePayeProd
  • Onglet ODBC - Description : Base de données Sybase de production pour la paie
  • Onglet Connexion - ID utilisateur : XXXXXX
  • Onglet Connexion - Mot de passe : XXXXXXXXX
  • Onglet Base de données - Nom du serveur : BasePayeProd
  • Onglet Réseau - TCP/IP : coché

Puis revenir sur l'onglet ODBC pour vérifier que tout est OK en cliquant sur le bouton "Tester la connexion".

Et quand on a comme moi on a la chance d'avoir un ODBCTST.EXE from Oracle, on peut immédiatement se connecter à la base de données pour vérifier que ça marche pour de vrai.

Ensuite, sous Visual Studio, on peut enfin se connecter à la base de donnée Sybase avec la commande suivante :

OdbcConnection db = new OdbcConnection("DSN=BasePayeProd");

lundi 29 septembre 2008

Répéter un formulaire PDF sur plusieurs pages

Pour parvenir à faire un état sur plusieurs pages en remplissant plusieurs fois le même formulaire PDF avec des données différentes, je n'ai finalement pas eu trop à me creuser la tête. J'ai simplement interrogé le tout nouveau site Stack Overflow qui m'a gentiment expliqué How do I programmatically create a PDF in my .NET application?

namespace Altrr.iText {

    using System;
    using System.Collections;
    using System.IO;
    using iTextSharp.text;
    using iTextSharp.text.pdf;

    /// <summary>
    /// Tests du composant iTextSharp
    /// </summary>
    public class Start {

        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        static void Main (string[] args) {

            string pdfSource = @"D:\Altrr\iText\register_form1.pdf";
            if (args.Length == 1) {
                pdfSource = args[0];
            }
            FillFields(pdfSource);

        }

        /// <summary>
        /// Remplis les différents champs d'un formulaire PDF
        /// </summary>
        static void FillFields (string pdfSource) {

            // Création d'un objet PDF Reader basé sur le formulaire PDF
            Console.WriteLine(pdfSource + " :");

            string pdfRempli = pdfSource.Replace(".pdf", "_test.pdf");
            Document doc = new Document();
            PdfCopy copy = new PdfCopy(doc, new FileStream(pdfRempli, FileMode.Create));
            doc.Open();

            for (int i = 0; i < 10; i++) {

                PdfReader pdfReader = new PdfReader(pdfSource);

                MemoryStream temp = new MemoryStream();
                PdfStamper pdfStamper = new PdfStamper(pdfReader, temp);

                AcroFields fields = pdfStamper.AcroFields;
                fields.SetField("person.name", i.ToString() + "Laura Specimen");
                fields.SetField("person.address", i.ToString() + "Paulo Soares Way 1");
                fields.SetField("person.postal_code", "F00b4R", "FOOBAR");
                fields.SetField("person.email", i.ToString() + "laura@lowagie.com");
                fields.SetField("person.programming", "JAVA");
                fields.SetField("person.language", "FR");
                fields.SetField("person.preferred", "EN");
                fields.SetField("person.knowledge.English", "On");
                fields.SetField("person.knowledge.French", "On");
                fields.SetField("person.knowledge.Dutch", "Off");

                pdfStamper.FormFlattening = true;
                pdfStamper.Close();

                PdfReader tempReader = new PdfReader(temp.ToArray());

                copy.AddPage(copy.GetImportedPage(tempReader, pdfReader.NumberOfPages));
                copy.FreeReader(tempReader);
            }
            doc.Close();
        }

    }
}

Pour que ça marche, j'ai dû ajouter un using iTextSharp.text; sans quoi le type 'Document' n'est pas référencé.

Et j'utilise aussi pdfStamper.FormFlattening = true; pour que le formulaire PDF ne soit plus modifiable. En fait, cela fait que le fichier PDF final redevient un simple PDF sans plus aucun champ saisissable.

Pour l'instant, je vais déjà utiliser ça comme ça parce que l'échéance pour la mise à jour de l'état approche à grands pas. Puis quand j'aurais fini j'essaierai de comprendre un peu mieux ce qui se passe et comment ça marche.

vendredi 26 septembre 2008

Remplir les champs d'un formulaire PDF

Après avoir réussi à lister les champs d'un formulaire PDF, ce coup-ci je fais un premier essai pour mettre des données dans mon formulaire. Pour commencer, je me contente d'y mettre un compteur numérique, ce qui va me permettre de repérer chaque champ dans le formulaire.

namespace Altrr.iText {

    using System;
    using System.Collections;
    using System.IO;
    using iTextSharp.text.pdf;

    /// <summary>
    /// Tests du composant iTextSharp
    /// </summary>
    public class Start {

        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        static void Main (string[] args) {

            string pdfSource = @"D:\Altrr\iText\register_form1.pdf";
            if (args.Length == 1) {
                pdfSource = args[0];
            }
            FillFields(pdfSource);

        }

        /// <summary>
        /// Remplis les différents champs d'un formulaire PDF
        /// </summary>
        static void FillFields (string pdfSource) {

            // Création d'un objet PDF Reader basé sur le formulaire PDF
            Console.WriteLine(pdfSource + " :");
            PdfReader pdfReader = new PdfReader(pdfSource);

            // Création d'un objet PDF Stamper à partir du formulaire PDF
            string pdfRempli = pdfSource.Replace(".pdf", "_test.pdf");
            PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileStream(pdfRempli, FileMode.Create));
            AcroFields fields = pdfStamper.AcroFields;

            // Boucle pour remplir les différents champs
            int i = 0;
            foreach (DictionaryEntry field in fields.Fields) {
                // nom du champ
                string key = field.Key.ToString();
                // type du champ (et selon le cas liste des valeurs possibles)
                string type = "";
                i++;
                string data = i.ToString();
                string[] list = null;
                switch (fields.GetFieldType(key)) {
                    case AcroFields.FIELD_TYPE_CHECKBOX:
                        type = "CheckBox";
                        list = fields.GetAppearanceStates(key);
                        break;
                    case AcroFields.FIELD_TYPE_COMBO:
                        type = "Combo";
                        list = fields.GetListOptionExport(key);
                        break;
                    case AcroFields.FIELD_TYPE_LIST:
                        type = "List";
                        list = fields.GetListOptionExport(key);
                        break;
                    case AcroFields.FIELD_TYPE_NONE:
                        type = "None";
                        break;
                    case AcroFields.FIELD_TYPE_PUSHBUTTON:
                        type = "PushButton";
                        break;
                    case AcroFields.FIELD_TYPE_RADIOBUTTON:
                        type = "RadioButton";
                        list = fields.GetAppearanceStates(key);
                        break;
                    case AcroFields.FIELD_TYPE_SIGNATURE:
                        type = "Signature";
                        break;
                    case AcroFields.FIELD_TYPE_TEXT:
                        type = "Text";
                        break;
                }
                if (list != null) {
                    data = list[list.Length - 1];
                }
                Console.WriteLine("- " + key + " : " + data);
                fields.SetField(key, data);
            }
            // Fermeture du formulaire PDF rempli
            pdfStamper.Close();
        }

    }
}

Maintenant, il me reste à trouver comment éviter de créer physiquement un nouveau fichier à chaque fois. Etant donné que c'est destiné à être utilisé dans une application ASP.NET, je préfèrerais trouver une méthode qui me permette de renvoyer directement le résultat vers le poste client. Normalement, c'est le genre de chose qui devrait se trouver facilement sur Google (à condition que cela soit réalisable).

Et ce qui serait bien, c'est de voir s'il est possible à partir du formulaire modèle de remplir plusieurs pages avec des données différentes données. De cette façon, je pourrais gérer des impressions groupées de plusieurs documents à la fois.

jeudi 25 septembre 2008

Lister les champs d'un formulaire PDF

Aujourd'hui, j'ai eu besoin de re-faire un état Crystal Report. Il s'agit d'un document officiel dont le format a changé du jour au lendemain et qui doit être en production pour le 1° février. Vu les délais et surtout étant donné que maintenant plus personne ne maitrise Crystal Report chez nous, ça risquait d'être un peu juste.

Mais heureusement, l'organisme qui demande ce nouvel état a eu la bonne idée de le fournir sous forme de formulaire PDF prêt à remplir. Comme ce n'est normalement pas un état trop demandé (ce qui fait qu'on n'a pas vraiment besoin de la puissance de feu de Crystal Report), j'en ai profité pour tester s'il était possible de remplir ce formulaire depuis ASP.NET.

Apparemment, la meilleure solution bon marché pour cela semble être iTextSharp. C'est gratuit et ça semble vraiment très simple à utiliser. Sauf en ce qui concerne la mise à jour des formulaires PDF où il m'a fallu beaucoup creuser pour réussir à trouver des exemples concrets :

Une fois tout ça trouvé, ça a de suite été plus facile de faire un premier essai qui marche :

namespace Altrr.iText {

    using System;
    using System.Collections;
    using iTextSharp.text.pdf;

    /// <summary>
    /// Tests du composant iTextSharp
    /// </summary>
    public class Start {

        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        static void Main (string[] args) {

            string pdfSource = @"D:\Altrr\iText\register_form1.pdf";
            if (args.Length == 1) {
                pdfSource = args[0];
            }
            ListFieldNames(pdfSource);

        }

        /// <summary>
        /// Affiche la liste des champs d'un formulaire PDF
        /// </summary>
        static void ListFieldNames (string pdfSource) {

            // Création d'un objet PDF Reader basé sur le formulaire PDF
            Console.WriteLine(pdfSource + " :");
            PdfReader pdfReader = new PdfReader(pdfSource);
            AcroFields fields = pdfReader.AcroFields;

            // Boucle sur les différents champs du formulaire
            foreach (DictionaryEntry field in fields.Fields) {
                // nom du champ
                string key = field.Key.ToString();
                // type du champ (et selon le cas liste des valeurs possibles)
                string type = "";
                string data = "";
                switch (fields.GetFieldType(key)) {
                    case AcroFields.FIELD_TYPE_CHECKBOX:
                        type = "CheckBox";
                        data = String.Join(", ", fields.GetAppearanceStates(key));
                        break;
                    case AcroFields.FIELD_TYPE_COMBO:
                        type = "Combo";
                        data = String.Join(", ", fields.GetListOptionExport(key));
                        break;
                    case AcroFields.FIELD_TYPE_LIST:
                        type = "List";
                        data = String.Join(", ", fields.GetListOptionExport(key));
                        break;
                    case AcroFields.FIELD_TYPE_NONE:
                        type = "None";
                        break;
                    case AcroFields.FIELD_TYPE_PUSHBUTTON:
                        type = "PushButton";
                        break;
                    case AcroFields.FIELD_TYPE_RADIOBUTTON:
                        type = "RadioButton";
                        data = String.Join(", ", fields.GetAppearanceStates(key));
                        break;
                    case AcroFields.FIELD_TYPE_SIGNATURE:
                        type = "Signature";
                        break;
                    case AcroFields.FIELD_TYPE_TEXT:
                        type = "Text";
                        break;
                }
                Console.Write("- " + key + " : " + type);
                if (data != "") {
                    Console.WriteLine(" (valeurs possibles : " + data + ")");
                } else {
                    Console.WriteLine("");
                }
            }
        }

    }
}

Ce qui avec le fichier d'exemple me donne :

D:\Altrr\iText\register_form1.pdf :
- person.name : Text
- person.postal_code : Text
- person.knowledge.Dutch : CheckBox (valeurs possibles : Off, On)
- person.language : Combo (valeurs possibles : EN, FR, NL)
- person.knowledge.French : CheckBox (valeurs possibles : Off, On)
- person.programming : List (valeurs possibles : JAVA, C, CS, VB)
- person.email : Text
- person.address : Text
- person.preferred : RadioButton (valeurs possibles : Off, NL, EN, FR)
- person.knowledge.English : CheckBox (valeurs possibles : Off, On)

Il y a quand même un problème, parce qu'avec mon formulaire PDF, j'obtiens une liste des champs totalement dans le désordre, c'est à dire ni dans l'ordre de saisie, ni même dans l'ordre alphabétique. Et comme les noms des champs sont moyennement clairs, c'est assez galère pour savoir à quoi correspond chaque champ.

Ca vaudrait peut être le coup d'essayer la fonction pdfReader.AcroFields.GetFieldPositions(key) qui renvoie la position du champ pour essayer de trier cette liste de haut en bas et de gauche à droite. Ou alors, simplement remplir chaque champ avec une valeur numérique croissante et voir ce que ça donne.

mardi 15 juillet 2008

Créer un fichier Excel en .NET

Mon voisin de bureau avait la désagréable mission de devoir réaliser une interface (lui aussi!) pour exporter des données au format Excel. Sinon y'aurait des têtes qui allaient tomber !

Comme la vie dans un openspace bondé exige une bonne dose de civilité et pour éviter les éclaboussures j'ai fait comme les autres et j'ai donc essayé de le sortir de ce mauvais pas.

Générer de l'Excel

Habituellement, on répond à ce genre de besoin en générant un bête fichier CSV (en tout cas moi c'est ce que je fais à chaque fois) ou dans les cas les plus extrêmes certains d'entre nous utilisent la librairie ExcelXmlWriter pour produire un fichier Excel au format XML.

Mais dans le cas présent, il était nécessaire de fournir un authentique fichier Excel parce que l'application en face s'attendait à lire un "vrai" format Excel bien binaire comme dans le temps, estampillé "Made in Microsoft" (pour un programme en PHP c'est d'ailleurs assez rigolo).

Il ne restait donc que 3 solutions possibles :

  • installer Excel sur le serveur IIS et y aller à coup d'objets COM : sur un serveur de prod! ça va pas la tête ?
  • trouver une librairie qui sache créer de "vrais" fichiers Excel : et pourquoi pas avoir à l'acheter en plus ?
  • vérifier une fois pour toute si le provider OleDb permet aussi d'écrire dans un fichier Excel : ah ouaih ça c'est marrant !

Lire depuis un fichier Excel

Pour commencer, créer vite fait un petit fichier Excel pour se rappeler comment on fait pour lire ce genre de truc :

  • 3 colonnes : Code, Libellé, Date
  • 1° ligne : 1, Un, 01/01/2008
  • 2° ligne : 2, Deux, 02/02/2008
  • 3° ligne : 3, Trois, 03/03/2008

Enregistrer tout ça dans un fichier que l'on nommera Classeur1.xls

Fouiller un peu dans sa mémoire et pas mal sur le disque dur pour retrouver comment coder un petit programme qui va lire le contenu de ce fichier :

using System;
using System.Data.OleDb;

namespace OleDbExcel {

    class Class1 {
        [STAThread]
        static void Main(string[] args) {

            // Défini la chaine de connexion au fichier Excel
            string cnstr = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=""Excel 8.0;HDR=YES;""";
            cnstr += @";Data Source=D:\Altrr\OleDbExcel\Classeur1.xls";

            // Connexion au fichier Excel
            OleDbConnection cn = new OleDbConnection(cnstr);
            cn.Open();

            // Affiche le contenu
            OleDbCommand cm = cn.CreateCommand();
            cm.CommandText = "SELECT * FROM [Feuil1$]";
            OleDbDataReader dr = cm.ExecuteReader();
            while (dr.Read() == true) {
                Console.WriteLine("{0} : {1}\t{2}", dr[0], dr[1], dr[2]);
            }
            dr.Close();

            // Ferme la connexion
            cn.Close();

            // Fin du test
            Console.WriteLine();
            Console.Write("(Entrée) pour terminer...");
            Console.ReadLine();
        }

    }
}

F5 => ça marche => au suivant!

(une parenthèse pour info : Feuil1 c'est le nom de la 1° feuille dans le classeur Excel et pour l'utiliser en tant que table il faut ajouter un $ au bout et mettre le tout entre crochets)

Ecrire dans un fichier Excel

Et maintenant, le saut dans l'inconnu, à savoir tenter d'écrire dans un fichier Excel via une connexion OleDb (de l'inédit pour moi) :

using System;
using System.Data.OleDb;

namespace OleDbExcel {

    class Class1 {
        [STAThread]
        static void Main(string[] args) {

            // Défini la chaine de connexion au fichier Excel
            string cnstr = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=""Excel 8.0;HDR=YES;""";
            cnstr += @";Data Source=D:\Altrr\OleDbExcel\Classeur1.xls";

            // Connexion au fichier Excel
            OleDbConnection cn = new OleDbConnection(cnstr);
            cn.Open();

            // Création d'un objet OleDbCommand
            OleDbCommand cm = cn.CreateCommand();

            // Insère une 4° ligne dans le fichier Excel
            cm.CommandText = "INSERT INTO [Feuil1$] ([Code], [Libellé], [Date]) VALUES (4, 'Quatre', '04/04/2008')";
            cm.ExecuteNonQuery();

            // Insère une 5° ligne dans le fichier Excel
            cm.CommandText = "INSERT INTO [Feuil1$] ([Code], [Libellé], [Date]) VALUES (5, 'Cinq', '05/05/2008')";
            cm.ExecuteNonQuery();

            // Affiche le contenu
            cm.CommandText = "SELECT * FROM [Feuil1$]";
            OleDbDataReader dr = cm.ExecuteReader();
            while (dr.Read() == true) {
                Console.WriteLine("{0} : {1}\t{2}", dr[0], dr[1], dr[2]);
            }
            dr.Close();

            // Ferme la connexion
            cn.Close();

            // Fin du test
            Console.WriteLine();
            Console.Write("(Entrée) pour terminer...");
            Console.ReadLine();
        }

    }
}

F5 => ça marche aussi !

On tente la même chose sur le serveur (de prod ! quand y'a des têtes en jeux on a vraiment plus peur de rien) et ça marche encore !!!

Que demander de plus ?

On peut même utiliser des paramètres au lieu de commandes SQL en "dur"

using System;
using System.Data.OleDb;

namespace OleDbExcel {

    class Class1 {
        [STAThread]
        static void Main(string[] args) {

            // Défini la chaine de connexion au fichier Excel
            string cnstr = @"Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=""Excel 8.0;HDR=YES;""";
            cnstr += @";Data Source=D:\Altrr\OleDbExcel\Classeur1.xls";

            // Connexion au fichier Excel
            OleDbConnection cn = new OleDbConnection(cnstr);
            cn.Open();

            // Création d'un objet OleDbCommand
            OleDbCommand cm = cn.CreateCommand();

            // Insère une 6° ligne dans le fichier Excel
            cm.CommandText = "INSERT INTO [Feuil1$] ([Code], [Libellé], [Date]) VALUES (@Code, @Libelle, @Date)";
            cm.Parameters.Add("@Code", 6);
            cm.Parameters.Add("@Libelle", "Six");
            cm.Parameters.Add("@Date", new DateTime(2008, 6, 6));
            cm.ExecuteNonQuery();

            // Affiche le contenu
            cm.CommandText = "SELECT * FROM [Feuil1$]";
            OleDbDataReader dr = cm.ExecuteReader();
            while (dr.Read() == true) {
                Console.WriteLine("{0} : {1}\t{2}", dr[0], dr[1], dr[2]);
            }
            dr.Close();

            // Ferme la connexion
            cn.Close();

            // Fin du test
            Console.WriteLine();
            Console.Write("(Entrée) pour terminer...");
            Console.ReadLine();
        }

    }
}

On ne peut pas faire un DELETE FROM [Feuil1$] pour vider le fichier avant d'y insérer de nouvelles données : pas très utile donc pas très grave.

Ce qui est un peu plus embêtant, c'est qu'il ne semble pas possible de partir d'un fichier Excel vide et d'y "créer" dynamiquement les colonnes que l'on veut

  • faire un 1° "INSERT INTO [Feuil1$] ([Code], [Caption], [Date]) VALUES ('Code', 'Libellé', 'Date')" plante
  • commencer par un "CREATE TABLE [Feuil1$] ..." plante aussi

Fin de la récréation

C'est ennuyeux, mais comme cela n'a strictement aucune importante dans le cas qui nous occupait au départ, on ne va pas y passer plus que la pause de midi.

jeudi 5 juin 2008

Vérifier que les identifiants automatiques sont à jour

L'objectif est de s'assurer que pour toutes les tables qui ont un identifiant automatique, cet identifiant ne soit pas supérieur à la valeur de la séquence utilisée pour générer cet identifiant. (Pourquoi on se retrouve dans cette situation, c'est un autre problème).

Conventions :

  • tous les noms de tables sont préfixés par LUX_
  • tous les noms de clés primaires sont préfixés par PK_
  • tous les noms de séquences sont préfixés par SEQ_LUX_

Générer un script renvoyant la valeur maximum des identifiants de chaque table

Requête pour générer les SELECT MAX() de chaque table
SELECT 'UNION SELECT ''' || TABLE_NAME || ''' AS TB_NAME, '
       || 'MAX(' || COLUMN_NAME || ') AS TB_IDMAX ' 
       || 'FROM ' || TABLE_NAME
FROM   USER_IND_COLUMNS
WHERE  TABLE_NAME LIKE 'LUX_%'
AND    INDEX_NAME LIKE 'PK_%'
AND    COLUMN_NAME IN (SELECT COLUMN_NAME FROM COLS WHERE DATA_TYPE = 'NUMBER')
ORDER BY TABLE_NAME
Compléter le script généré

Sous TOAD, faire clic droit dans le résultat de la requête et choisir SaveAs et enregistrer dans le presse-papiers, ce qui donne :

UNION SELECT 'LUX_BANQUES' AS TB_NAME, MAX(IDBANQUE) AS TB_IDMAX FROM LUX_BANQUES
UNION SELECT 'LUX_CLIENTS' AS TB_NAME, MAX(IDCLIENT) AS TB_IDMAX FROM LUX_CLIENTS
UNION SELECT 'LUX_COMPTES' AS TB_NAME, MAX(IDCOMPTE) AS TB_IDMAX FROM LUX_COMPTES
UNION SELECT 'LUX_CONTACTS' AS TB_NAME, MAX(IDCONTACT) AS TB_IDMAX FROM LUX_CONTACTS
...

Dans Notepad, enlever le premier UNION et préfixer par un CREATE TABLE

CREATE TABLE TMP_IDMAXIMUMS AS 
SELECT 'LUX_BANQUES' AS TB_NAME, MAX(IDBANQUE) AS TB_IDMAX FROM LUX_BANQUES
UNION SELECT 'LUX_CLIENTS' AS TB_NAME, MAX(IDCLIENT) AS TB_IDMAX FROM LUX_CLIENTS
UNION SELECT 'LUX_COMPTES' AS TB_NAME, MAX(IDCOMPTE) AS TB_IDMAX FROM LUX_COMPTES
UNION SELECT 'LUX_CONTACTS' AS TB_NAME, MAX(IDCONTACT) AS TB_IDMAX FROM LUX_CONTACTS
...

Puis exécuter ce script pour obtenir une première table TMP_IDMAXIMUMS qui enregistrera la valeur maximum de l'identifiant de chaque table

Créer une seconde table pour enregistrer la valeur maximum de chaque séquence

CREATE TABLE TMP_SEQMAXIMUMS AS 
SELECT SUBSTR(SEQUENCE_NAME, 5) AS TB_NAME, LAST_NUMBER AS TB_SEQMAX
FROM   USER_SEQUENCES
WHERE  SEQUENCE_NAME LIKE 'SEQ_LUX_%'

Comparer le contenu des deux tables

SELECT T1.TB_NAME, TB_IDMAX, TB_SEQMAX
FROM   TMP_IDMAXIMUMS  T1,
       TMP_SEQMAXIMUMS T2
WHERE  T1.TB_NAME = T2.TB_NAME
AND    TB_IDMAX > TB_SEQMAX