2016. február 29., hétfő

Hibakeresés SSRS-ben

A napokban több feladatom is volt SSRS használatával és megosztanám néhány problémás eset tanulságát.

1) Textbox helye nem frissül
Egy újabb riport elkészítése során egy már létezőből indultam ki, jelentős részét átmásolva, felhasználva. Az egyik helyen két textboxot próbáltam egymás alá igazítani, de bármennyire is növeltem a távolságot a kettő között, a generált képen mindig egy sornyira volt a két szöveg. Ekkor fedeztem fel, hogy a textboxnak van egy olyan tulajdonsága, hogy CanShrink, ami pont azt csinálja, hogy csökkentheti a magasságát, ha kevesebbet igényel a szöveg. Ez a tulajdonság, alapértelmezetten ki van kapcsolva, de esetemben, mivel másolt kóddal dolgoztam, engedélyezve volt. Végül én is bekapcsolva hagytam, mert jól jött ez a funkció, de ettől függetlenül nem árt tudni róla, hogy létezik és magyarázatot adhat arra, hogy miért nem pont úgy néz ki a generált kép, mint ahogyan a tervezőnézetben.


2) Subreportok beágyazása
A másik esetben a lényeg az volt, hogy két riportot kellett volna előállítani úgy, hogy az egyik a másikhöz felolvasott adatok egy részével paraméterezve működött. Úgy álltam neki, hogy egy container reportra felhelyeztem két subreportot és külön-külön behivatkoztam a már létező, kész és működő riportokat. 

Hibakeresés - subreport
Ekkor jött az első hibaüzenet, ami nem volt túl beszédes: Error: The subreport could not been shown.
Az SSRS saját logja már legalább azt megmondta, hogy az egyik subreporttal van baja:
[rsWarningExecutingSubreport] Warnings occurred while executing the subreport ‘Subreport1’. [rsNone] One or more parameters required to run the report have not been specified. [rsNone] One or more parameters were not specified for the subreport, 'Subreport1', located at: /Child. \\blahblahblah\Parent.rdl

A naplófájl alapértelmezetten itt található:
C:\Program Files\Microsoft SQL Server\MSRS[X].MSSQLSERVER\Reporting Service\LogFiles

A hibaüzenet alapján átvizsgáltam a paramétereket, leellenőrizve, hogy megfelelő típusút adok mindenhova illetve, hogy nincs-e elírás. Mivel ezen téren nem találtam hibát, konstans értékeket kötöttem be, mivel ezzel ki behatárolhattam, hogy a beállítandó értékek hibáznak-e vagy máshol lehet a hiba. A tesztelésnél így is jött a hibaüzenet. Ezt követően a subreportot módosítottam úgy, hogy minden paraméterének adtam alapértelmezettet értéket, és a parentben semmit sem adtam át.

Ekkor belefutottam abba, amiről már korábban is írtam, hogy ha megváltoztatom egy paraméter tulajdonságát, akkor azt csak úgy nem lehet frissíteni egy redeploy alkalmával. Ezt követően a Visual Studio report preview-ja is cache-elt, amit könnyedén elintéztem, erre már van egy toolom, amiről itt írtam.

Mivel a riport probléma nélkül betöltődött az alapértelmezett értékkel, így egyesével elkezdtem beállítani konstans értékekkel a paramétereket a parentből a subreportnak és figyeltem, hogy mikor romlik el. Egy olyan paraméternél akadt meg, ami a subreporton text típusú, egy másik paraméter alapján egy datasetből vette az értékkészletét, ráadásul integert. Pusztán annyira szolgált ez a text paraméter, hogy egy szöveget felolvassunk az adatbázisból az egyik paraméter alapján. Az lett a megoldás, hogy felszámoltam ezt a text paramétert és a datasetből közvetlenül használtam a riporton a szöveget.

Hibakeresés - fejléc/lábléc
Miután végre sikeresen betöltődött mindkét riport, akkor jött a felismerés, hogy nem látszik a fejléc/lábléc a subreportról. Az MSDN szerint tervezetten nem jelenik meg egy subreport ezen része a parenten. 

Léteznek különböző ilyen-olyan megkerülő módszerek arra, hogy mégiscsak látszódjanak, de eléggé gányolásnak tűnnek számomra, szemben azzal, mintha csak fel kellene tenni két subreportot egy containter parentre, bepipálni, hogy látszódjanak és máris megfelelően beágyazná őket a subreporthoz tartozó oldalakon.
  1. A child legyen felbontva 3 subreportra: header, body, footer, és mindegyik a child body részébe legyen beillesztve. Így emiatt a parent teljes értékűen meg fogja jeleníteni azokat is, már a lefejlesztése sem kis idő és pénz és utána a karbantartás költsége is az egekben fog járni, nem beszélve a potenciálisan behozott hibalehetőségekről.
  2. A child fejléc/lábléc eleme legyen külön hozzáadva a parentben is, majd pedig az oldalszám függvényében legyen állítva a láthatósága. Ehhez valamennyire részletes leírás itt található. Önmagában egyszerűnek tűnhet, viszont arra nem találtam semmi megoldást, hogy dinamikusan hogyan lehetne kezelni az oldalszámokat és azok alapján az egyes fejlécek láthatóságát.
Összegzés
Végeredményben arra jutottam, hogy C# kódból egymás után hívom a két riport generálását úgy, hogy kódban előre felolvasom a közös adatokat és azzal paraméterezem őket, ezzel csökkentve a redundáns adatmozgatást.



Szerintetek létezik valamilyen használható megoldás arra, hogy több subreportot beágyazva egy containerbe meg lehessen jeleníteni a saját fejléc/lábléc részeiket? Másként szólva lehetséges teljes értékű merge funkciót kicsikarni a reporting services-ből?

2016. február 19., péntek

Hasznos MSSQL infók

Megosztom veletek az elmúlt napok tapasztalatát az MSSQL világából, hátha hasznos lesz nektek is:

Dinamikus SQL

Dinamikus SQL utasításba táblaváltozót (@ prefix) nem lehet átadni kimeneti paraméterként, viszont léteznek megkerülő megoldások. 

  • Az egyik a temp tábla (# prefix) használata, ami addig létezik, amíg az adott session tart vagy el nem dobtuk, emiatt elérhető a dinamikus SQL scope-jában is

CREATE TABLE #t ( id INT ) DECLARE @q NVARCHAR(MAX) = 'insert into #t values(1),(2)' EXEC (@q) SELECT * FROM #t

  • Táblaváltozót használunk amibe beleszúrjuk a dinamikus SQL futási eredményét

DECLARE @t TABLE ( id INT ) DECLARE @q NVARCHAR(MAX) = 'declare @t table(id int)
                            insert into @t values(1),(2)
                            --itt a lényeg:                            select * from @t'INSERT INTO @t EXEC(@q) SELECT * FROM @t


Nem mindegy, hogy az EXEC (@SQL) vagy az EXEC @SQL utasítást használjuk. Első esetben a @SQL változó tartalmát utasításként értelmezi, ezzel szemben az utóbbinál pedig a @SQL változó tartalmának megfelelő nevű tárolt eljárást akarja futtatni.

Konkatenálás vs Concat

Az SQL nem végez automatikus típuskonverziót konkatenációnál, ami nem baj, de nem árt fejben tartani, mert körülményes lehet utólag átírni egy dinamikus SQL kifejezést a Concat függvényre egy int változó miatt. 

Linked Server

Linked serveren keresztül nem lehet ki-/bekapcsolni az IDENTITY_INSERT tulajdonságot. Ha mégis elkerülhetetlen, akkor létre kell hozni egy tárolt eljárást a célrendszeren, ami beállítja és azt kell meghívni távoli eljárásként. Egy másik korlátozás, hogy nem használható az OUTPUT clause.

SQL Server Alias

SQL alias beállításánál a SERVER-nek hiába van a gépnév mellett megadva a kívánt SQL instance neve is, mivel a gépnév + portszám párost veszi figyelembe. Ezen infó nélkül, ha úgy állítjuk be az aliasokat, hogy RemoteHost\Instance1, RemoteHost\Instance2 és mindkettő az 50000 porton figyel, akkor minden gond nélkül mindkét esetben ugyanarra fog mutatni. A port beállításához leírás itt található.

Cursor

Egyazon Cursort többször is fel lehet használni, ha korábban már le lett zárva és fel lett szabadítva.


2016. február 17., szerda

Log4Net konfigurálása

Minden alkalmazás életében előbb vagy utóbb eljön a pillanat, hogy szükség lenne naplózásra azért, hogy beleláthassunk a nagy fekete doboz működésébe a debuggeren kívüli életben is. A Log4Net egy kiváló eszköz erre a célra, ráadásul a Nuget Package Managerrel könnyedén hozzá lehet adni az alkalmazásunkhoz. Eléggé részletes a hivatalos dokumentáció arról, hogy miként kell bekonfigurálni, viszont szerintem eleinte nehezen áll össze a teljes kép, pedig viszonylag egyszerűen be lehet üzemelni.

Jelen cikk keretében nem célom a különböző szűrők, naplózási szinten és formázások használatára kitérni, csak arra, hogy miként lehet szóra bírni.

Konfigurációs fájl

A Log4Nethez az úgynevezett appendereket és azok működését egy konfigurációs fájl írja le. Egyidejűleg akár több appendert is megadhatunk, a rendszer automatikusan kezeli, hogy mindegyik meghívódjon. Természetesen a szűrők és a naplózási szintek módosíthatják, hogy egy-egy naplózó utasítás esetén melyik appender fog ténylegesen írni is valamit.

Az alábbi konfig fájl példában a megadott két appenderrel egyidejűleg írunk fájlba óránkénti bontásban és a konzolra is:
 
<configuration>
  <log4net >
    <appender name="RollingFileAppender" 
              type="log4net.Appender.RollingFileAppender">
      <file value="log\dm-info" />
      <param name="DatePattern" value=".yyyy.MM.dd-HH'.log'"/>
      <appendToFile value="true" />
      <rollingStyle value="Date" />
      <maxSizeRollBackups value="1" />
      <staticLogFileName value="false" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date | %-5level | thread:%thread | %message%newline" />
      </layout>
    </appender>
  
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <target value="Console.Out" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date | %-5level | thread:%thread | %message%newline" />
      </layout>
      <filter type="log4net.Filter.LevelRangeFilter">
        <param name="LevelMin" value="DEBUG"/>
      </filter>
    </appender>

    <root>
      <level value="INFO" />
      <appender-ref ref="ConsoleAppender" />
      <appender-ref ref="RollingFileAppender" />
    </root>
  </log4net>
</configuration>

Fontos kiemelni, hogy a konfigfájlon legyen beállítva a Copy to output directory tulajdonság valamelyik Copy lehetőségre, ellenkező esetben nem fog csinálni semmit sem, viszont hibát sem fog dobni. 

Inicializálás

Ahhoz, hogy egyáltalán tudjon róla, hogy milyen appenderek vannak beállítva, illetve melyik fájl tartalmazza a konfigot, az XmlConfiguratorral fel kell dolgozni a megadott konfig fájlt. Ez többféleképpen is történhet. Például meghívhatjuk az alkalmazás belépési pontjánál. Ezzel az a baj, hogy ha ki van emelve egy olyan közös projektbe a naplózás, amit több alkalmazás is használ, akkor mindegyik alkalmazásban külön szükséges hozzáadni ezt az utasítást:

log4net.Config.XmlConfigurator.Configure(
                               new System.IO.FileInfo(@"Path\To\Log4net.config"));

Ennél szerintem szebb és egyszerűbb megoldás, hogy ha a naplózást tartalmazó projekt Properties/AssemblyInfo.cs fájlba hozzáadjuk az alábbi utasítást. A két paramétere megadja, hogy melyik konfigurációs fájlt szeretnénk betölteni hozzá, illetve hogy kell-e frissíteni a beállításokat, hogy ha megváltozott a konfigurációs fájl tartalma. Ezzel a módszerrel a hivatkozó projektekben, alkalmazásokban később már nem kell újra és újra meghívni az XmlConfiguratort.

[assembly: log4net.Config.XmlConfigurator(ConfigFile = "Path\To\Log4Net.config", Watch = true)]

Logger osztály és interface

Miután megvagyunk a konfigurációs fájllal és annak beolvastatásával, jöhet a tényleges logger implementálása. Önmagában a log4net használatához elegendő lenne az alábbi kódrészlet, amivel a log változó metódusaival már lehetne naplózni sorainkat:

private static readonly log4net.ILog log = 
                        log4net.LogManager.GetLogger(typeof(Logger));

Másfelől lehet ezt még csinosítani egy wrapper osztállyal, ami már tetszőleges paraméterezésű metódusokat tartalmazhat kifelé. Az ILogger interface-re azért lehet szükség, mert így a későbbiekben egyéb naplózó logikát lehet implementálni és transzparensen használni.

public class Logger : ILogger
{
    private static readonly log4net.ILog log = 
                            log4net.LogManager.GetLogger(typeof(Logger));

    #region ILogger Members

    public void LogException(Exception exception, string message)
    {
        if (log.IsErrorEnabled)
           log.Error(
                string.Format(CultureInfo.InvariantCulture, "{0}", message), 
                exception);
    }
       
    public void LogException(Exception exception)
    {
        LogException(exception, exception.Message);
    }

    public void LogError(string message)
    {
        if (log.IsErrorEnabled)
            log.Error(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }

    public void LogWarningMessage(string message)
    {
        if (log.IsWarnEnabled)
            log.Warn(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }

    public void LogInfoMessage(string message)
    {
        if (log.IsInfoEnabled)
            log.Info(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }
    
    public void LogDebug(string message)
    {
        if (log.IsDebugEnabled)
            log.Debug(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }
       
    #endregion
}

A hozzátartozó ILogger interface:
public interface ILogger
{
    void LogException(Exception exception);
    void LogException(Exception exception, string customMessage);
    void LogError(string message);
    void LogDebug(string message);
    void LogWarningMessage(string message);
    void LogInfoMessage(string message);
}

LogHelper osztály

Jó lenne, ha elkerülhetnénk, hogy minden egyes alkalommal, ahol használjuk, példányosítani kelljen a Loggert. A metódus statikussá tétele nem megoldás, mivel az interface-t implementáló metódus nem lehet statikus. Egy lehetséges megoldás a problémára, hogy ha becsomagoljuk egy újabb wrapper osztályba, aminek a statikus metódusai az egyetlen Logger példány metódusait hívják.

public static class LogHelper
{
    private static readonly ILogger logger = new Logger();
       
    public static void LogInfo(string message)
    {
        logger.LogInfoMessage(message);
    }

    public static void LogInfo(string pattern, params object[] args)
    {
        LogInfo(string.Format(CultureInfo.InvariantCulture, pattern, args));
    }
       
    public static void LogDebug(string message)
    {
        logger.LogDebug(message);
    }

    public static void LogDebug(string pattern, params object[] args)
    {
        LogDebug(string.Format(CultureInfo.InvariantCulture, pattern, args));
    }

    public static void LogError(string message)
    {
        logger.LogError(message);
    }

    public static void LogError(string pattern, params object[] args)
    {
        LogError(string.Format(CultureInfo.InvariantCulture, pattern, args));
    }

    public static void LogException(Exception exception, string message)
    {
        logger.LogException(exception, message);
    }

    public static void LogException(
         Exception exception, string pattern, params object[] args)
    {
        string message = string.Format(CultureInfo.InvariantCulture, pattern, args);
        LogException(exception, message);
    }

    public static void LogException(Exception ex)
    {
        logger.LogException(ex);
    }
       
    public static void SetThreadVariable(string key, string variable)
    {
        log4net.ThreadContext.Properties[key] = variable;
    }

    public static void SetGlobalVariable(string key, string variable)
    {
        log4net.GlobalContext.Properties[key] = variable;
    }
}


Összegzés

Ezek után nem maradt más hátra, mint hogy használjuk a LogHelper statikus metódusait és élvezzük, ahogy termeli a bejegyzéseket a beállított helyen és formában.