2021. november 22., hétfő

Nagy mennyiségű adat beolvasása SQL adatbázisba EntityFramework segítségével

  

Nagy mennyiségű adat beolvasása könnyen hatékonysági problémába torkolhat. Dotnet keretrendszerbe az adatbázisba írásra és olvasásra használhatunk EntityFramework-öt. Ezt elsősorban egy egyszerű példán mutatnám be. Ehhez a példához egy konzol aplikációt kezdtem el 4.7.2 keretrendszerben. Elsősorban kell az EntityFramework NuGet csomag:



Feladat:

A feladat a következő: Egy txt fájlból írjuk be az adatokat SQL adatbázisba. A fájlba utaknak a nevei és kódja vannak tárolva. A fájl minden sora egy utat ír le, ahol az első tag a név a második a kód ezek egy helyközzel vannak elválasztva.

Megoldás:

Elsősorban létrehoztam egy Model könyvtárat a megoldáson belül és ebben létrehoztam az Utak nevű osztályt. Ez az osztály reprezentálja az adatbázisban a táblát, aminek 3 oszlopa lesz: Id, Nev, Kod.

namespace Beolvasas.Model

{

    [Table("Utak")]

    public class Utak

    {

        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]

        [Key]

        public int Id { getset; }

        public string Nev { getset; }

        public long? Kod { getset; }

 

    }

}

Ezek után létrehoztam a Data könyvtárat a megoldáson belül. Ebben raktam az adatbázis osztályt. Ami tartalmazta az Utak táblát.

namespace Beolvasas.Data

{

    class UtakDbContext : DbContext

    {

        public UtakDbContext()

            base("name=UtakDbContext")

        {

        }

        public DbSet<Utak> Utak { getset; }

    }

}

Amit fontos még megtenni, hogy az App.configba rakjuk bele a connectionStringet.

<connectionStrings>

             <add name="UtakDbContext" connectionString="data source=.;initial catalog=UtakDb;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />

       </connectionStrings>

Végül az adatbázis kezelésére is létrehoztam egy osztályt.

namespace Beolvasas

{

    public class AdatbazisKezeles : IDisposable

    {

        private UtakDbContext _utakdb;

        public AdatbazisKezeles()

        {

            _utakdb = new UtakDbContext();

        }

        public void AdatbazisLetrehozasa()//letrehozza az adatbázist, ha nem létezik

        {

            _utakdb.Database.CreateIfNotExists();

            _utakdb.SaveChanges();

        }

        public void AdatokBetoltese(string fajl)

        {

            string fileName = fajl;

            FileStream fs = File.OpenRead(fileName);

            StreamReader sr = new StreamReader(fs);

            string sor;

            while ((sor = sr.ReadLine()) != null)

            {

                Utak ut = new Utak();

                string[] adatok = sor.Split();

                ut.Nev = adatok[0];

                ut.Kod = long.Parse(adatok[1]);

                _utakdb.Utak.Add(ut);

            }

            _utakdb.SaveChanges();

        }

        public void Dispose()

        {

            if (_utakdb != null)

                _utakdb.Dispose();

        }

    }

}

Ennek az osztálynak az AdatbazisLetrehozasa() nevű eljárás létrehozza az adatbázist, ha nem létezik, az AdatokBetoltese(string fajl) eljárás pedig a paraméterben megadott fájlnak az adatait betölti az adatbázisba.
Végül a főprogram:

namespace Beolvasas

{

    class Program

    {

        static void Main(string[] args)

        {

            AdatbazisKezeles adatb = new AdatbazisKezeles();

            adatb.AdatbazisLetrehozasa();      adatb.AdatokBetoltese(@"C:\Users\LászlóCsernok\source\repos\Beolvasas\Utak.txt");

        }

    }

}

Számunkra az AdatokBetoltese nevű eljárás fontosabb. Ebben az eljárásban soronként olvassa be a program az adatokat a fájlból majd létrehoz egy Utak nevű osztályt, ezt feltölti a megfelelő adatokkal, amit a fájlból olvas be. Egy adatot szépen berak a DbSet<Utak> Utak nevű változóba. Amikor minden soron végig ment akkor az adatbázisba beleírja az adatokat a _utakdb.SaveChanges() metódus hívással.
Ez egy működő megvalósítás kisebb adatokra szépen gyorsan működik, de nagyobb adatoknál már érezhető a lassulás. A legnagyobb problémát _utakdb.Utak.Add(uthívás jelenti, mert ez a legidőigényesebb.

 

Javítás 1:

 

Első próbálkozás az lehet, hogy csak egyszer hívjuk a legköltségesebb műveletet az Add-ot. Ezt úgy érjük el, hogy létrehozunk egy listát, amibe beletesszük a beolvasott adatokat. Majd ez a listát adjuk hozzá. Ezzel jelentős mértékben növelhetjük a kód hatékonyságát.
A módosított kód:

 

public void AdatokBetoltese(string fajl)

        {

            string fileName = fajl;

            FileStream fs = File.OpenRead(fileName);

            StreamReader sr = new StreamReader(fs);

            string sor;

            List<Utak> utak = new List<Utak>();// a lista amikbe az elemeket rakjuk

            while ((sor = sr.ReadLine()) != null)

            {

                Utak ut = new Utak();

                string[] adatok = sor.Split();

                ut.Nev = adatok[0];

                ut.Kod = long.Parse(adatok[1]);

                utak.Add(ut);  //hozzá adás

                //_utakdb.Utak.Add(ut); -- régi megvalósítás

            }

            _utakdb.Utak.AddRange(utak);// hozzá adjuk a dbSethez

            _utakdb.SaveChanges();

        }

 

Javítás 2:


Ha még szeretnénk gyorsítani a kódon akkor használhatunk egy NuGet csomagot én az alábbit használtam:



A kód az előbbihez nagyon hasonló marad, csak a _utakdb.Utak.AddRange(utak) sort kell            _utakdb.BulkInsert(utak) -ra kicserélni.

public void AdatokBetoltese(string fajl)

        {

            string fileName = fajl;

            FileStream fs = File.OpenRead(fileName);

            StreamReader sr = new StreamReader(fs);

            string sor;

            List<Utak> utak = new List<Utak>();// a lista amikbe az elemeket rakjuk

            while ((sor = sr.ReadLine()) != null)

            {

                Utak ut = new Utak();

                string[] adatok = sor.Split();

                ut.Nev = adatok[0];

                ut.Kod = long.Parse(adatok[1]);

                utak.Add(ut);  //hozzá adás

                //_utakdb.Utak.Add(ut); -- régi megvalósítás

            }

 

            //_utakdb.Utak.AddRange(utak);// hozzá adjuk a dbSethez -- helyett

            _utakdb.BulkInsert(utak);

            _utakdb.SaveChanges();

        }

Szerintem ezzel a módszer elég gyors adatbetöltés érhető el. Esetleg baj lehet, ha sok az adat és nem fér el a listában, ekkor szerintem darabolással megoldható.

Másik példa: https://dotnetfiddle.net/awlJdf

2021. október 5., kedd

Hogyan lehet könnyen threadsafe eseményt kezelni.

 https://codeblog.jonskeet.uk/2015/01/30/clean-event-handlers-invocation-with-c-6/

What is this thing you call thread-safe?

The code we’ve got so far is “thread-safe” in that it doesn’t matter what other threads do – you won’t get a NullReferenceException from the above code. However, if other threads are subscribing to the event or unsubscribing from it, you might not see the most recent changes for the normal reasons of memory models being complicated.

As of C# 4, field-like events are implemented using Interlocked.CompareExchange, so we can just use a corresponding Interlocked.CompareExchange call to make sure we get the most recent value. There’s nothing new about being able to do that, admittedly, but it does mean we can just write:

1
2
3
4
public void OnFoo()
{
    Interlocked.CompareExchange(ref Foo, null, null)?.Invoke(this, EventArgs.Empty);
}

with no other code, to invoke the absolute latest set of event subscribers, without failing if a NullReferenceException is thrown. Thanks to David Fowler for reminding me about this aspect.

Admittedly the CompareExchange call is ugly. In .NET 4.5 and up, there’s Volatile.Read which may do the tricky, but it’s not entirely clear to me (based on the documentation) whether it actually does the right thing. (The summary suggests it’s about preventing the movement of later reads/writes earlier than the given volatile read; we want to prevent earlier writes from being moved later.)

1
2
3
4
5
public void OnFoo()
{
    // .NET 4.5+, may or may not be safe...
    Volatile.Read(ref Foo)?.Invoke(this, EventArgs.Empty);
}

… but that makes me nervous in terms of whether I’ve missed something. Expert readers may well be able to advise me on why this is sufficiently foolish that it’s not in the BCL.

An alternative approach

One alternative approach I’ve used in the past is to create a dummy event handler, usually using the one feature that anonymous methods have over lambda expressions – the ability to indicate that you don’t care about the parameters by not even specifying a parameter list:

1
2
3
4
5
6
7
public event EventHandler Foo = delegate {}
 
public void OnFoo()
{
    // Foo will never be null
    Volatile.Read(ref Foo).Invoke(this, EventArgs.Empty);  
}

This has all the same memory barrier issues as before, but it does mean you don’t have to worry about the nullity aspect. It looks a little odd and presumably there’s a tiny performance penalty, but it’s a good alternative option to be aware of.