Русский
Русский
English
Статистика
Реклама

Ms sql server

ASP.NET Core MVC WebAPI Entity Framework Microsoft SQL Server Angular

09.08.2020 20:18:26 | Автор: admin


Введение


Небольшой курс по созданию простого веб-приложения с помощью технологий ASP.NET Core MVC, фреймворка Entity Framework, СУБД Microsoft SQL Server и фреймворка Angular. Тестировать web API будем через приложение Postman.

Курс состоит из нескольких частей:
  1. Создание web API с помощью ASP.NET Core MVC и Entity Framework Core.
  2. Реализация пользовательского интерфейса на Angular.
  3. Добавление аутентификации в приложение.
  4. Расширение модели приложения и рассмотрение дополнительных возможностей Entity Framework.


Часть 1. Создание web API с помощью ASP.NET Core MVC и Entity Framework Core


В качестве примера будем расматривать уже ставшее классическим приложение списка дел. Для разработки приложения я буду использовать Visual Studio 2019(в Visual Studio 2017 процесс аналогичен).

Создание проекта


Создадим новый проект ASP.NET Core Web Application в Visual Studio:



Назовем приложение и укажем путь к каталогу с проектом:



И выберем шаблон приложения API:



Модель


Создадим каталог Models и в новый каталог добавим первый класс TodoItem.cs, объекты которого будут описывать некоторые задачи списка дел в приложении:

public class TodoItem{    public int Id { get; set; }    public string TaskDescription { get; set; }    public bool IsComplete { get; set; }}


В качестве СУБД мы будем использовать Sql Server, а доступ к базе данных будет осуществляться через Entity Framework Core и для начала установим фреймворк через встроенный пакетный менеджер NuGet:



Одним из подходов в работе с Entity Framework является подход Code-First. Суть подхода заключается в том, что на основе модели приложения(в нашем случае модель представляет единственный класс TodoItem.cs) формируется струткура базы данных(таблицы, первичные ключи, ссылки), вся эта работа происходит как бы за кулисами и напрямую с SQL мы не работаем. Обязательным условием класса модели является наличие поля первичного ключа, по умолчанию Entity Framework ищет целочисленное поле в имени которого присутствует подстрока id и формирует на его основе первичный ключ. Переопределить такое поведение можно с помощью специальных атрибутов или используя возможности Fluent API.
Главным компонентом в работе с Entity Framework является класс контекста базы данных, через который собственно и осуществляется доступ к данным в таблицах:

public class EFTodoDBContext : DbContext{    public EFTodoDBContext(DbContextOptions<EFTodoDBContext> options) : base(options)     { }    public DbSet<TodoItem> TodoItems{ get; set; }}


Базовый класс DbContext создает контекст БД и обеспечивает доступ к функциональности Entity Framework.
Для хранения данных приложения мы будем использовать SQL Server 2017 Express. Строки подключения хранятся в файле JSON под названием appsettings.json:

{  "ConnectionStrings": {    "DefaultConnection": "Server=.\\SQLEXPRESS;Database=Todo;Trusted_Connection=true"  }}


Далее нужно внести изменения в класс Startup.cs, добавив в метод ConfigureServices() следующий код:

services.AddDbContext<EFTodoDBContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));


Метод AddDbContext() настраивает службы, предоставляемые инфраструктурой Entity Framework Core для класса контекста базы EFTodoDBContext. Аргументом метода AddDbContext () является лямбда-выражение, которое получает объект options, конфигурирующий базу данных для класса контекста. В этом случае база данных конфигурируется с помощью метода UseSqlServer() и указания строки подключения.
Определим основные операции для работы с задачами в интерфейсе ITodoRepository:

 public interface ITodoRepository {    IEnumerable<TodoItem> Get();    TodoItem Get(int id);    void Create(TodoItem item);    void Update(TodoItem item);    TodoItem Delete(int id); }


Данный интерфейс позволяет нам не задумываться о конкретной реализации хранилища данных, возможно мы точно не определились с выбором СУБД или ORM фреймворком, сейчас это не важно, класс описывающий доступ к данным будет наследовать от этого интерфейса.
Реализуем репозиторий, который как уже сказано ранее, будет наследовать от ITodoRepository и использовать в качестве источника данных EFTodoDBContext:

public class EFTodoRepository : ITodoRepository{    private EFTodoDBContext Context;    public IEnumerable<TodoItem> Get()    {        return Context.TodoItems;    }    public TodoItem Get(int Id)    {        return Context.TodoItems.Find(Id);    }    public EFTodoRepository(EFTodoDBContext context)    {        Context = context;    }    public void Create(TodoItem item)    {        Context.TodoItems.Add(item);        Context.SaveChanges();    }    public void Update(TodoItem updatedTodoItem)    {        TodoItem currentItem = Get(updatedTodoItem.Id);        currentItem.IsComplete = updatedTodoItem.IsComplete;        currentItem.TaskDescription = updatedTodoItem.TaskDescription;        Context.TodoItems.Update(currentItem);        Context.SaveChanges();        }    public TodoItem Delete(int Id)    {        TodoItem todoItem = Get(Id);        if (todoItem != null)        {            Context.TodoItems.Remove(todoItem);            Context.SaveChanges();        }        return todoItem;    }    }


Контроллер


Контроллер, реализация которого будет описана ниже, ничего не будет знать о контексте данных EFTodoDBContext, а будет использовать в своей работе только интерфейс ITodoRepository, что позволяет изменить источник данных не меняя при этом контроллера. Такой подход Адам Фримен в своей книге Entity Framework Core 2 для ASP.NET Core MVC для профессионалов назвал паттерн Хранилище.
Контроллер реализует обработчики стандартных методов HTTP-запросов: GET, POST, PUT, DELETE, которые будут изменять состояние наших задач, описанных в классе TodoItem.cs. Добавим в каталог Controllers класс TodoController.cs со следующим содержимым:

[Route("api/[controller]")]public class TodoController : Controller{    ITodoRepository TodoRepository;    public TodoController(ITodoRepository todoRepository)    {        TodoRepository = todoRepository;    }    [HttpGet(Name = "GetAllItems")]    public IEnumerable<TodoItem> Get()    {        return TodoRepository.Get();    }    [HttpGet("{id}", Name = "GetTodoItem")]    public IActionResult Get(int Id)    {        TodoItem todoItem = TodoRepository.Get(Id);        if (todoItem == null)        {            return NotFound();        }        return new ObjectResult(todoItem);    }    [HttpPost]    public IActionResult Create([FromBody] TodoItem todoItem)     {        if (todoItem == null)        {            return BadRequest();        }        TodoRepository.Create(todoItem);        return CreatedAtRoute("GetTodoItem", new { id = todoItem.Id }, todoItem);    }    [HttpPut("{id}")]    public IActionResult Update(int Id, [FromBody] TodoItem updatedTodoItem)    {        if (updatedTodoItem == null || updatedTodoItem.Id != Id)        {            return BadRequest();        }        var todoItem = TodoRepository.Get(Id);        if (todoItem == null)        {            return NotFound();        }        TodoRepository.Update(updatedTodoItem);        return RedirectToRoute("GetAllItems");    }    [HttpDelete("{id}")]    public IActionResult Delete(int Id)    {        var deletedTodoItem = TodoRepository.Delete(Id);        if (deletedTodoItem == null)        {            return BadRequest();        }        return new ObjectResult(deletedTodoItem);    } }


Перед определением класса указан атрибут с описанием шаблона маршрута для доступа к контроллеру: [Route(api/[controller])]. Контроллер TodoController будет доступен по следующему маршруту: https://<ip хоста>:<порт>/api/todo. В [controller] указывается название класса контроллера в нижнем регистре, опуская часть Controller.
Перед определением каждого метода в контроллере TodoController указан специальный атрибут вида: [<метод HTTP>(параметр,Name = псевдоним метода)]. Атрибут определяет какой HTTP-запрос будет обработан данным методом, параметр, который передается в URL запроса и псевдоним метода с помощью которого можно переотправлять запрос. Если не указать атрибут, то по умолчанию инфраструктура MVC попытается найти самый подходящий метод в контроллере для обработки запроса исходя из названия метода и указанных параметров в запросе, так, если не указать в контроллере TodoController атрибут для метода Get(), то при HTTP-запросе методом GET: https://<ip хоста>:<порт>/api/todo, инфраструткура определит для обработки запроса метод Get() контроллера.
В своем конструкторе контроллер получает ссылку на объект типа ITodoRepository, но пока что инфраструктура MVC не знает, какой объект подставить при создании контроллера. Нужно создать сервис, который однозначно разрешит эту зависисмость, для этого внесем некотрые изменения в класс Startup.cs, добавив в метод ConfigureServices() следующий код:

services.AddTransient<ITodoRepository, EFTodoRepository>();


Метод AddTransient<ITodoRepository, EFTodoRepository>() определяет сервис, который каждый раз, когда требуется экземпляр типа ITodoRepository, например в контроллере, создает новый экземпляр класс EFTodoRepository.
Полный код класса Startup.cs:

public class Startup{    public Startup(IConfiguration configuration)    {        Configuration = configuration;    }    public IConfiguration Configuration { get; }    public void ConfigureServices(IServiceCollection services)    {        services.AddControllers();        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);        services.AddDbContext<EFTodoDBContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));        services.AddTransient<ITodoRepository, EFTodoRepository>();    }    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)    {        if (env.IsDevelopment())        {            app.UseDeveloperExceptionPage();        }        app.UseHttpsRedirection();        app.UseRouting();        app.UseAuthorization();        app.UseEndpoints(endpoints =>        {            endpoints.MapControllers();        });    } }


Миграции


Для того чтобы Entity Framework сгенерировал базу данных и таблицы на основе модели, нужно использовать процесс миграции базы данных. Миграции это группа команд, которая выполняет подготовку базы данных для работы с Entity Framework. Они используются для создания и синхронизации базы данных. Команды можно выполнять как в консоли диспетчера пакетов (Package Manager Console), так и в Power Shell(Developer Power Shell). Мы будем использовать консоль диспетчера пакетов, для работы с Entity Framework потребуется установить пакет Microsoft.EntityFrameworkCore.Tools:



Запустим консоль диспетчера пакетов и выполним команду Add-Migration Initial:





В проекте появится новый каталог Migrations, в котором будут хранится классы миграции, на основе которых и будут создаваться объекты в базе данных после выполнения команды Update-Database:



Web API готово, запустив приложение на локальном IIS Express мы можем протестировать работу контроллера.

Тестирование WebAPI


Создадим новую коллекцию запросов в Postman под названием TodoWebAPI:



Так как наша база пуста, протестируем для начала создание новой задачи. В контроллере за создание задач отвечает метод Create(), который будет обрабатывать HTTP запрос отправленный методом POST и будет содержать в теле запроса сериализированный объект TodoItem в JSON формате. Аттрибут [FromBody] перед параметром todoItem в методе Create() подсказывает инфраструктуре MVC, что нужно десериализировать объект TodoItem из тела запроса и передать его в качестве параметра методу. Создадим запрос в Postman, который отправит на webAPI запрос на создание новой задачи:



Метод Create() после успешного создания задачи перенаправляет запрос на метод Get() с псевдонимом GetTodoItem и передает в качестве параметра Id только что созданной задачи, в результате чего в ответ на запрос мы получим созданный объект задачи в формате JSON.

Отправив HTTP запрос методом PUT и указав при этом в URL Id(http://personeltest.ru/aways/localhost:44370/api/todo/1) уже созданного объекта, а в теле запроса передав объект с некоторыми изменениями в формате JSON, мы изменим этот объект в базе:



HTTP запросом с методом GET без указания параметров получим все объекты в базе:



Запрос HTTP с методом DELETE и указанием Id объекта в URL(http://personeltest.ru/aways/localhost:44370/api/todo/2), удалит объект из базы и вернет JSON с удаленной задачей:



На этом все, в следующей части реализуем пользовательский интерфейс с помощью JavaScript-фреймворка Angular.
Подробнее..

ASP.NET Core MVC WebAPI Entity Framework Microsoft SQL Server Angular. Часть 1

09.08.2020 22:07:12 | Автор: admin


Введение


Небольшой курс по созданию простого веб-приложения с помощью технологий ASP.NET Core MVC, фреймворка Entity Framework, СУБД Microsoft SQL Server и фреймворка Angular. Тестировать web API будем через приложение Postman.

Курс состоит из нескольких частей:
  1. Создание web API с помощью ASP.NET Core MVC и Entity Framework Core.
  2. Реализация пользовательского интерфейса на Angular.
  3. Добавление аутентификации в приложение.
  4. Расширение модели приложения и рассмотрение дополнительных возможностей Entity Framework.


Часть 1. Создание web API с помощью ASP.NET Core MVC и Entity Framework Core


В качестве примера будем расматривать уже ставшее классическим приложение списка дел. Для разработки приложения я буду использовать Visual Studio 2019(в Visual Studio 2017 процесс аналогичен).

Создание проекта


Создадим новый проект ASP.NET Core Web Application в Visual Studio:



Назовем приложение и укажем путь к каталогу с проектом:



И выберем шаблон приложения API:



Модель


Создадим каталог Models и в новый каталог добавим первый класс TodoItem.cs, объекты которого будут описывать некоторые задачи списка дел в приложении:

public class TodoItem{    public int Id { get; set; }    public string TaskDescription { get; set; }    public bool IsComplete { get; set; }}


В качестве СУБД мы будем использовать Sql Server, а доступ к базе данных будет осуществляться через Entity Framework Core и для начала установим фреймворк через встроенный пакетный менеджер NuGet:



Одним из подходов в работе с Entity Framework является подход Code-First. Суть подхода заключается в том, что на основе модели приложения(в нашем случае модель представляет единственный класс TodoItem.cs) формируется струткура базы данных(таблицы, первичные ключи, ссылки), вся эта работа происходит как бы за кулисами и напрямую с SQL мы не работаем. Обязательным условием класса модели является наличие поля первичного ключа, по умолчанию Entity Framework ищет целочисленное поле в имени которого присутствует подстрока id и формирует на его основе первичный ключ. Переопределить такое поведение можно с помощью специальных атрибутов или используя возможности Fluent API.
Главным компонентом в работе с Entity Framework является класс контекста базы данных, через который собственно и осуществляется доступ к данным в таблицах:

public class EFTodoDBContext : DbContext{    public EFTodoDBContext(DbContextOptions<EFTodoDBContext> options) : base(options)     { }    public DbSet<TodoItem> TodoItems{ get; set; }}


Базовый класс DbContext создает контекст БД и обеспечивает доступ к функциональности Entity Framework.
Для хранения данных приложения мы будем использовать SQL Server 2017 Express. Строки подключения хранятся в файле JSON под названием appsettings.json:

{  "ConnectionStrings": {    "DefaultConnection": "Server=.\\SQLEXPRESS;Database=Todo;Trusted_Connection=true"  }}


Далее нужно внести изменения в класс Startup.cs, добавив в метод ConfigureServices() следующий код:

services.AddDbContext<EFTodoDBContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));


Метод AddDbContext() настраивает службы, предоставляемые инфраструктурой Entity Framework Core для класса контекста базы EFTodoDBContext. Аргументом метода AddDbContext () является лямбда-выражение, которое получает объект options, конфигурирующий базу данных для класса контекста. В этом случае база данных конфигурируется с помощью метода UseSqlServer() и указания строки подключения.
Определим основные операции для работы с задачами в интерфейсе ITodoRepository:

 public interface ITodoRepository {    IEnumerable<TodoItem> Get();    TodoItem Get(int id);    void Create(TodoItem item);    void Update(TodoItem item);    TodoItem Delete(int id); }


Данный интерфейс позволяет нам не задумываться о конкретной реализации хранилища данных, возможно мы точно не определились с выбором СУБД или ORM фреймворком, сейчас это не важно, класс описывающий доступ к данным будет наследовать от этого интерфейса.
Реализуем репозиторий, который как уже сказано ранее, будет наследовать от ITodoRepository и использовать в качестве источника данных EFTodoDBContext:

public class EFTodoRepository : ITodoRepository{    private EFTodoDBContext Context;    public IEnumerable<TodoItem> Get()    {        return Context.TodoItems;    }    public TodoItem Get(int Id)    {        return Context.TodoItems.Find(Id);    }    public EFTodoRepository(EFTodoDBContext context)    {        Context = context;    }    public void Create(TodoItem item)    {        Context.TodoItems.Add(item);        Context.SaveChanges();    }    public void Update(TodoItem updatedTodoItem)    {        TodoItem currentItem = Get(updatedTodoItem.Id);        currentItem.IsComplete = updatedTodoItem.IsComplete;        currentItem.TaskDescription = updatedTodoItem.TaskDescription;        Context.TodoItems.Update(currentItem);        Context.SaveChanges();        }    public TodoItem Delete(int Id)    {        TodoItem todoItem = Get(Id);        if (todoItem != null)        {            Context.TodoItems.Remove(todoItem);            Context.SaveChanges();        }        return todoItem;    }    }


Контроллер


Контроллер, реализация которого будет описана ниже, ничего не будет знать о контексте данных EFTodoDBContext, а будет использовать в своей работе только интерфейс ITodoRepository, что позволяет изменить источник данных не меняя при этом контроллера. Такой подход Адам Фримен в своей книге Entity Framework Core 2 для ASP.NET Core MVC для профессионалов назвал паттерн Хранилище.
Контроллер реализует обработчики стандартных методов HTTP-запросов: GET, POST, PUT, DELETE, которые будут изменять состояние наших задач, описанных в классе TodoItem.cs. Добавим в каталог Controllers класс TodoController.cs со следующим содержимым:

[Route("api/[controller]")]public class TodoController : Controller{    ITodoRepository TodoRepository;    public TodoController(ITodoRepository todoRepository)    {        TodoRepository = todoRepository;    }    [HttpGet(Name = "GetAllItems")]    public IEnumerable<TodoItem> Get()    {        return TodoRepository.Get();    }    [HttpGet("{id}", Name = "GetTodoItem")]    public IActionResult Get(int Id)    {        TodoItem todoItem = TodoRepository.Get(Id);        if (todoItem == null)        {            return NotFound();        }        return new ObjectResult(todoItem);    }    [HttpPost]    public IActionResult Create([FromBody] TodoItem todoItem)     {        if (todoItem == null)        {            return BadRequest();        }        TodoRepository.Create(todoItem);        return CreatedAtRoute("GetTodoItem", new { id = todoItem.Id }, todoItem);    }    [HttpPut("{id}")]    public IActionResult Update(int Id, [FromBody] TodoItem updatedTodoItem)    {        if (updatedTodoItem == null || updatedTodoItem.Id != Id)        {            return BadRequest();        }        var todoItem = TodoRepository.Get(Id);        if (todoItem == null)        {            return NotFound();        }        TodoRepository.Update(updatedTodoItem);        return RedirectToRoute("GetAllItems");    }    [HttpDelete("{id}")]    public IActionResult Delete(int Id)    {        var deletedTodoItem = TodoRepository.Delete(Id);        if (deletedTodoItem == null)        {            return BadRequest();        }        return new ObjectResult(deletedTodoItem);    } }


Перед определением класса указан атрибут с описанием шаблона маршрута для доступа к контроллеру: [Route(api/[controller])]. Контроллер TodoController будет доступен по следующему маршруту: https://<ip хоста>:<порт>/api/todo. В [controller] указывается название класса контроллера в нижнем регистре, опуская часть Controller.
Перед определением каждого метода в контроллере TodoController указан специальный атрибут вида: [<метод HTTP>(параметр,Name = псевдоним метода)]. Атрибут определяет какой HTTP-запрос будет обработан данным методом, параметр, который передается в URL запроса и псевдоним метода с помощью которого можно переотправлять запрос. Если не указать атрибут, то по умолчанию инфраструктура MVC попытается найти самый подходящий метод в контроллере для обработки запроса исходя из названия метода и указанных параметров в запросе, так, если не указать в контроллере TodoController атрибут для метода Get(), то при HTTP-запросе методом GET: https://<ip хоста>:<порт>/api/todo, инфраструткура определит для обработки запроса метод Get() контроллера.
В своем конструкторе контроллер получает ссылку на объект типа ITodoRepository, но пока что инфраструктура MVC не знает, какой объект подставить при создании контроллера. Нужно создать сервис, который однозначно разрешит эту зависисмость, для этого внесем некотрые изменения в класс Startup.cs, добавив в метод ConfigureServices() следующий код:

services.AddTransient<ITodoRepository, EFTodoRepository>();


Метод AddTransient<ITodoRepository, EFTodoRepository>() определяет сервис, который каждый раз, когда требуется экземпляр типа ITodoRepository, например в контроллере, создает новый экземпляр класс EFTodoRepository.
Полный код класса Startup.cs:

public class Startup{    public Startup(IConfiguration configuration)    {        Configuration = configuration;    }    public IConfiguration Configuration { get; }    public void ConfigureServices(IServiceCollection services)    {        services.AddControllers();        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);        services.AddDbContext<EFTodoDBContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));        services.AddTransient<ITodoRepository, EFTodoRepository>();    }    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)    {        if (env.IsDevelopment())        {            app.UseDeveloperExceptionPage();        }        app.UseHttpsRedirection();        app.UseRouting();        app.UseAuthorization();        app.UseEndpoints(endpoints =>        {            endpoints.MapControllers();        });    } }


Миграции


Для того чтобы Entity Framework сгенерировал базу данных и таблицы на основе модели, нужно использовать процесс миграции базы данных. Миграции это группа команд, которая выполняет подготовку базы данных для работы с Entity Framework. Они используются для создания и синхронизации базы данных. Команды можно выполнять как в консоли диспетчера пакетов (Package Manager Console), так и в Power Shell(Developer Power Shell). Мы будем использовать консоль диспетчера пакетов, для работы с Entity Framework потребуется установить пакет Microsoft.EntityFrameworkCore.Tools:



Запустим консоль диспетчера пакетов и выполним команду Add-Migration Initial:





В проекте появится новый каталог Migrations, в котором будут хранится классы миграции, на основе которых и будут создаваться объекты в базе данных после выполнения команды Update-Database:



Web API готово, запустив приложение на локальном IIS Express мы можем протестировать работу контроллера.

Тестирование WebAPI


Создадим новую коллекцию запросов в Postman под названием TodoWebAPI:



Так как наша база пуста, протестируем для начала создание новой задачи. В контроллере за создание задач отвечает метод Create(), который будет обрабатывать HTTP запрос отправленный методом POST и будет содержать в теле запроса сериализированный объект TodoItem в JSON формате. Аттрибут [FromBody] перед параметром todoItem в методе Create() подсказывает инфраструктуре MVC, что нужно десериализировать объект TodoItem из тела запроса и передать его в качестве параметра методу. Создадим запрос в Postman, который отправит на webAPI запрос на создание новой задачи:



Метод Create() после успешного создания задачи перенаправляет запрос на метод Get() с псевдонимом GetTodoItem и передает в качестве параметра Id только что созданной задачи, в результате чего в ответ на запрос мы получим созданный объект задачи в формате JSON.

Отправив HTTP запрос методом PUT и указав при этом в URL Id(http://personeltest.ru/aways/localhost:44370/api/todo/1) уже созданного объекта, а в теле запроса передав объект с некоторыми изменениями в формате JSON, мы изменим этот объект в базе:



HTTP запросом с методом GET без указания параметров получим все объекты в базе:



Запрос HTTP с методом DELETE и указанием Id объекта в URL(http://personeltest.ru/aways/localhost:44370/api/todo/2), удалит объект из базы и вернет JSON с удаленной задачей:



На этом все, в следующей части реализуем пользовательский интерфейс с помощью JavaScript-фреймворка Angular.
Подробнее..

Из песочницы Как выбрать инструмент для бизнес-анализа

08.10.2020 18:22:32 | Автор: admin

Какой у Вас выбор?


Часто применение дорогих и сложных BI-систем может быть заменено простыми и относительно недорогими, но достаточно эффективными аналитическими инструментами. Прочитав эту статью, Вы сможете оценить Ваши потребности в области бизнес-аналитики и понять какой вариант лучше подойдет для Вашего бизнеса.

Разумеется, все BI-системы имеют чрезвычайно сложную архитектуру и их внедрение в компании, является не простой задачей, требующей крупной суммы для решения и интеграторов высокой квалификации. Вам придется неоднократно прибегать к их услугам, так как внедрением и введением в эксплуатацию все не закончится в будущем будет требоваться доработка функционала, разработка новых отчетов и показателей. Нужно учесть, что в случае успешной работы системы, вы захотите, чтобы в ней работало все больше сотрудников, а это означает приобретение дополнительных пользовательских лицензий.

Еще одной неотъемлемой чертой продвинутых бизнес-аналитических систем, является чрезвычайно большой набор функций, многими из которых Вы никогда не воспользуетесь, но будете за них продолжать платить каждый раз, продлевая лицензии.

Приведенные особенности BI-систем заставляют задуматься о подборе альтернативы. Далее я предлагаю сравнить решение стандартного набора задач при подготовке отчетности с помощью Power BI и Excel.

Power BI или Excel?


Как правило, для построения квартального отчета по продажам, аналитик выгружает данные из учетных систем, сопоставляет их со своими справочниками и собирает с помощью функции ВПР в одну таблицу, на основе которой строится отчет.

А как решается эта задача с помощью Power BI?

Данные из источников загружаются в систему и подготавливаются к анализу: разбиваются на таблицы, очищаются и сопоставляются. После этого конструируется бизнес-модель: таблицы связываются друг с другом, определяются показатели, создаются пользовательские иерархии. Следующий этап визуализация. Здесь, простым перетаскиванием элементов управления и виджетов, формируется интерактивный дашборд. Все элементы оказываются связанными через модель данных. При анализе это позволяет сконцентрироваться на нужной информации, отфильтровав ее во всех представлениях одним нажатием на любой элемент дашборда.

Какие преимущества применения Power BI по сравнению с традиционным подходом можно заметить в приведенном примере?

1 Автоматизация процедуры получения данных и подготовка их к анализу.
2 Построение бизнес-модели.
3 Невероятная визуализация.
4 Разграниченный доступ к отчетам.

А теперь давайте разберем каждый пункт по отдельности.

1 Для подготовки данных к построению отчета, нужно единожды определить процедуру, выполняющую подключение к данным и их обработку и каждый раз, когда понадобится получить отчет за другой период, Power BI будет пропускать данные через созданную процедуру. Таким образом автоматизируется большая часть работы по подготовки данных к анализу. Но дело в том, что Power BI осуществляет процедуру подготовки данных с помощью инструмента, который доступен в классической версии Excel, и называется он Power Query. Он позволяет выполнить поставленную задачу в Excel абсолютно тем же способом.

2 Здесь та же ситуация. Инструмент Power BI для построения бизнес-модели имеется и в Excel это Power Pivot.

3 Как Вы, наверное, уже догадались, с визуализацией дело обстоит подобным образом: расширение Excel Power View справляется с этой задачей на ура.

4 Остается разобраться с доступом к отчетам. Тут не так все радужно. Дело в том, что Power BI это облачный сервис, доступ к которому осуществляется через персональную учетную запись. Администратор сервиса распределяет пользователей по группам и задает для этих групп различный уровень доступа к отчетам. Этим достигается разграничение прав доступа между сотрудниками компании. Таким образом, аналитики, менеджеры и директора заходя на одну и туже страницу видят отчет в доступном для них представлении. Может быть ограничен доступ к определенному набору данных, либо к отчету целиком. Однако, если отчет находится в файле формата Excel, то усилиями системного администратора можно попытаться решить задачу с доступом, но это будет уже не то. Я еще вернусь к рассмотрению этой задачи, когда буду описывать особенности корпоративного портала.

Стоит отметить, что, как правило, потребность компании в сложных и красивых дашбордах не велика и часто, для анализа данных в Excel, после построения бизнес-модели не прибегают к возможностям Power View, а пользуются сводными таблицами. Они предоставляют OLAP-функциональность, которой вполне достаточно чтобы решить большинство бизнес-аналитических задач.

Таким образом, вариант ведения бизнес-анализа в Excel вполне может удовлетворить потребности средней компании с небольшим количеством сотрудников, нуждающихся в отчетах. Тем не менее, если запросы Ваше компании более амбициозны, не торопитесь прибегать к инструментам, которые решат все и сразу.

Предлагаю Вашему вниманию более профессиональный подход, воспользовавшись которым, Вы получите свою собственную, полностью управляемую, автоматизированную систему построения бизнес-аналитических отчетов с разграниченным доступом к ним.

ETL и DWH


В ранее рассматриваемых подходах к построению бизнес-отчетов, загрузка и подготовка данных к анализу осуществлялась с помощью технологии Power Query. Этот способ остается вполне оправданным и эффективным до тех пор, пока источников данных не много: одна учетная система и справочники из Excel-таблиц. Однако, с увеличением числа учетных систем, решение этой задачи посредством Power Query становится очень громоздким, трудным для поддерживания и развития. В таких случаях на помощь приходят инструменты для ETL.

С их помощью осуществляется выгрузка данных из источников (Extract), их преобразование (Transform), что подразумевает очистку и сопоставление, и загрузка в хранилище данных (Load). Хранилище данных (DWH Data Warehouse) это, как правило, реляционная база данных, расположенная на сервере. Эта база содержит данные, пригодные для анализа. По расписанию запускается ETL-процесс, который обновляет данные хранилища до актуальных. Кстати говоря, всю эту кухню прекрасно обслуживает Integration Services, входящие в состав MS SQL Server.

Далее, как и раньше для построения бизнес-модели данных и визуализации можно воспользоваться Excel, Power BI, либо другими аналитическими инструментами, такими как Tableau или Qlik Sense. Но прежде, мне бы хотелось обратить Ваше внимание еще на одну возможность, о которой Вы могли не знать, несмотря на то, что она Вам давно доступна. Речь идет о построении бизнес-моделей с помощью аналитических служб MS SQL Server, а именно Analysis Services.

Модели данных в MS Analysis Services


Этот раздел статьи будет более интересен тем, кто уже использует MS SQL Server в своей компании.

На данный момент службы Analysis Services предоставляют два вида моделей данных это многомерная и табличная модели. Кроме того, что данные в этих моделях связаны, значения показателей модели предварительно агрегируются и хранятся в ячейках OLAP кубов, доступ к которым осуществляется MDX, либо DAX запросами. За счет такой архитектуры хранения данных, запрос, который охватывает миллионы записей, возвращается за секунды. Такой способ доступа к данным необходим компаниям, таблицы транзакций которых содержат от миллиона записей (верхний придел не ограничен).

Excel, Power BI и многие другие солидные инструменты умеют подключаться к таким моделям и визуализировать данные их структур.

Если Вы пошли продвинутым путем: автоматизировали процесс ETL и построили бизнес-модели при помощи служб MS SQL Server, то Вы достойны иметь свой собственный корпоративный портал.

Корпоративный портал


Через него администраторы будут осуществлять мониторинг и управление процессом построения отчетов. Наличие портала позволит унифицировать справочники компании: информация о клиентах, продуктах, менеджерах, поставщиках, будет доступна для сопоставления, редактирования и скачивания в одном месте для всех кто ей пользуется. На портале можно реализовать выполнение различных функций по изменению данных учетных систем, например, управлять репликацией данных. А самое главное, с помощью портала, благополучно решается проблема организации разграниченного доступа к отчетам сотрудники будут видеть только те отчеты, которые были подготовленные персонально для их отделов в предназначенном для них виде.

Однако, пока не понятно, как будет организовано отображение отчетов на странице портала. Чтобы ответить на этот вопрос, сначала нужно определиться с технологией, на основе которой будет строиться портал. Я предлагаю взять за основу один из фреймворков: ASP.NET MVC/Web Forms/Core, либо Microsoft SharePoint. Если в Вашей компании имеется хотя бы один .NET разработчик, то выбор не составит труда. Теперь можно подбирать встраиваемый в приложение OLAP-клиент, способный подключаться к многомерным или табличным моделям служб Analysis Services.

Выбор OLAP-клиента для визуализации


Сравним несколько инструментов по уровню сложности встраивания, функциональности и цене: Power BI, компоненты Telerik UI for ASP.NET MVC и компоненты RadarCube ASP.NET MVC.

Power BI


Чтобы организовать доступ сотрудников компании к отчетам Power BI на странице своего портала, нужно воспользоваться функцией Power BI Embedded.

Сразу скажу, что потребуется лицензия Power BI Premium и дополнительная выделенная емкость. Наличие выделенной емкости позволяет публиковать дашборды и отчеты для пользователей в организации без необходимости приобретать лицензии для них.

Сначала отчет, сформированный в Power BI Desktop, публикуется на портале Power BI и потом, с помощью не простой настройки, встраивается в страницу web-приложения.

С процедурой формирования простого отчета и его публикацией вполне может справится аналитик, а вот со встраиванием могут возникнуть серьезные проблемы. Так же очень непросто разобраться с механизмом работы этого инструмента: большое количество настроек облачного сервиса, множество подписок, лицензий, емкостей сильно повышают требование к уровню подготовки специалиста. Так что эту задачу лучше поручить IT-специалисту.

Компоненты Telerik и RadarCube


Для встраивания компонентов Telerik и RadarCube достаточно владеть программными технологиями на базовом уровне. Поэтому профессиональных навыков одного программиста из IT-отдела будет вполне достаточно. Все что нужно, это разместить компонент на web-странице и настроить их под свои нужды.

Компонент PivotGrid из набора Telerik UI for ASP.NET MVC встраивается на страницу в изящной манере Razor и предоставляет самые необходимые OLAP-функции. Однако, если требуется более гибкие настройки интерфейса и развитый функционал, то лучше использовать компоненты RadarCube ASP.NET MVC. Большое количество настроек, богатый функционал с возможностями его переопределения и расширения, позволят создать OLAP-отчет любой сложности.

Ниже приведу таблицу сравнения характеристик рассматриваемых инструментов по шкале Низкий-Средний-Высокий.

Power BI Telerik UI for ASP.NET MVC RadarCube ASP.NET MVC
Визуализация Высокий Низкий Средний
Набор OLAP-функций Высокий Низкий Высокий
Гибкость настройки Высокий Высокий Высокий
Возможность переопределения функций - - +
Программная кастомизация - - +
Уровень сложности встраивания и настройки Высокий Низкий Средний
Минимальная стоимость Power BI Premium EM3

190 000 руб./месяц
Лицензия на одного разработчика

90 000 руб.

Лицензия на одного разработчика

25 000 руб.


Теперь можно перейти к определению критериев для выбор аналитического инструмента.

Условия выбора Power BI


  • Вас интересуют отчеты, насыщенные разнообразными показателями и элементами, связанными с данными.
  • Вы хотите, чтобы сотрудники, работающие с отчетами, могли легко и быстро получать ответы на поставленные бизнес-задачи в интуитивно понятной форме.
  • В штате компании имеется IT-специалист, с навыками BI-разработки.
  • В бюджет компании заложена крупная сумму на ежемесячную оплату облачного сервиса бизнес-аналитики.

Условия выбора компонентов Telerik


  • Нужен простой OLAP-клиент для Ad hock анализа.
  • В штате компании имеется .NET разработчик начального уровня.
  • Небольшой бюджет на разовую покупку лицензии и дальнейшее ее продление со скидкой менее 20%.

Условия выбора компонентов RadarCube


  • Необходим многофункциональный OLAP-клиент с возможностью кастомизации интерфейса, а также поддерживающий встраивание собственных функций.
  • В штате компании имеется .NET разработчик среднего уровня. Если такого нет, то разработчики компонента любезно предоставят свои услуги, но за дополнительную плату, не превышающую уровня оплаты труда штатного программиста.
  • Небольшой бюджет на разовую покупку лицензии и дальнейшее ее продление со скидкой 60%.

Заключение


Правильный выбор инструмента для бизнес-аналитики позволит полностью отказаться от формирования отчетности в Excel. Ваша компания сможет постепенно и безболезненно перейти к использованию передовых технологий в области BI и автоматизировать работу аналитиков всех отделов.
Подробнее..

Путеводитель по репликации баз данных

10.08.2020 14:22:42 | Автор: admin
Повторяться, но каждый раз по-новому разве не это есть искусство?

Станислав Ежи Лец, из книги Непричёсанные мысли

Словарь определяет репликацию как процесс поддержания двух (или более) наборов данных в согласованном состоянии. Что такое согласованное состояние наборов данных отдельный большой вопрос, поэтому переформулируем определение проще: процесс изменения одного набора данных, называемого репликой, в ответ на изменения другого набора данных, называемого основным. Совсем не обязательно наборы при этом будут одинаковыми.



Поддержка репликации баз данных одна из важнейших задач администратора: почти у каждой сколько-нибудь важной базы данных есть реплика, а то и не одна.

Среди задач, решаемых репликацией, можно назвать как минимум

  • поддержку резервной базы данных на случай потери основной;
  • снижение нагрузки на базу за счёт переноса части запросов на реплики;
  • перенос данных в архивные или аналитические системы.

В этой статье я расскажу о видах репликации и о том, какие задачи решает каждый вид репликации.


Можно выделить три подхода к репликации:

  • Блочная репликация на уровне системы хранения данных;
  • Физическая репликация на уровне СУБД;
  • Логическая репликация на уровне СУБД.

Блочная репликация


При блочной репликации каждая операция записи выполняется не только на основном диске, но и на резервном. Таким образом тому на одном массиве соответствует зеркальный том на другом массиве, с точностью до байта повторяющий основной том:



К достоинствам такой репликации можно отнести простоту настройки и надёжность. Записывать данные на удалённый диск может либо дисковый массив, либо нечто (устройство или программное обеспечение), стоящее между хостом и диском.

Дисковые массивы могут быть дополнены опциями, позволяющими включить репликацию. Название опции зависит от производителя массива:

Производитель Торговая марка
EMC SRDF (Symmetrix Remote Data Facility)
IBM Metro Mirror синхронная репликация
Global Mirror асинхронная репликация
Hitachi TrueCopy
Hewlett-Packard Continuous Access
Huawei HyperReplication

Если дисковый массив не способен реплицировать данные, между хостом и массивом может быть установлен агент, осуществляющей запись на два массива сразу. Агент может быть как отдельным устройством (EMC VPLEX), так и программным компонентом (HPE PeerPersistence, Windows Server Storage Replica, DRBD). В отличие от дискового массива, который может работать только с таким же массивом или, как минимум, с массивом того же производителя, агент может работать с совершенно разными дисковыми устройствами.

Главное назначение блочной репликации обеспечение отказоустойчивости. Если база данных потеряна, то можно перезапустить её с использованием зеркального тома.

Блочная репликация хороша своей универсальностью, но за универсальность приходится платить.

Во-первых, никакой сервер не может работать с зеркальным томом, поскольку его операционная система не может управлять записью на него; с точки зрения наблюдателя данные на зеркальном томе появляются сами собой. В случае аварии (отказ основного сервера или всего ЦОДа, где находится основной сервер) следует остановить репликацию, размонтировать основной том и смонтировать зеркальный том. Как только появится возможность, следует перезапустить репликацию в обратном направлении.

В случае использования агента все эти действия выполнит агент, что упрощает настройку, но не уменьшает время переключения.

Во-вторых, сама СУБД на резервном сервере может быть запущена только после монтирования диска. В некоторых операционных системах, например, в Solaris, память под кеш при выделении размечается, и время разметки пропорционально объёму выделяемой памяти, то есть старт экземпляра будет отнюдь не мгновенным. Плюс ко всему кеш после рестарта будет пуст.

В-третьих, после запуска на резервном сервере СУБД обнаружит, что данные на диске неконсистентны, и нужно потратить значительное время на восстановление с применением журналов повторного выполнения: сначала повторить те транзакции, результаты которых сохранились в журнале, но не успели сохраниться в файлы данных, а потом откатить транзакции, которые к моменту сбоя не успели завершиться.

Блочная репликация не может использоваться для распределения нагрузки, а для обновления хранилища данных используется похожая схема, когда зеркальный том находится в том же массиве, что и основной. У EMC и HP эта схема называется BCV, только EMC расшифровывает аббревиатуру как Business Continuance Volume, а HP как Business Copy Volume. У IBM на этот случай нет специальной торговой марки, эта схема так и называется mirrored volume.



В массиве создаются два тома, и операции записи синхронно выполняются на обоих (A). В определённое время зеркало разрывается (B), то есть тома становятся независимыми. Зеркальный том монтируется к серверу, выделенному для обновления хранилища, и на этом сервере поднимается экземпляр базы данных. Экземпляр будет подниматься так же долго, как и при восстановлении с помощью блочной репликации, но это время может быть существенно уменьшено за счёт разрыва зеркала в период минимальной нагрузки. Дело в том, что разрыв зеркала по своим последствиям эквивалентен аварийному завершению СУБД, а время восстановление при аварийном завершении существенно зависит от количества активных транзакций в момент аварии. База данных, предназначенная для выгрузки, доступна как на чтение, так и на запись. Идентификаторы всех блоков, изменённых после разрыва зеркала как на основном, так и на зеркальном томе, сохраняются в специальной области Block Change Tracking BCT.

После окончания выгрузки зеркальный том размонтируется (С), зеркало восстанавливается, и через некоторое время зеркальный том вновь догоняет основной и становится его копией.

Физическая репликация


Журналы (redo log или write-ahead log) содержат все изменения, которые вносятся в файлы базы данных. Идея физической репликации состоит в том, что изменения из журналов повторно выполняются в другой базе (реплике), и таким образом данные в реплике повторяют данные в основной базе байт-в-байт.

Возможность использовать журналы базы данных для обновления реплики появилась в релизе Oracle7.3, который вышел в 1996 году, а уже в релизе Oracle 8i доставка журналов с основной базы в реплику была автоматизирована и получила название DataGuard. Технология оказалась настолько востребованной, что сегодня механизм физической репликации есть практически во всех современных СУБД.

СУБД Опция репликации
Oracle Active DataGuard
IBM DB2 HADR
Microsoft SQL Server Log shipping/Always On
PostgreSQL Log shipping/Streaming replication
MySQL Alibaba physical InnoDB replication

Опыт показывает, что если использовать сервер только для поддержания реплики в актуальном состоянии, то ему достаточно примерно 10% процессорной мощности сервера, на котором работает основная база.

Журналы СУБД не предназначены для использования вне этой платформы, их формат не документируется и может меняться без предупреждения. Отсюда совершенно естественное требование, что физическая репликация возможна только между экземплярами одной и той же версии одной той же СУБД. Отсюда же возможные ограничения на операционную систему и архитектуру процессора, которые тоже могут влиять на формат журнала.

Естественно, никаких ограничений на модели СХД физическая репликация не накладывает. Более того, файлы в базе-реплике могут располагаться совсем по-другому, чем на базе-источнике надо лишь описать соответствие между томами, на которых лежат эти файлы.

Oracle DataGuard позволяет удалить часть файлов из базы-реплики в этом случае изменения в журналах, относящиеся к этим файлам, будут проигнорированы.

Физическая репликация базы данных имеет множество преимуществ перед репликацией средствами СХД:

  • объём передаваемых данных меньше за счёт того, что передаются только журналы, но не файлы с данными; эксперименты показывают уменьшение трафика в 5-7 раз;
  • переключение на резервную базу происходит значительно быстрее: экземпляр-реплика уже поднят, поэтому при переключении ему нужно лишь откатить активные транзакции; более того, к моменту сбоя кеш реплики уже прогрет;
  • на реплике можно выполнять запросы, сняв тем самым часть нагрузки с основной базы. В частности, реплику можно использовать для создания резервных копий.

Возможность читать данные с реплики появилась в 2007 году в релизе Oracle 11g именно на это указывает эпитет active, добавленный к названию технологии DataGuard. В других СУБД возможность чтения с реплики также есть, но в названии это никак не отражено.

Запись данных в реплику невозможна, поскольку изменения в неё приходят побайтно, и реплика не может обеспечить конкурентное исполнение своих запросов. Oracle Active DataGuard в последних релизах разрешает запись в реплику, но это не более чем сахар: на самом деле изменения выполняются на основной базе, а клиент ждёт, пока они докатятся до реплики.

В случае повреждения файла в основной базе можно просто скопировать соответствующий файл с реплики (прежде, чем делать такое со своей базой, внимательно изучите руководство администратора!). Файл на реплике может быть не идентичен файлу в основной базе: дело в том, что когда файл расширяется, новые блоки в целях ускорения ничем не заполняются, и их содержимое случайно. База может использовать не всё пространство блока (например, в блоке может оставаться свободное место), но содержимое использованного пространства совпадает с точностью до байта.

Физическая репликация может быть как синхронной, так и асинхронной. При асинхронной репликации всегда есть некий набор транзакций, которые завершены на основной базе, но ещё не дошли до резервной, и в случае перехода на резервную базу при сбое основной эти транзакции будут потеряны. При синхронной репликации завершение операции commit означает, что все журнальные записи, относящиеся к данной транзакции, переданы на реплику. Важно понимать, что получение репликой журнала не означает применения изменений к данным. При потере основной базы транзакции не будут потеряны, но если приложение пишет данные в основную базу и считывает их из реплики, то у него есть шанс получить старую версию этих данных.

В PostgreSQL есть возможность сконфигурировать репликацию так, чтобы commit завершался только после применения изменений к данным реплики (опция synchronous_commit = remote_apply), а в Oracle можно сконфигурировать всю реплику или отдельные сессии, чтобы запросы выполнялись только если реплика не отстаёт от основной базы (STANDBY_MAX_DATA_DELAY=0). Однако всё же лучше проектировать приложение так, чтобы запись в основную базу и чтение из реплик выполнялись в разных модулях.

При поиске ответа на вопрос, какой режим выбрать, синхронный или асинхронный, нам на помощь приходят маркетологи Oracle. DataGuard предусматривает три режима, каждый из которых максимизирует один из параметров сохранность данных, производительность, доступность за счёт остальных:

  • Maximum performance: репликация всегда асинхронная;
  • Maximum protection: репликация синхронная; если реплика не отвечает, commit на основной базе не завершается;
  • Maximum availability: репликация синхронная; если реплика не отвечает, то репликация переключается в асинхронный режим и, как только связь восстанавливается, реплика догоняет основную базу и репликация снова становится синхронной.

Несмотря на бесспорные преимущества репликации средствами БД над блочной репликацией, администраторы во многих компаниях, особенно со старыми традициями надёжности, до сих пор очень неохотно отказываются от блочной репликации. Причин у этого две.

Во-первых, в случае репликации средствами дискового массива трафик идёт не по сети передачи данных (LAN), а по сети хранения данных (Storage Area Network). Зачастую в инфраструктурах, построенных давно, SAN гораздо надёжнее и производительнее, чем сеть передачи данных.

Во-вторых, синхронная репликация средствами СУБД стала надёжной относительно недавно. В Oracle прорыв произошёл в релизе 11g, который вышел в 2007 году, а в других СУБД синхронная репликация появилась ещё позже. Конечно, 10 лет по меркам сферы информационных технологий срок не такой уж маленький, но когда речь идёт о сохранности данных, некоторые администраторы до сих пор руководствуются принципом как бы чего не вышло

Логическая репликация


Все изменения в базе данных происходят в результате вызовов её API например, в результате выполнения SQL-запросов. Очень заманчивой кажется идея выполнять одну и ту же последовательность запросов на двух разных базах. Для репликации необходимо придерживаться двух правил:

  1. Нельзя начинать транзакцию, пока не завершены все транзакции, которые должны закончиться раньше. Так на рисунке ниже нельзя запускать транзакцию D, пока не завершены транзакции A и B.
  2. Нельзя завершать транзакцию, пока не начаты все транзакции, которые должны закончиться до завершения текущей транзакции. Так на рисунке ниже даже если транзакция B выполнилась мгновенно, завершить её можно только после того, как начнётся транзакция C.

Репликация команд (statement-based replication) реализована, например, в MySQL. К сожалению, эта простая схема не приводит к появлению идентичных наборов данных тому есть две причины.

Во-первых, не все API детерминированы. Например, если в SQL-запросе встречается функция now() или sysdate(), возвращающая текущее время, то на разных серверах она вернёт разный результат из-за того, что запросы выполняются не одновременно. Кроме того, к различиям могут привести разные состояния триггеров и хранимых функций, разные национальные настройки, влияющие на порядок сортировки, и многое другое.

Во-вторых, репликацию, основанную на параллельном исполнении команд, невозможно корректно приостановить и перезапустить.



Если репликация остановлена в момент T1 транзакция B должна быть прервана и откачена. При перезапуске репликации исполнение транзакции B может привести реплику к состоянию, отличному от состояния базы-источника: на источнике транзакция B началась до того, как закончилась транзакция A, а значит, она не видела изменений, сделанных транзакцией A.
Репликация запросов может быть остановлена и перезапущена только в момент T2, когда в базе нет ни одной активной транзакции. Разумеется, на сколько-нибудь нагруженной промышленной базе таких моментов не бывает.

Обычно для логической репликации используют детерминированные запросы. Детерминированность запроса обеспечивается двумя свойствами:

  • запрос обновляет (или вставляет, или удаляет) единственную запись, идентифицируя её по первичному (или уникальному) ключу;
  • все параметры запроса явно заданы в самом запросе.

В отличие от репликации команд (statement-based replication) такой подход называется репликацией записей (row-based replication).

Предположим, что у нас есть таблица сотрудников со следующими данными:

ID Name Dept Salary
3817 Иванов Иван Иванович 36 1800
2274 Петров Пётр Петрович 36 1600
4415 Кузнецов Семён Андреевич 41 2100

Над этой таблицей была выполнена следующая операция:

update employee set salary = salary*1.2 where dept=36;


Для того, чтобы корректно реплицировать данные, в реплике будут выполнены такие запросы:

update employee set salary = 2160 where id=3817;update employee set salary = 1920 where id=2274;


Запросы приводят к тому же результату, что и на исходной базе, но при этом не эквивалентны выполненным запросам.

База-реплика открыта и доступна не только на чтение, но и на запись. Это позволяет использовать реплику для выполнения части запросов, в том числе для построения отчётов, требующих создания дополнительных таблиц или индексов.

Важно понимать, что логическая реплика будет эквивалентна исходной базе только в том случае, если в неё не вносится никаких дополнительных изменений. Например, если в примере выше в реплике добавить в 36 отдел Сидорова, то он повышения не получит, а если Иванова перевести из 36 отдела, то он получит повышение, несмотря ни на что.

Логическая репликация предоставляет ряд возможностей, отсутствующих в других видах репликации:

  • настройка набора реплицируемых данных на уровне таблиц (при физической репликации на уровне файлов и табличных пространств, при блочной репликации на уровне томов);
  • построение сложных топологий репликации например, консолидация нескольких баз в одной или двунаправленная репликация;
  • уменьшение объёма передаваемых данных;
  • репликация между разными версиями СУБД или даже между СУБД разных производителей;
  • обработка данных при репликации, в том числе изменение структуры, обогащение, сохранение истории.

Есть и недостатки, которые не позволяют логической репликации вытеснить физическую:

  • все реплицируемые данные обязаны иметь первичные ключи;
  • логическая репликация поддерживает не все типы данных например, возможны проблемы с BLOBами.
  • логическая репликация на практике не бывает полностью синхронной: время от получения изменений до их применения слишком велико, чтобы основная база могла ждать;
  • логическая репликация создаёт большую нагрузку на реплику;
  • при переключении приложение должно иметь возможность убедиться, что все изменения с основной базы, применены на реплике СУБД зачастую сама не может этого определить, так как для неё режимы реплики и основной базы эквивалентны.

Два последних недостатка существенно ограничивают использование логической реплики как средства отказоустойчивости. Если один запрос в основной базе изменяет сразу много строк, реплика может существенно отставать. А возможность смены ролей требует недюжинных усилий как со стороны разработчиков, так и со стороны администраторов.

Есть несколько способов реализации логической репликации, и каждый из этих способов реализует одну часть возможностей и не реализует другую:

  • репликация триггерами;
  • использование журналов СУБД;
  • использование программного обеспечения класса CDC (change data capture);
  • прикладная репликация.

Репликация триггерами


Триггер хранимая процедура, которая исполняется автоматически при каком-либо действии по модификации данных. Триггеру, который вызывается при изменении каждой записи, доступны ключ этой записи, а также старые и новые значения полей. При необходимости триггер может сохранять новые значения строк в специальную таблицу, откуда специальный процесс на стороне реплики будет их вычитывать. Объём кода в триггерах велик, поэтому существуют специальное программное обеспечение, генерирующее такие триггеры, например, Репликация слиянием (merge replication) компонент Microsoft SQL Server или Slony-I отдельный продукт для репликации PostgreSQL.

Сильные стороны репликации триггерами:

  • независимость от версий основной базы и реплики;
  • широкие возможности преобразования данных.

Недостатки:

  • нагрузка на основную базу;
  • большая задержка при репликации.

Использование журналов СУБД


Сами СУБД также могут предоставлять возможности логической репликации. Источником данных, как и для физической репликации, являются журналы. К информации о побайтовом изменении добавляется также информация об изменённых полях (supplemental logging в Oracle, wal_level = logical в PostgreSQL), а также значение уникального ключа, даже если он не меняется. В результате объём журналов БД увеличивается по разным оценкам от 10 до 15%.

Возможности репликации зависят от реализации в конкретной СУБД если в Oracle можно построить logical standby, то в PostgreSQL или Microsoft SQL Server встроенными средствами платформы можно развернуть сложную систему взаимных подписок и публикаций. Кроме того, СУБД предоставляет встроенные средства мониторинга и управления репликацией.

К недостаткам данного подхода можно отнести увеличение объёма журналов и возможное увеличение трафика между узлами.

Использование CDC


Существует целый класс программного обеспечения, предназначенного для организации логической репликации. Это ПО называется CDC, change data capture. Вот список наиболее известных платформ этого класса:

  • Oracle GoldenGate (компания GoldenGate приобретена в 2009 году);
  • IBM InfoSphere Data Replication (ранее InfoSphere CDC; ещё ранее DataMirror Transformation Server, компания DataMirror приобретена в 2007 году);
  • VisionSolutions DoubleTake/MIMIX (ранее Vision Replicate1);
  • Qlik Data Integration Platform (ранее Attunity);
  • Informatica PowerExchange CDC;
  • Debezium;
  • StreamSets Data Collector...

В задачу платформы входит чтение журналов базы данных, преобразование информации, передача информации на реплику и применение. Как и в случае репликации средствами самой СУБД, журнал должен содержать информацию об изменённых полях. Использование дополнительного приложения позволяет на лету выполнять сложные преобразования реплицируемых данных и строить достаточно сложные топологии репликации.

Сильные стороны:

  • возможность репликации между разными СУБД, в том числе загрузка данных в отчётные системы;
  • широчайшие возможности обработки и преобразования данных;
  • минимальный трафик между узлами платформа отсекает ненужные данные и может сжимать трафик;
  • встроенные возможности мониторинга состояния репликации.

Недостатков не так много:

  • увеличение объёма журналов, как при логической репликации средствами СУБД;
  • новое ПО сложное в настройке и/или с дорогими лицензиями.

Именно CDC-платформы традиционно используются для обновления корпоративных хранилищ данных в режиме, близком к реальному времени.

Прикладная репликация


Наконец, ещё один способ репликации формирование векторов изменений непосредственно на стороне клиента. Клиент должен формировать детерминированные запросы, затрагивающие единственную запись. Добиться этого можно, используя специальную библиотеку работы с базой данных, например, Borland Database Engine (BDE) или Hibernate ORM.



Когда приложение завершает транзакцию, подключаемый модуль Hibernate ORM записывает вектор изменений в очередь и выполняет транзакцию в базе данных. Специальный процесс-репликатор вычитывает векторы из очереди и выполняет транзакции в базе-реплике.
Этот механизм хорош для обновления отчётных систем. Может он использоваться и для обеспечения отказоустойчивости, но в этом случае в приложении должен быть реализован контроль состояния репликации.

Традиционно сильные и слабые стороны данного подхода:

  • возможность репликации между разными СУБД, в том числе загрузка данных в отчётные системы;
  • возможность обработки и преобразования данных, мониторинга состояния ит.д.;
  • минимальный трафик между узлами платформа отсекает ненужные данные и может сжимать трафик;
  • полная независимость от базы данных как от формата, так и от внутренних механизмов.

Достоинства этого способа бесспорны, однако есть два очень серьёзных недостатка:
  • ограничения на архитектуру приложения;
  • огромный объём собственного кода, обеспечивающего репликацию.


Так что же лучше?


Однозначного ответа на этот вопрос, как и на многие другие, не существует. Но надеюсь, что таблица ниже поможет сделать правильный выбор для каждой конкретной задачи:

Блочная репликация СХД Блочная репликация агентом Физическая репликация Логическая репликация СУБД Репликация триггерами CDC Прикладная репликация
Воспроизведение источника Побайтно Побайтно Побайтно Логически Логически Логически Логически
Выборочная репликация На уровне томов На уровне томов На уровне файлов На уровне таблиц и строк На уровне таблиц и строк На уровне таблиц и строк На уровне таблиц и строк
Объём трафика X X X/7..X/5 X/7..X/5 X/10 X/10 X/10
Скорость переключения 5 мин часы 5 мин часы 1..10 мин 1..10 мин 1..2 мин 1..2 мин 1..2 мин
Гарантия переключения + + +++ +
Доступность реплики RO R/W R/W R/W R/W
Топология репликации точка-точка точка-точка
broadcast
точка-точка
broadcast
каскад
точка-точка
broadcast
каскад
встречная*
p2p*
точка-точка
broadcast
каскад
встречная*
p2p*
слияние
точка-точка
broadcast
каскад
встречная*
p2p*
слияние
точка-точка
broadcast
каскад
встречная*
p2p*
слияние
Нагрузка на источник
Простота настройки + + + + + + + + + +
Стоимость дополнительного ПО
Гетерогенные среды + + + + + + + + + + + + +

  • Блочная репликация имеет смысл, когда других способов репликации нет; для баз данных её лучше не использовать.
  • Физическая репликация хороша, когда требуется обеспечение отказоустойчивости инфраструктуры или перенос части читающих приложений на реплики.
  • Логическая репликация подходит для обеспечения отказоустойчивости только в том случае, если приложение знает об этой репликации и умеет в случае аварии ждать синхронизации реплик.
  • Логическая репликация идеальна для всевозможных отчётных баз.
  • Репликация триггерами имеет смысл в том случае, если база сильно нагружена, а реплицировать нужно крайне ограниченное количество информации.
  • Платформы CDC хороши, если у вас большое количество реплицируемых баз и/или есть необходимость сложных преобразований данных.
  • Разработка прикладной репликации оправдана только в случае разработки собственной платформы или фреймворка.
Подробнее..

Путеводитель по резервному копированию баз данных

25.08.2020 10:05:02 | Автор: admin
О, никакое убежище не выдержит попадания метеорита. Но ведь у вас, как и у каждого, есть резерв, так что можете не беспокоиться.

Станислав Лем, Звёздные дневники Ийона Тихого

Резервным копированием называется сохранение копии данных где-то вне основного места их хранения.



Главное назначение резервного копирования восстановление данных после их потери. В связи с этим нередко приходится слышать, что при наличии реплики базы данных с неё всегда можно восстановить данные, и резервное копирование не нужно. На самом деле резервное копирование позволяет решить как минимум три задачи, которые не могут быть решены при помощи реплики, да и реплику без резервной копии не инициализировать.

Во-первых, резервная копия позволяет восстановить данные после логической ошибки. Например, бухгалтер удалил группу проводок или администратор БД уничтожил табличное пространство. Обе операции абсолютно легитимны с точки зрения базы данных, и процесс репликации воспроизведёт их в базе-реплике.

Во-вторых, современные СУБД весьма надёжные программные комплексы, однако изредка всё же происходит повреждение внутренних структур базы данных, после которого доступ к данным пропадает. Что особенно обидно, такое нарушение происходит обычно при высокой нагрузке или при установке какого-нибудь обновления. Но как высокая нагрузка, так и регулярные обновления говорят о том, что база данных отнюдь не тестовая, и данные, хранящиеся в ней, ценны.

Наконец, третья задача, решение которой требует наличия резервной копии, это клонирование базы, например, для целей тестирования.

Резервное копирование баз данных так или иначе базируется на одном из двух принципов:

  • Выборка данных с последующим сохранением в произвольном формате;
  • Снимок состояния файлов БД и сохранение журналов.

Давайте рассмотрим эти принципы и реализующие их инструменты подробнее.

Выгрузка данных


В наборе утилит, прилагающихся к любой СУБД, обязательно есть инструменты для выгрузки и загрузки данных. Данные сохраняются либо в текстовом формате, либо в двоичном формате, специфичном для конкретной СУБД. В таблице ниже приведён список таких инструментов:

Двоичный формат Текстовый формат
Oracle DataPump Export/DataPump Import
Export/Import
SQL*Plus/SQL*Loader
PostgreSQL pg_dump, pg_dumpall/pg_restore pg_dump, pg_dumpall/psql
Microsoft SQL Server bcp bcp
DB2 unload/load unload/load
MySQL mysqldump, mysqlpump/mysql, mysqlimport
MongoDB mongodump/mongorestore mongoexport/mongoimport
Cassandra nodetool snapshot/sstableloader cqlsh

Текстовый формат хорош тем, что его можно редактировать или даже создавать внешними программами, а двоичный в свою очередь хорош тем, что позволяет быстрее выгружать и загружать данные за счёт экономии ресурсов на преобразовании форматов.

Несмотря на простоту и очевидность идеи выгрузки данных, для резервирования нагруженных промышленных баз такой метод применяют редко. Вот причины, по которым выгрузка не подходит для полноценного резервного копирования:

  • процесс выгрузки создаёт значительную нагрузку на систему-источник;
  • выгрузка занимает много времени к моменту окончания выгрузки она станет уже неактуальной;
  • сделать согласованную выгрузку всей базы данных при высокой нагрузке практически невозможно, поскольку СУБД вынуждена хранить снимок своего состояния на момент начала выгрузки. Чем больше транзакций совершено с момента начала выгрузки, тем больше объём снимка (неактуальных копий данных в PostgreSQL, пространства undo в Oracle, tempdb в Microsoft SQL Server ит.п.);
  • выгрузка сохраняет логическую структуру данных, но не сохраняет их физическую структуру параметры физического хранения таблиц, индексы идр.

Тем не менее, у выгрузки есть и достоинства:

  • высокая избирательность: можно выгрузить отдельные таблицы, отдельные поля и даже отдельные строки;
  • выгруженные данные можно загрузить в базу данных другой версии, а если выгрузка сделана в текстовом формате, то и в другую базу данных.

Таким образом, выгрузка используется в основном для таких задач как резервирование небольших таблиц (например, справочников) или распространение наборов данных с очередным релизом приложения.

Самым же распространённым методом резервного копирования баз данных является копирование файлов базы.

Холодное сохранение файлов БД


Очевидная идея остановить базу данных и скопировать все её файлы. Такая резервная копия называется холодной. Способ крайне надёжный и простой, но у него есть два очевидных недостатка:

  • из холодной резервной копии можно восстановить только то состояние базы данных, которое было в момент останова; транзакции, сделанные после рестарта базы, в холодную резервную копию не попадут;
  • далеко не у каждой базы данных есть технологическое окно, когда базу можно остановить.

Если же холодное резервное копирование вас устраивает, нужно помнить что

  • холодная копия иногда должна включать в себя и журналы. Методы определения журналов, которые должны попасть в холодную копию, индивидуальны для каждой СУБД. Например, в Oracle необходимо скопировать так называемые online redo, то есть фиксированное количество журнальных файлов в специальном каталоге, причём даже тогда, когда база остановлена корректно. В PostgreSQL нужно сохранить все журналы начиная с журнала, содержащего последнюю контрольную точку, информация о которой содержится в управляющем файле.
  • каталог базы данных может содержать достаточно большие файлы временных табличных пространств, которые не обязательно включать в резервную копию. Кстати, это замечание верно и для горячего резервного копирования.

Горячее сохранение файлов


Большинство резервных копий современных баз данных выполняется путём копирования файлов базы данных без остановки базы. Здесь видны несколько проблем:

  • В момент начала копирования содержимое базы данных может не совпадать с содержимым файлов, т.к. часть информации находится в кеше и ещё не записана на диск.
  • Во время копирования содержимое базы может меняться. Если используются изменяемые структуры данных, то меняется содержимое файлов, а при использовании неизменяемых структур меняется набор файлов: новые файлы появляются, а старые удаляются.
  • Поскольку запись данных в базу и чтение файлов БД никак не синхронизированы, программа резервного копирования может прочитать некорректную страницу, в которой половина будет от старой версии страницы, а другая половина от новой.

Для того, чтобы резервная копия получилась согласованной, у каждой СУБД существует команда, которая сообщает, что начат процесс резервного копирования. Синтаксически эта команда может выглядеть по-разному:

  • в Oracle это отдельная команда ALTER DATABASE/TABLESPACE BEGIN BACKUP;
  • в PostgreSQL функция pg_start_backup();
  • в Microsoft SQL Server и DB2 подготовка к резервному копированию выполняется неявно в процессе выполнения команды BACKUP DATABASE;
  • в MySQL Enterprise, Cassandra и MongoDB подготовка неявно выполняется внешней утилитой mysqlbackup, OpsCenter и Ops Manager соответственно.

Несмотря на синтаксические различия, процесс подготовки к резервному копированию выглядит одинаково.

Вот как выглядит подготовка к резервному копированию в СУБД с изменяемыми дисковыми структурами, т.е. во всех традиционных дисковых реляционных системах:

  1. Запоминается момент начала резервного копирования; резервная копия должна будет содержать журналы базы данных начиная с этого момента.
  2. Выполняется контрольная точка, то есть все изменения, которые произошли в страницах данных до запомненного момента, сбрасываются на диск. Это гарантирует, что журналы до момента начала резервного копирования при восстановлении не потребуются.
  3. Включается особый режим журналирования: если страница данных изменилась в первый раз после загрузки с диска, то вместо того, чтобы записывать в журнал изменения страницы, база запишет туда страницу целиком. При выполнении подготовительной процедуры все страницы вытесняются на диск, и поэтому при первом изменении блок всегда будет записан в журнал целиком. Но если в процессе резервного копирования страница снова будет вытеснена на диск, то следующее её изменение также приведёт к появлению в журнале полной копии страницы. Это гарантирует, что если вдруг при копировании файла с данными страница получится некорректной, применение журнала сделает его корректной вновь.
  4. Блокируется изменение заголовков файлов данных, то есть той его части, изменения которой не отражаются в журналах. Это гарантирует, что заголовок будет скопирован корректно, а потом к файлу данных корректно будут применены журналы.

После того, как все перечисленные выше процедуры выполнены, можно копировать файлы данных средствами операционной системы cp, rsync и другими. Включение режима резервного копирования снижает производительность базы данных: во-первых, увеличивается объём журналов, а во-вторых, если вдруг в режиме резервного копирования произойдёт сбой, восстановление будет более продолжительным, т.к. заголовки файлов данных не обновляются. Чем быстрее резервное копирование закончится, тем лучше для базы данных, поэтому здесь уместно применение таких средств как снимок (snapshot) файловой системы или разрыв зеркала (BCV) в дисковом массиве. Одни СУБД (Oracle, PostgreSQL) оставляют администратору возможность самостоятельно выбрать способ копирования, другие (Microsoft SQL Server) предоставляют интерфейс для интеграции собственных утилит резервного копирования с механизмами файловых систем или СХД.

По окончании резервного копирования нужно перевести базу данных обратно в обычное состояние. В Oracle это делается командой ALTER DATABASE/TABLESPACE END BACKUP, в PostgreSQL вызовом функции pg_stop_backup(), а в других базах внутренними подпрограммами соответствующих команд или внешних сервисов.

Вот как выглядит времення диаграмма процесса резервного копирования:



  • Подготовка к резервному копированию (begin backup) занимает время, иногда значительное. Даже если используются зеркальные тома или файловые системы с возможностью изготовления снимков, процесс резервного копирования не будет мгновенным.
  • Вместе с файлами данных необходимо сохранить журналы начиная с момента начала подготовки к резервному копированию и заканчивая моментом возврата базы в нормальное состояние.
  • Восстановиться из этой резервной копии можно на момент возврата базы в нормальное состояние. Восстановление на более ранний момент невозможно.

С базами данных, использующими неизменяемые структуры данных (снимки памяти, LSM-деревья) ситуация проще. Подготовка к резервному копированию состоит из следующих шагов:

  1. Данные из памяти сбрасываются на диск.
  2. Фиксируется список файлов, попадающих в резервную копию. До тех пор, пока процесс резервного копирования не закончится, базе запрещено удалять эти файлы, даже если они становятся не нужны.

По сигналу об окончании резервного копирования база с неизменяемыми структурами снова может удалять ненужные файлы.

Восстановление на точку


Резервная копия позволяет восстановить состояние базы данных на момент, когда завершилась команда возврата из режима резервного копирования. Однако авария, после которой потребуется восстановление, может произойти в любой момент. Задача восстановления состояния БД на произвольный момент называется восстановлением на точку (point-in-time recovery).

Чтобы обеспечить такую возможность, следует сохранять журналы БД начиная с момента окончания резервного копирования, а в процессе восстановления продолжить применять журналы к восстановленной копии. После того, как БД восстановлена из резервной копии на момент окончания копирования, состояние базы (файлов и кэшированных страниц) гарантированно корректно, поэтому особый режим журналирования не нужен. Применяя журналы до нужного момента, можно получить состояние базы данных на любую точку во времени.

Если скорость восстановления резервной копии ограничена лишь пропускной способностью диска, то скорость применения журналов обычно ограничена производительностью процессора. Если в основной базе данных изменения происходят параллельно, то при восстановлении все изменения выполняются последовательно в порядке чтения из журнала. Таким образом время восстановления линейно зависит от того, насколько далеко точка восстановления отстоит от точки окончания резервного копирования. Из-за этого приходится довольно часто делать полные резервные копии минимум раз в неделю для баз с небольшой транзакционной нагрузкой и до ежедневного копирования высоконагруженных баз.

Инкрементальное резервное копирование


Чтобы ускорить восстановление на точку, хотелось бы иметь возможность выполнять резервное копирование как можно чаще, но при этом не занимать лишнего места на дисках и не нагружать базу задачами резервного копирования.

Решение задачи инкрементальное резервное копирование, то есть копирование только тех страниц данных, которые изменились с момента предыдущего резервного копирования.
Инкрементальное резервное копирование имеет смысл только для СУБД, использующих изменяемые структуры данных.

Инкремент может отсчитываться как от полной резервной копии (кумулятивная копия), так и от любой предыдущей копии (дифференциальная копия).



К сожалению, единой терминологии не существует, и разные производители используют разные термины:

Дифференциальная Кумулятивная
Oracle Differential Cumulative
PostgresPro Incremental
Microsoft SQL Server Differential
IBM DB2 Delta Incremental

При наличии инкрементальных копий процесс восстановления на точку выглядит следующим образом:

  • восстанавливается последняя полная резервная копия, сделанная до момента восстановления;
  • поверх полной копии восстанавливаются инкрементальные копии;
  • накатываются журналы с точки начала резервного копирования до точки восстановления.

Наличие кумулятивной копии ускоряет процесс восстановления. Так, например, для восстановления состояния базы на точку между T3 и T4 необходимо восстановить две инкрементальных копии, а для восстановления на точку после T4 только одну.
Очевидно, что объём одной кумулятивной копии меньше, чем объём нескольких дифференциальных копий, потому что некоторые страницы изменились по несколько раз, и каждая инкрементальная копия содержит свою версию страницы.

Есть три способа создания инкрементальной копии:

  1. создание полной копии и вычисление разницы с предыдущей полной копией;
  2. разбор журналов, создание списка изменённых страниц и резервирование страниц, включённых в список;
  3. запрос изменённых страниц в базе данных.

Первый способ экономит дисковое пространство, но не решает задачу снижения нагрузки на базу данных. Более того, если у нас есть полная резервная копия, то превращать её в инкрементальную бессмысленно, т.к. восстановление полной копии быстрее, чем восстановление предыдущей полной копии и инкремента. Задачу экономии дискового пространства при таком подходе лучше переложить на специальные компоненты со встроенными механизмами дедупликации. Это могут быть как специальные СХД (EMC DataDomain, HPE StorageWorks VLS, вся линейка NetApp), так и программные продукты (ZFS, Veritas NetBackup PureFile, Windows Server Data Deduplication).

Второй и третий способ отличаются механизмом определения списка изменённых страниц. Разбор журналов более ресурсоёмкий, плюс для его реализации необходимо знать структуру журнальных файлов. Спросить у самой базы, какие именно страницы изменились, проще всего, но для этого ядро СУБД должно иметь функциональность отслеживания изменённых блоков (block change tracking).

Впервые функциональность инкрементального резервного копирования была создана в ПО Oracle Recovery Manager (RMAN), появившемся в релизе Oracle8i. Oracle сразу реализовал отслеживание изменённых блоков, поэтому необходимости в разборе журналов нет.

PostgreSQL не отслеживает изменённые блоки, поэтому утилита pg_probackup, разработанная российской компанией Postgres Professional, определяет изменённые страница путём анализа журнала. Однако компания поставляет и СУБД PostgresPro, которая включает расширение ptrack, отслеживающее изменение страниц. При использовании pg_probackup с СУБД PostgresPro утилита запрашивает изменённые страницы на у самой базы точно так же, как и RMAN.

Microsoft SQL Server так же, как и Oracle, отслеживает изменённые страницы, но команда BACKUP позволяет делать только полные и кумулятивные резервные копии.

В DB2 есть возможность отслеживания измененных страниц, но по умолчанию она выключена. После включения DB2 позволит делать полные, дифференциальные и кумулятивные резервные копии.

Важное отличие описанных в этом разделе средств (кроме pg_probackup) от файловых средств резервного копирования в том, что они запрашивают образы страниц у базы данных, а не читают данные с диска самостоятельно. Недостаток такого подхода небольшая дополнительная нагрузка на базу. Однако этот недостаток с лихвой компенсируется тем, что прочитанная страница всегда корректна, поэтому нет необходимости во включении на время резервного копирования особого режима журналирования.

Ещё раз обратите внимание, что наличие инкрементальных копий не отменяет требований к наличию журналов для восстановления на произвольную точку во времени. Поэтому в промышленных базах данных журналы постоянно переписываются на внешний носитель, а резервные копии, полные и/или инкрементальные, создаются по расписанию.

Наилучшей на сегодня реализацией идеи инкрементального резервного копирования является программно-аппаратный комплекс (в терминологии Oracle engineered system) Zero Data Loss Recovery Appliance специализированное решение Oracle для резервного копирования собственной БД. Комплекс представляет собой кластер серверов с большим объёмом дисков, на которые установлена модифицированная версия ПО Recovery Manager и может работать как с другими программно-аппаратными комплексами Oracle (Database Appliance, Exadata, SPARC Supercluster), так и с базами Oracle на традиционной инфраструктуре. В отличие от обычного RMAN, в ZDLRA реализована концепция вечного инкремента (incremental forever). Система единственный раз создаёт полную копию базы данных, а потом делает только инкрементальные копии. Дополнительные модули RMAN позволяют объединять копии, создавая новые полные копии из инкрементальных.

К чести российских разработчиков нужно заметить, что и pg_probackup умеет объединять инкрементальные копии.



В отличие от многих похожих вопросов, вопрос какой метод резервного копирования лучше имеет однозначный ответ лучше всего родная для используемой СУБД утилита, обеспечивающая возможность инкрементального копирования.

Для администратора БД гораздо более важными являются вопросы выбора стратегии резервного копирования и интеграция средств резервирования баз данных в корпоративную инфраструктуру. Но эти вопросы выходят за рамки данной статьи.
Подробнее..

Ведение разработки БД. Шаблоны созданияизменения объектов MSSQL

05.11.2020 04:10:16 | Автор: admin

При постоянной работе с MSSQL необходимо создавать различные объекты БД: таблицы, представления, триггеры и т.д.
В статье приведу шаблоны SQL запросов, которые помогут, и, возможно, стандартизируют подход создания кода на языке T-SQL.
Кроме этого, опишу о том, как я веду репозиторий БД в системе контроля версий.


Основные требования реализации SQL скриптов


1) скрипт должен выполняться многократно не выдавая ошибок
2) в скрипте должны быть предусмотрены операторы PRINT для удобства отладки
3) выполнение скриптов должно логироваться в один общий файл
4) скрипты должны выполняться через командную строку используя стандартный набор утилит (sqlcmd, bcp)
5) создание и изменение каждого объекта БД хранится отдельным SQL файлом
6) SQL скрипты (файлы) запускаются BAT файлом при каждом обновлении БД


Далее приводятся примеры SQL файлов и BAT файл для запуска этих SQL запросов.


Шаблоны T-SQL


Создание/изменение таблицы
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGO--проверяем на существование таблицы в БДif objectproperty (object_id ('dbo.TableName'), N'IsUserTable') IS NULLbegin    print N'Создание таблицы - dbo.TableName'    create table dbo.TableName    (        TableNameId uniqueidentifier default newid() not null,        FieldName1 uniqueidentifier not null,        FieldName2 varchar(20) not null,        CONSTRAINT PK_TableName PRIMARY KEY (TableNameId),        CONSTRAINT FK_TableName_FieldName1 FOREIGN KEY (FieldName1)                REFERENCES dbo.ReferenceTableName (RefFieldName)                ON UPDATE CASCADE,        CONSTRAINT UQ_TableName_FieldName1_FieldName2 UNIQUE (FieldName1, FieldName2)    );endGO-- Добавить полеif not exists (        select *        from INFORMATION_SCHEMA.COLUMNS        where TABLE_SCHEMA = 'dbo'          and TABLE_NAME = 'TableName'          and COLUMN_NAME = 'FieldName'        )begin    alter table dbo.TableName add FieldName varchar(500)    print N'Добавлено поле FieldName в таблице dbo.TableName'end-- Создать FOREIGN KEY, если его не существуетif not exists (select * from sys.foreign_keys where object_id = OBJECT_ID(N'dbo.FK_TableName_FieldName1') AND parent_object_id = OBJECT_ID(N'dbo.TableName'))ALTER TABLE dbo.TableName WITH CHECK ADD CONSTRAINT FK_TableName_FieldName1 FOREIGN KEY(FieldName1)REFERENCES dbo.ReferenceTableName (RefFieldName)GOIF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'dbo.FK_TableName_FieldName1') AND parent_object_id = OBJECT_ID(N'dbo.TableName'))ALTER TABLE dbo.TableName CHECK CONSTRAINT FK_TableName_FieldName1GO-- Создание уникального индекса, если он не существуетif not exists (select * from information_schema.key_column_usage where CONSTRAINT_NAME='UQ_TableName_FieldName1_FieldName2')begin    ALTER TABLE dbo.TableName ADD CONSTRAINT UQ_TableName_FieldName1_FieldName2 UNIQUE (FieldName1, FieldName2)end-- Создание DEFAULT ограничения, если он не существуетif not exists (select * from sysconstraints where id = OBJECT_ID('dbo.TableName') AND COL_NAME(id,colid) = 'FieldName2' AND OBJECTPROPERTY(constid, 'IsDefaultCnst') = 1)begin    ALTER TABLE dbo.TableName ADD CONSTRAINT DF_TableName_FieldName2 DEFAULT ('DefaultValue') FOR FieldName2enddeclare @SchemaName varchar(128) = 'dbo'declare @TableName varchar(128) = 'TableName'-- Создание описания таблицыIF NOT EXISTS (SELECT * FROM fn_listextendedproperty('MS_Description', 'SCHEMA', @SchemaName, 'TABLE', @TableName, default, default))    EXECUTE sp_addextendedproperty        N'MS_Description', N'Описание таблицы',        N'SCHEMA', @SchemaName,        N'TABLE', @TableName-- Создание описания поля, если его не существуетIF NOT EXISTS (SELECT * FROM fn_listextendedproperty ('MS_Description', 'schema', @SchemaName, 'table', @TableName, 'column', 'FieldName1'))    EXECUTE sp_addextendedproperty        N'MS_Description', N'Описание поля FieldName1',        N'SCHEMA', @SchemaName,        N'TABLE', @TableName,        N'COLUMN', N'FieldName1'GO

Создание/изменение представления
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGO--проверяем на существование представления в БДif objectproperty (object_id ('dbo.vw_ViewName'), N'IsView') is nullBEGIN    PRINT 'CREATE VIEW - '+db_name()+'.dbo.vw_ViewName'    EXECUTE('CREATE VIEW dbo.vw_ViewName AS SELECT 1/0 as ColumnName');ENDGOPRINT 'ALTER VIEW - '+db_name()+'.dbo.vw_ViewName'GOalter view dbo.vw_ViewNameasselect    FieldName1, FieldName2from dbo.TableName

Создание/изменение процедуры
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOif objectproperty (object_id ('dbo.usp_StoredProcedureName'), N'IsProcedure') is nullBEGIN-- If procedure exists, we exclude script execution.    PRINT 'CREATE PROCEDURE - '+db_name()+'.dbo.usp_StoredProcedureName'    EXECUTE('CREATE PROCEDURE dbo.usp_StoredProcedureName as select 1/0');ENDELSE    PRINT 'ALTER PROCEDURE - '+db_name()+'.dbo.usp_StoredProcedureName'GOalter procedure dbo.usp_StoredProcedureNameas select null

Создание/изменение триггера
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGO--проверяем на существование триггера в БДif objectproperty (object_id ('dbo.TriggerName'), N'IsTrigger') is nullBEGIN    PRINT 'CREATE TRIGGER - '+db_name()+'.dbo.TriggerName'    EXECUTE('CREATE TRIGGER dbo.TriggerName on dbo.TableName for insert as print 0');ENDGOPRINT 'ALTER TRIGGER - '+db_name()+'.dbo.TriggerName'GOalter trigger dbo.TriggerName on dbo.TableNamefor insert, update, deleteasif (exists(SELECT 1 from inserted) and exists (SELECT 1 from deleted)) -- определение UPDATEbegin  if update(FieldName)    begin      update dbo.TableName      set FieldName = FieldValue      where SearchFieldName in      (        select FieldValue from inserted        union        select FieldValue from deleted      )    endendelseif (exists(SELECT 1 from inserted) and not exists (SELECT 1 from deleted)) -- определение INSERTbegin  /* Ваш код обработки */endelseif (not exists(SELECT 1 from inserted) and exists (SELECT 1 from deleted)) -- определение DELETEbegin  /* Ваш код обработки */end;

Создание/изменение табличного типа
--USE [DatabaseName]--GO/*DROP PROCEDURE dbo.ProcedureNameDROP TYPE dbo.CustomUserType*/IF TYPE_ID(N'dbo.CustomUserType') IS NULL BEGIN    PRINT N'Создание типа - dbo.CustomUserType'    --Если тип не существует, создаем его.    CREATE TYPE dbo.CustomUserType AS TABLE    (        FieldId TYPE,        PRIMARY KEY (FieldId)    )ENDGO

Создание/изменение скалярной функции
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOif objectproperty (object_id ('dbo.ufn_FunctionName'), N'IsScalarFunction') is nullBEGIN-- If procedure exists, we exclude script execution.    PRINT 'CREATE FUNCTION - '+db_name()+'.dbo.ufn_FunctionName'    EXECUTE('CREATE FUNCTION dbo.ufn_FunctionName() returns int begin return 0 end');ENDELSE    PRINT 'ALTER FUNCTION - '+db_name()+'.dbo.ufn_FunctionName'GOalter function dbo.ufn_FunctionName ()returns varchar(100)begin    return null;endGO

Создание/изменение табличной функции
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOif objectproperty (object_id ('dbo.ufn_TableFunctionName'), N'IsTableFunction') is nullBEGIN-- If procedure exists, we exclude script execution.    PRINT 'CREATE FUNCTION - '+db_name()+'.dbo.ufn_TableFunctionName'    EXECUTE('CREATE FUNCTION dbo.ufn_TableFunctionName() returns table as return (select null as c)');ENDELSE    PRINT 'ALTER FUNCTION - '+db_name()+'.dbo.ufn_TableFunctionName'GOALTER function dbo.ufn_TableFunctionName()returns table    as return(    select 1 as Field1, 2 as Field2)

Удаление/создание XSD-схем (XmlSchemaCollection)
--USE [DatabaseName]--GOSET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOif exists (select * from sys.xml_schema_collections where [schema_id] = schema_id('dbo') and [name] = 'SdmxXsd')begin    drop XML SCHEMA COLLECTION dbo.SdmxXsd    print 'Удалена XSD схема - dbo.SdmxXsd'end--Создать XSD схему, если она отсутствует в БДif not exists (select * from sys.xml_schema_collections where [schema_id] = schema_id('dbo') and [name] = 'SdmxXsd')begin    CREATE XML SCHEMA COLLECTION dbo.SdmxXsd asN'<?xml version="1.0" encoding="utf-16"?><xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://personeltest.ru/away/www.w3.org/2001/XMLSchema">  <xs:element name="Ref">    <xs:complexType>      <xs:attribute name="agencyID" type="xs:string" use="required" />      <xs:attribute name="id" type="xs:unsignedInt" use="required" />      <xs:attribute name="version" type="xs:decimal" use="required" />    </xs:complexType>  </xs:element></xs:schema>'    ALTER XML SCHEMA COLLECTION dbo.SdmxXsd addN'<?xml version="1.0" encoding="utf-16"?><xs:schema xmlns:tns="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/common" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/common" xmlns:xs="http://personeltest.ru/away/www.w3.org/2001/XMLSchema">  <xs:import />  <xs:element name="Structure">    <xs:complexType>      <xs:sequence>        <xs:element ref="Ref" />      </xs:sequence>    </xs:complexType>  </xs:element></xs:schema>'    ALTER XML SCHEMA COLLECTION dbo.SdmxXsd addN'<?xml version="1.0" encoding="utf-16"?><xs:schema xmlns:tns="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/data/generic" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/data/generic" xmlns:xs="http://personeltest.ru/away/www.w3.org/2001/XMLSchema">  <xs:element name="Series">    <xs:complexType>      <xs:sequence>        <xs:element name="SeriesKey">          <xs:complexType>            <xs:sequence>              <xs:element maxOccurs="unbounded" name="Value">                <xs:complexType>                  <xs:attribute name="id" type="xs:string" use="required" />                  <xs:attribute name="value" type="xs:string" use="required" />                </xs:complexType>              </xs:element>            </xs:sequence>          </xs:complexType>        </xs:element>        <xs:element name="Attributes">          <xs:complexType>            <xs:sequence>              <xs:element maxOccurs="unbounded" name="Value">                <xs:complexType>                  <xs:attribute name="id" type="xs:string" use="required" />                  <xs:attribute name="value" type="xs:string" use="required" />                </xs:complexType>              </xs:element>            </xs:sequence>          </xs:complexType>        </xs:element>        <xs:element name="Obs">          <xs:complexType>            <xs:sequence>              <xs:element name="ObsDimension">                <xs:complexType>                  <xs:attribute name="value" type="xs:string" use="required" />                </xs:complexType>              </xs:element>              <xs:element name="ObsValue">                <xs:complexType>                  <xs:attribute name="value" type="xs:decimal" use="required" />                </xs:complexType>              </xs:element>            </xs:sequence>          </xs:complexType>        </xs:element>      </xs:sequence>    </xs:complexType>  </xs:element></xs:schema>'    ALTER XML SCHEMA COLLECTION dbo.SdmxXsd addN'<?xml version="1.0" encoding="utf-16"?><xs:schema xmlns:message="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/message" xmlns:common="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/common" xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance" xmlns:generic="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/data/generic" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/message" xmlns:xs="http://personeltest.ru/away/www.w3.org/2001/XMLSchema">  <xs:import namespace="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/common" />  <xs:import namespace="http://personeltest.ru/away/www.sdmx.org/resources/sdmxml/schemas/v2_1/data/generic" />  <xs:element name="GenericData">    <xs:complexType>      <xs:sequence>        <xs:element name="Header">          <xs:complexType>            <xs:sequence>              <xs:element name="ID" type="xs:string" />              <xs:element name="Test" type="xs:boolean" />              <xs:element name="Truncated" type="xs:boolean" />              <xs:element name="Prepared" type="xs:dateTime" />              <xs:element name="Sender">                <xs:complexType>                  <xs:attribute name="id" type="xs:string" use="required" />                </xs:complexType>              </xs:element>              <xs:element name="Structure">                <xs:complexType>                  <xs:sequence>                    <xs:element ref="common:Structure" />                  </xs:sequence>                  <xs:attribute name="structureID" type="xs:string" use="required" />                  <xs:attribute name="dimensionAtObservation" type="xs:string" use="required" />                </xs:complexType>              </xs:element>            </xs:sequence>          </xs:complexType>        </xs:element>        <xs:element name="DataSet">          <xs:complexType>            <xs:sequence>              <xs:element maxOccurs="unbounded" ref="generic:Series" />            </xs:sequence>            <xs:attribute name="structureRef" type="xs:string" use="required" />          </xs:complexType>        </xs:element>      </xs:sequence>    </xs:complexType>  </xs:element></xs:schema>'    print 'Создана XSD схема - dbo.SdmxXsd'endgo

Ведение репозитория


1) Репозиторий состоит из папок, разделенных по типу объектов (например: Tables, Views, Triggers и т.д.)
2) Каждый объект БД хранится в отдельном SQL файле для удобства просмотра истории изменений в системе контроля версий.
3) Если изменения касаются данных, то такие изменения ведутся в отдельном файле "CommonChanges (version 000).sql", который создается на каждое обновление БД
4) Для автоматизации применения изменений на нескольких серверах запросы выполняются с помощью BAT файла.


Пример BAT файла
:: Описание: Обновление БД Microsoft SQL Server:::: ВНИМАНИЕ: В случае если в разделе "1. Описание параметров" содержатся русские символы, то необходимо раскомментировать следующую строку::chcp 1251:::: 1. ОПИСАНИЕ ПАРАМЕТРОВ:::: Получить общие настройки (соединение с БД и др.)call Settings.bat:::: Лог-файл выполнения запроса (перезаписывается с каждым вызовом sqlcmd)set LogFileName="%LogDir%\update_0001_%NowDateTime%_tmp.log":::: Полный лог-файл обновления БДset FullLogFileName="%LogDir%\update_0001_%NowDateTime%.log":::: Лог файла командного выполненияset LogCmdFileName="%LogDir%\update_0001_%NowDateTime%_cmd.log":::: 2. ВПОЛНЕНИЕ SQL-ЗАПРОСОВ::set SqlFileName="..\CommonChanges (version 001).sql"@If Exist %SqlFileName% (%sqlcmd% -S %ServerName% -U %UserName% -P %Password% -d %DatabaseName% -i %SqlFileName% -o %LogFileName% -btype %LogFileName% >> %FullLogFileName%) ElSE (echo Не найден файл скрипта %SqlFileName% >> %LogCmdFileName%)::set SqlFileName="..\AlterObjects\StoredProcedures\dbo.SP.sql"@If Exist %SqlFileName% (%sqlcmd% -S %ServerName% -U %UserName% -P %Password% -d %DatabaseName% -i %SqlFileName% -o %LogFileName% -btype %LogFileName% >> %FullLogFileName%) ElSE (echo Не найден файл скрипта %SqlFileName% >> %LogCmdFileName%)::set SqlFileName="..\UpdateVersion.sql"@If Exist %SqlFileName% (%sqlcmd% -S %ServerName% -U %UserName% -P %Password% -d %DatabaseName% -i %SqlFileName% -o %LogFileName% -btype %LogFileName% >> %FullLogFileName%) ElSE (echo Не найден файл скрипта %SqlFileName% >> %LogCmdFileName%):::: Вывести содержимое лога файла командного выполнения в общий лог, если он был сформирован@If Exist %LogCmdFileName% (echo ----Внимание, не найдены следующие файлы ожидающие выполнения:---- >> %FullLogFileName%type %LogCmdFileName% >> %FullLogFileName%:: Удалить лог файла командного выполненияdel %LogCmdFileName%):: Удалить сокращенный лог (оставшийся при последнем вызове sqlcmd)del %LogFileName%:::: Вывести содержимое файла-лога с результатом в командную строкуtype %FullLogFileName%pause

Файл Settings.bat
:: Настройки соединения с БД:::: ВНИМАНИЕ: В случае если в разделе "1. Описание параметров" содержатся русские символы, то необходимо раскомментировать следующую строку::chcp 1251:::: 1. ОПИСАНИЕ ПАРАМЕТРОВ:::: Путь к файлу sqlcmd.exe на локальном дискеset sqlcmd="sqlcmd.exe":::: Путь к файлу bcp.exe на локальном дискеset bcp="bcp.exe":::: Имя SQL-сервера (именованный экземпляр)set ServerName="(local)\InstanceName":::: Имя пользователяset UserName="sa":::: Парольset Password="my_password":::: Имя БДset DatabaseName=my_db:::: Каталог логовset LogDir=.\log:::: Использовать на русскоязычной версии Windows. Текущее время в формате YYYYMMDD_HHMMSS (рекомендуется использовать в имени файла лога)set NowDateTime=%date:~6%%date:~3,2%%date:~0,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%:: Использовать на английской версии Windows. Текущее время в формате YYYYMMDD_HHMMSS (рекомендуется использовать в имени файла лога)::set NowDateTime=%date:~10,4%%date:~4,2%%date:~7,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%:::: 2. СОЗДАНИЕ КАТАЛОГА ДЛЯ ХРАНЕНИЯ ЛОГОВmd %LogDir%

Достоинства


1) Удобно отслеживать историю изменения каждого объекта БД
2) Не требуется разрабатывать собственные приложения для выполнения SQL файлов
3) Работает на всех версиях MSSQL и Windows
4) Настройки соединения с БД, имена БД и др. переменные хранятся в одном файле "Settings.bat", которые легко изменить


Недостатки


1) Не предусмотрен общий откат изменений, если какой-то скрипт выполнится с ошибкой
2) Если репозиторий состоит из большого количества объектов БД, то чтобы не передавать заказчику весь репозиторий, нужно копировать файлы (входящие в обновление) в отдельный каталог


Выгрузка структуры БД


В качестве автоматизации выгрузки структуры БД, в формате один объект = один файл, на данный момент использую SSMS, но в скором времени подобный функционал будет в моей программе ImportExportDataSql, которую рекомендую всем разработчикам БД.


Немного о ImportExportDataSql


Приложение ImportExportDataSql бесплатное, без рекламы, оповещает о новых версиях, наличие командной строки и Вы можете скачать его и использовать в своих проектах.


С помощью ImportExportDataSql Вы сможете:


  • быстро загружать CSV файлы большого объема (более 1Гб) в SQL Server
  • загружать Excel файлы и CSV с возможностью настройки полей, а также с ограничением количества обрабатываемых строк (удобно при отладке)
  • выгружать выборочные данные из БД, в SQL формате и затем выполнять этот скрипт на другой БД (т.е. использовать как средство синхронизации данных)
  • копировать джобы с одной машины на другую

    image
  • выгружать структуру БД.

Главной особенностью ImportExportDataSql, является то, что можно объединять несколько SELECT запросов, выгружая результат в виде SQL в один файл.


Добавляйтесь в группу VK, пишите свои пожелания, буду рад доработать приложение под Ваши нужды.


Заключение


Рад был поделиться результатом своего опыта. Данный способ ведения репозитория меня вполне устраивает. Надеюсь, эта статья поможет нам навести порядок в базах данных.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru