Mango: Lokalna baza danych – cz. 1  

Udostępnij na: Facebook

Autor: Marcin Kruszyński

Opublikowano: 2011-07-27

W Windows Phone 7.1 (nazwa kodowa „Mango”) aplikacje mogą przechowywać strukturalne dane w lokalnej relacyjnej bazie danych. To kolejna znacząca nowość, której nie można przegapić.

Po przeczytaniu tego artykułu będziesz:

  • znał miejsca, w których aplikacja może przechowywać plik lokalnej bazy danych,
  • wiedział, jakie są właściwości takiej bazy,
  • potrafił stworzyć ją z poziomu kodu aplikacji,
  • umiał wykonywać operacje CRUD na bazie danych.

W artykule (Mango: Lokalna baza danych - cz.2) pokażemy, jak w praktyce dokonać aktualizacji schematu bazy, dystrybuować bazę danych razem z aplikacją oraz przedstawimy inne przydatne informacje.

Wprowadzenie

Lokalna baza danych w Windows Phone 7.1 oparta jest na SQL Server CE. Na urządzeniu mobilnym zasoby są ograniczone i baza ta różni się pod kilkoma względami od swojego desktopowego odpowiednika opartego na SQL Server:

  • wykonuje się w procesie aplikacji, nie może wykonywać się w sposób ciągły jako usługa w tle,  
  • może być udostępniona tylko dla powiązanej z nią aplikacji,
  • dostęp do niej jest możliwy jedynie za pomocą LINQ to SQL – inne metody dostępu nie są wspierane, w tym bezpośrednie korzystanie z języka Transact-SQL.

**Rys.1. Miejsca przechowywania przez aplikację lokalnej bazy danych.

Plik lokalnej bazy danych (.sdf) aplikacja może przechowywać w jednym z dwóch miejsc (rys. 1) – w Isolated Storage lub folderze instalacyjnym (w trybie tylko do odczytu).

Przy dystrybucji aplikacji mamy dwie możliwości dostarczenia bazy danych:

  • podejście Code First Development (standardowe) – stworzenie bazy danych w Isolated Storage przy pierwszym uruchomieniu aplikacji,
  • dołączenie do zasobów aplikacji pliku z bazą danych (przy dużej liczbie danych referencyjnych, np. słownik). Po zainstalowaniu aplikacji plik umieszczany jest w jej folderze i pozwala na odczyt danych. Gdy dane w bazie mają być modyfikowane, aplikacja powinna skopiować plik do Isolated Storage.

Plik bazy danych, tak jak inne pliki i foldery z Isolated Storage, możemy skopiować na dysk komputera za pomocą narzędzia Isolated Storage Explorer - ISETool.exe (możliwa jest także czynność odwrotna).

Na koniec wprowadzenia do bazy danych należy zaznaczyć, że powinniśmy używać jej tam, gdzie ma to uzasadnienie. Zastosowanie bazy danych powoduje dłuższy start aplikacji i jej większe zapotrzebowanie na pamięć. Dla małych zbiorów danych lepiej używać konfiguracji aplikacji lub plików w Isolated Storage.

LINQ to SQL

LINQ to SQL to znany z platformy .NET obiektowo-relacyjny maper, który tłumaczy zapytania LINQ na język Transact-SQL i wysyła je do bazy, gdzie są wykonywane. Gdy baza danych zwróci wyniki, LINQ to SQL tłumaczy je z powrotem na obiekty. Obiektowy model LINQ to SQL jest głównie realizowany przez kontekst danych – obiekt typu System.Data.Linq.DataContext, który stanowi proxy dla bazy danych. Zawiera on w sobie obiekty typu Table, które odpowiadają tabelkom w bazie. W obiektach tabel znajdują się encje odpowiadające wierszom danych. Każda encja jest obiektem POCO z atrybutami. Wszystkie atrybuty definiują bazodanową strukturę tabeli i mapowania między obiektowym modelem danych a schematem bazy.

W Windows Phone LINQ to SQL jest jedynym sposobem dostępu do bazy danych (rysunek 2). Za jego pośrednictwem wykonujemy wszystkie operacje na bazie, w tym definiowanie i modyfikowanie jej schematu. LINQ to SQL na Windows Phone nie wspiera bezpośredniego wykonywania zapytań w Transact-SQL, Data Definition Language (DDL) i Data Modeling Language (DML)(metoda ExecuteCommand nie jest wspierana). LINQ to SQL w swojej implementacji wykorzystuje technologię ADO.NET, nie mamy jednak bezpośredniego dostępu do jej obiektów, tj. DataReader (zapytania zwracają tylko obiekty zdefiniowane w kontekście danych).

**Rys.2. LINQ to SQL w Windows Phone.

Oprócz ograniczeń LINQ to SQL na Windows Phone oferuje także dodatkowe funkcjonalności, m.in. obsługę wielu indeksów i aktualizację schematu bazy z poziomu kodu.

Więcej informacji na temat  LINQ to SQL na omawianej platformie znajdziesz w dokumentacji na stronie LINQ to SQL Support for Windows Phone.

Tworzenie bazy danych

Aby stworzyć bazę danych na Windows Phone, potrzebujemy najpierw zdefiniować kontekst danych i encje. Na podstawie informacji zawartych w atrybutach mapowań (tj. tabele, kolumny, klucze główne, indeksy) generowana jest baza. Baza danych SQL CE stworzona na desktopie może działać na Windows Phone, ale nie jest oficjalnie wspierana.

Stwórzmy bazę, która może znaleźć zastosowanie w przykładzie przedstawionym w artykule Windows Phone Mango - agenci. Lokalna baza danych umożliwia jednoczesną pracę z aplikacjami na pierwszym planie i z agentami w tle. Potrzebujemy zamodelować taski, które dodatkowo powiążemy z projektami. Proponowane encje i relacje między nimi przedstawia rysunek 3.

**Rys.3. Encje bazy danych.

W naszym przykładzie każda encja będzie implementować dwa interfejsy – INotifyPropertyChanged i INotifyPropertyChanging. Pierwszy z nich należy implementować ze względu na change tracking w kontekście danych, drugi zaś pozwala zmniejszyć w zauważalny sposób zużycie pamięci przy korzystaniu z tego mechanizmu. Aby nie implementować tych interfejsów w każdej encji z osobna, zdefiniujmy sobie pomocniczy typ PropertyChangedBase, po którym encje będą dziedziczyć.

public class PropertyChangedBase : INotifyPropertyChanged, INotifyPropertyChanging
    {
        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion

        #region INotifyPropertyChanging Members

        public event PropertyChangingEventHandler PropertyChanging;

        protected void NotifyPropertyChanging(string propertyName)
        {
            if (PropertyChanging != null)
            {
                PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
            }
        }

        #endregion
    }



        private EntityRef<Project> _project;

        [Association(Storage = "_project", ThisKey = "_projectId", OtherKey = "Id", 
         IsForeignKey = true)]
        public Project Project
        {
            get
            {
                return _project.Entity;
            }
            set
            {
                Project previousValue = _project.Entity;
                if (((previousValue != value) || (_project.HasLoadedOrAssignedValue ==
                   false)))
                {
                    NotifyPropertyChanging("Project");
                    if ((previousValue != null))
                    {
                        _project.Entity = null;
                        previousValue.Tasks.Remove(this);
                    }
                    _project.Entity = value;
                    if ((value != null))
                    {
                        value.Tasks.Add(this);
                        _projectId = value.Id;
                    }
                    else
                    {
                        _projectId = default(Nullable<int>);
                    }
                    NotifyPropertyChanged("Project");
                }
            }            
        }

     [Column(IsVersion = true)]
        private Binary _version;

        public Task()
        {
            _project = default(EntityRef<Project>);
        }
    }

    public enum TaskStatus
    {
        ToDo,
        InProgress,
        Verify,
        Done        
    }

    [Table(Name = "Projects")]
    public class Project: PropertyChangedBase
    {
        [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL
         IDENTITY", CanBeNull = false, AutoSync = AutoSync.OnInsert)]
        public int Id
        {
         …            
        }

        [Column(DbType = "NVarChar(50) NOT NULL", CanBeNull = false)]
        public string Name
        {
         …            
        }     
    
        [Column(DbType = "NVarChar(255)")]
        public string Description
        {
         …            
        }

        private EntitySet<Task> _tasks;

        [Association(Storage = "_tasks", ThisKey = "Id", OtherKey = "_projectId")]
        public EntitySet<Task> Tasks
        {
            get
            {
                return this._tasks;
            }
            set
            {
                this._tasks.Assign(value);
            }
        }

     [Column(IsVersion = true)]
        private Binary _version;  

        public Project()
        {
            _tasks = new EntitySet<Task>(new Action<Task>(this.attach_Task),
                new Action<Task>(this.detach_Task));
        }

        private void attach_Task(Task task)
        {
            NotifyPropertyChanging("Task");
            task.Project = this;
        }

        private void detach_Task(Task task)
        {
            NotifyPropertyChanging("Task");
            task.Project = null;
        }
    }

Zdefiniujmy teraz encje Task i Project:

[Table(Name = "Tasks")]
    [Index(Columns = "DueDate")]
    public class Task: PropertyChangedBase
    {
        private int _id;

        [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL
         IDENTITY", CanBeNull = false, AutoSync = AutoSync.OnInsert)]
        public int Id
        {
            get { return _id; }
            set
            {
                if (_id != value)
                {
                    NotifyPropertyChanging("Id");
                    _id = value;
                    NotifyPropertyChanged("Id");
                }
            }
        }

        [Column(DbType = "NVarChar(100) NOT NULL", CanBeNull = false)]
        public string Title
        {
         …            
        }       

        [Column(DbType = "NVarChar(1024)")]
        public string Description
        {
         …            
        }        

        [Column]
        public DateTime DueDate
        {
         …            
        }

        [Column(DbType="Int")]
        public TaskStatus Status 
        {
         …            
        }

        [Column]
        internal int? _projectId;

Dla większej przejrzystości zastosowaliśmy uproszczenie – w przypadkach gdy kod był powtarzalny dla różnych kolumn, zdecydowaliśmy się pokazać go tylko raz. Pełne kody źródłowe omawianej w artykule aplikacji znajdują się tutaj. Na przykładzie powyższych encji możemy zaobserwować, w jaki sposób definiujemy różne mappingi pomiędzy światem obiektowym a relacyjną bazą. Zwróćmy uwagę na asocjacje między encjami. W przykładzie ponadto zdefiniowano dodatkowy indeks dla encji Task (dla kluczy głównych indeksy są automatycznie generowane), a we wszystkich encjach zdefiniowano kolumny z wersją (znacząco zwiększa to wydajność przy aktualizowaniu danych, optymalizacja specyficzna dla LINQ to SQL na Windows Phone).

Mając napisane encje, możemy zdefiniować kontekst danych:

public class TaskDataContext: DataContext
{
    public TaskDataContext(string connectionString): base(connectionString)
    {
    }
     public Table<Task> Tasks;
     public Table<Project> Projects;        
}

Aby stworzyć bazę danych, najpierw tworzymy kontekst danych. W jego konstruktorze podajemy lokalizację dla pliku z bazą. Po upewnieniu się za pomocą metody DatabaseExists(), że baza jeszcze nie istnieje, generujemy ją  przez wywołanie CreateDatabase().

var db = new TaskDataContext("Data Source=isostore:/Tasks.sdf");

if (!db.DatabaseExists())
{
    db.CreateDatabase();
}

Operacje CRUD na bazie danych

Omówimy teraz krótko podstawowe operacje na bazie, takie jak wyszukiwanie, dodawanie, aktualizowanie i usuwanie danych.

Wyszukiwanie danych

Załóżmy, że chcemy w naszej bazie znaleźć wszystkie zaległe taski, posortowane po przewidzianym terminie ich wykonania. Zapytanie może wtedy wyglądać następująco:

from t in db.Tasks
   where t.Status != TaskStatus.Done && (t.DueDate - DateTime.Now).TotalSeconds < 0
   orderby t.DueDate
   select t;

Przy korzystaniu z zapytań LINQ to SQL należy być świadomym, że nie są one wykonywane w pamięci, tylko każde z nich tłumaczone jest na język Transact-SQL i wykonywane na bazie, co w niektórych przypadkach może prowadzić do zysku wydajności (np. wybór niewiekiej liczby elementów z dużej bazy danych). Warto wspomnieć, że można optymalizować ich wykonywanie przez zdefiniowanie odpowiednich indeksów w bazie i kompilowane zapytania.    

Wstawienie danych

Wstawianie nowych danych do bazy danych wymaga:

  • dodania ich do kontekstu, 
  • zatwierdzenia zmian w kontekście.

Aby dodać nowy projekt, należy napisać kod (item - encja typu Project):

db.Projects.InsertOnSubmit(item);
db.SubmitChanges();

Aktualizowanie danych

Aktualizacji danych w bazie dokonujemy poprzez ich modyfikację i zatwierdzenie zmian w kontekście. Jeśli encja binduje do kontrolki UI, to zmiany automatycznie są nanoszone do kontekstu danych.

Usuwanie danych

Również operację usuwania należy zatwierdzić w kontekście danych. Dodatkowo powinniśmy tutaj uwzględnić warunek na klucz obcy. Przykładowo w naszej bazie, aby usunąć projekt, musimy najpierw usunąć wszystkie z nim związane zadania (item – encja typu Project):   

db.Tasks.DeleteAllOnSubmit(item.Tasks);            
db.Projects.DeleteOnSubmit(item);
db.SubmitChanges();

Podsumowanie

W tym artykule zapoznaliśmy się z lokalną bazą danych w Windows Phone 7.1. Wiemy, gdzie aplikacje mogą ją przechowywać. Z poziomu kodu potrafimy wygenerować prostą bazę. Umiemy wykonywać na niej operacje CRUD.