пятница, 11 января 2013 г.

Join и Left Join в Linq to SQL

Всем привет!
   Совсем недавно озадачился проблемой извлечения данных из нескольких таблиц БД, при котором обеспечивался бы 100% -й вывод колонок одной из таблиц при отсутствии записей в другой.То есть, если две таблицы связаны по  ключу, и во второй таблице отсутствуют записи по этому ключу, при запросе к этим таблицам выводились колонки 1-й таблицы и вместо колонок 2-й таблицы что то вроде "NULL". Это фактически программная интерпретация  LEFT JOIN в SQL.
   Постараюсь показать на примере как можно реализовать LEFT JOIN в LINQ to SQL.
   Cоздайте новое консольное приложение в Visual Studio. Можно использовать SharpDevelop, единственный момент - необходимо создавать проект с версией .NET Framework не меньше чем 3.5  - необходимость для использования LINQ to SQL.
   В нашем примере будем использовать три таблицы данных: это авторы книг, книги и жанры.    Таблица авторы будет связана с таблицей книги отношение один ко многим (для упрощения и наглядности примера случай когда у одной книги несколько авторов - не рассматривается), т.е. у одного автора может быть много написанных книг или ни одной. Таблица книги связана с таблицей авторов и с таблицей жанры отношением много к одному. Каждая написанная книга принадлежит к какому либо жанру и несколько книг могут принадлежать одному жанру.

1. В созданное консольное приложение добавляем класс и называем его "Books":

Список свойств объекта класса Books

        public int Id { get; set; }
        public int AuthorId { get; set; }
        public int GenreId { get; set; }
        public string BookName { get; set; }
     
Создадим метод для получения массива объектов класса Books - это будет наша имитация извлечения данных из БД:

        public static Books[] GetBooks()
        {
            Books[] booksLst = new Books[]{
        new Books{
         Id = 1,
         AuthorId = 1,
         GenreId = 1,
         BookName = "Книга №1 "},
          new Books{
         Id = 2,
         AuthorId = 1,
         GenreId = 1,
         BookName = "Книга №2 "},
          new Books{
         Id = 3,
         AuthorId = 1,
         GenreId = 4,
         BookName = "Книга №3 "},
          new Books{
         Id = 4,
         AuthorId = 2,
         GenreId = 1,
         BookName = "Книга №4 "},
          new Books{
         Id = 5,
         AuthorId = 2,
         GenreId = 1,
         BookName = "Книга №5 "},
          new Books{
         Id = 6,
         AuthorId = 3,
         GenreId = 1,
         BookName = "Книга №6 "},
          new Books{
         Id = 7,
         AuthorId = 4,
         GenreId = 1,
         BookName = "Книга №7 "},
          new Books{
         Id = 8,
         AuthorId = 4,
         GenreId = 1,
         BookName = "Книга №8 "},
          new Books{
         Id = 9,
         AuthorId = 5,
         GenreId = 1,
         BookName = "Книга №9 "},
             new Books{
         Id = 10,
         AuthorId = 5,
         GenreId = 1,
         BookName = "Книга №10 "},
         new Books{
         Id = 11,
         AuthorId = 5,
         GenreId = 1,
         BookName = "Книга №11"},
          new Books{
         Id = 12,
         AuthorId = 8,
         GenreId = 2,
         BookName = "Книга №12"},
          new Books{
         Id = 13,
         AuthorId = 8,
         GenreId = 2,
         BookName = "Книга №13"},
          new Books{
         Id = 14,
         AuthorId = 9,
         GenreId = 1,
         BookName = "Книга №14"},
          new Books{
         Id = 15,
         AuthorId = 10,
         GenreId = 1,
         BookName = "Книга №15"}
            };

            return booksLst;
        }


Класс Books готов.

2. Создаем класс Authors - авторы книг:


class Authors
    {
            public int Id { get; set; }
            public string AuthorName { get; set; }


            public static Authors[] GetAuthors()
            {
                Authors[] authorsLst = new Authors[]
                {
         new Authors{
         Id = 1,
         AuthorName = "Автор №1"},
         new Authors{
         Id = 2,
         AuthorName = "Автор №2"},
         new Authors{
         Id = 3,
         AuthorName = "Автор №3"},
         new Authors{
         Id = 4,
         AuthorName = "Автор №4"},
         new Authors{
         Id = 5,
         AuthorName = "Автор №5"},
         new Authors{
         Id = 6,
         AuthorName = "Автор №6"},
         new Authors{
         Id = 7,
         AuthorName = "Автор №7"},
          new Authors{
         Id = 8,
         AuthorName = "Автор №8"}
                };

                return authorsLst;
         
        }
    }

Создается по аналогии и останавливаться подобно на нем я не буду.
3. Класс Genres -  жанры:


class Genres
    {
        public int Id { get; set; }
        public string Genre { get; set; }


        public static Genres[] GetGenres()
        {
            Genres[] genresLst = new Genres[]
                {
         new  Genres{
         Id = 1,
         Genre = "Жанр №1"},
         new Genres{
         Id = 2,
         Genre = "Жанр №2"}
                };

            return genresLst;
        }
    }

4. Теперь, когда у нас есть три источника данных для запроса LINQ to SQL, займемся этими самыми запросами:
В методе Main нашего приложения пишем:


static void Main(string[] args)
        {
получаем массивы объектов книги авторы и жанры для последующих запросов:
            Books[] books = Books.GetBooks();
            Authors[] authors = Authors.GetAuthors();
            Genres[] genres = Genres.GetGenres();



            var result = from b in books
                          join a in authors on b.AuthorId equals a.Id into temp1
                          from t1 in temp1
                          select new
                          {
                              bookname = b.BookName,
                              authorName = t1.AuthorName
                          };


 foreach (var item in result)
            {
                Console.WriteLine("{0}     {1}", item.authorName, item.bookname);           
            }

            Console.ReadKey();

}


В первом запросе используем только массивы books и authors. Необходимо найти все книги и авторов, которые их написали.
Результат выполнения:




 Видно, что у нас есть книги №№ 14,15. Но они не попали в результат, т. к. для них не оказалось соответствующего автора.  А что делать, если их все равно необходимо вывести в результат запроса? Переделаем запрос:



static void Main(string[] args)
        {
получаем массивы объектов книги авторы и жанры для последующих запросов:
            Books[] books = Books.GetBooks();
            Authors[] authors = Authors.GetAuthors();
            Genres[] genres = Genres.GetGenres();



           var result = from b in books
                          join a in authors on b.AuthorId equals a.Id into temp1
                          from t1 in temp1.DefaultIfEmpty()
                          select new
                                     {
                                         bookname = b.BookName,
                                         authorName = t1 == null ? "null" : t1.AuthorName
                                     };


 foreach (var item in result)
            {
                Console.WriteLine("{0}     {1}", item.authorName, item.bookname);           
            }

            Console.ReadKey();

}


Результат выполнения:

Для того, чтобы отображались все столбцы из массива author, необходимо использовать функцию DefaultIfEmpty(), которая есть в  LINQ TO SQL.
Конструкция authorName = t1 == null ? "null" : t1.AuthorName проверяет на NULL объект t1 и подставляет "null" типа string в результирующий набор. "null" я выбрал для наглядности, можно подставлять empty, но проверку делать необходимо т.к. иначе компилятор ругается на отсутствие объекта по ссылки при выполнении данного кода.

В следующем запросе добавим дополнительное условие:


static void Main(string[] args)
        {
получаем массивы объектов книги авторы и жанры для последующих запросов:
            Books[] books = Books.GetBooks();
            Authors[] authors = Authors.GetAuthors();
            Genres[] genres = Genres.GetGenres();


 var result = from b in books
                          join a in authors on b.AuthorId equals a.Id into temp1
                          from t1 in temp1.DefaultIfEmpty()
                          where b.BookName == "Книга №15"
                          select new
                                     {
                                         bookname = b.BookName,
                                         authorName = t1 == null ? "null" : t1.AuthorName
                                     };

 foreach (var item in result)
            {
                Console.WriteLine("{0}     {1}", item.authorName, item.bookname);           
            }

            Console.ReadKey();

}



Результат выполнения:

Выбрали авторов и книгу, которая называется "Книга №15". Автора у нее пока в массиве нету.

Ну и напоследок запрос к трем массивам: авторы, книги и жанры.

static void Main(string[] args)
        {
получаем массивы объектов книги авторы и жанры для последующих запросов:
            Books[] books = Books.GetBooks();
            Authors[] authors = Authors.GetAuthors();
            Genres[] genres = Genres.GetGenres();


 var result = from b in books
                          join a in authors on b.AuthorId equals a.Id into temp1
                          from t1 in temp1.DefaultIfEmpty()
                          join g in genres on b.GenreId equals g.Id into temp2
                          from t2 in temp2.DefaultIfEmpty()
                          select new
                                    {
                                         bookname = b.BookName,
                                         genre = t2 == null ? "null" : t2.Genre,
                                         authorName = t1 == null ? "null" : t1.AuthorName

                                     };


foreach (var item in result)
            {           
               Console.WriteLine("{0}     {1}     {2}",item.authorName,item.bookname,item.genre);             
            }

            Console.ReadKey();

        }



Результат выполнения:

По результату видно что не у всех книг есть автор и жанр. Прошу заметить, что тут реализован именно LEFT JOIN а не FULL JOIN. Т.е. если будет какой то автор у которого нет книг, (в колонке авторы будет информация а в колонке книги null) то такая запись просто не выведется в результат. Чтобы было понятнее я переставил местами колонки книга и автор:




Всем спасибо за внимание и успехов!