Генератор / подсказка к 4 задачке

Будем теперь возиться с генератором иерархической структур. У меня в качестве такой структуры выступает некоторая локация.

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

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

И так добавляем новый таб под генератор локаций

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

Сначала настрою трекбары. Создаю в форме метод

private void UpdateLabelForTrackbar(TrackBar tb, Label lbl)
{
    if (tb.Minimum != tb.Value)
    {
        lbl.Text = $"от {tb.Minimum} до {tb.Value}";
    } else
    {
        lbl.Text = tb.Value.ToString();
    }
}

который заполняет лейбл по значению на трекбаре.

Далее создаю метод, который должен привязать все трекбары к лейблам, вот так:

private void BindTrackBars()
{
    // тут я добавляю реакцию на изменение значений
    tbApartaments.ValueChanged += (e, o) => UpdateLabelForTrackbar(tbApartaments, lblAparataments);
    tbFloors.ValueChanged += (e, o) => UpdateLabelForTrackbar(tbFloors, lblFloors);
    tbRooms.ValueChanged += (e, o) => UpdateLabelForTrackbar(tbRooms, lblRooms);
    
    // а тут один раз вызываю методы чтобы сразу обновить тексты на лейблах
    // лейблы использую которые зелененьким выделил
    UpdateLabelForTrackbar(tbApartaments, lblAparataments);
    UpdateLabelForTrackbar(tbFloors, lblFloors);
    UpdateLabelForTrackbar(tbRooms, lblRooms);
}

ну и вызываю этот метод в конструкторе формы

public Form1()
{
    // ...

    BindTrackBars();
}

проверяю:

в принципе пойдет =)

Пробуем сгенерить локацию

Создаем метод для генерации здания

// добавляю себе для полного счастья некоторый набор типов зданий
readonly string[] BUILDING_TYPES = new []{ "Дом", "Усадьба", "Пансионат", "Небоскреб", "Коттедж" };

// метод для генерации здания, принимает на вход конекшен
private void GenerateBuilding(SqlConnection connection)
{
    // генерим имя
    var maleAdjectives = this.adjectives.Where(x => x.gender == "муж").ToList();
    var adjective = maleAdjectives.ElementAt(random.Next(0, maleAdjectives.Count()));
    var buildingType = BUILDING_TYPES.ElementAt(random.Next(0, BUILDING_TYPES.Count()));
    var buildingName = $"{adjective.word.Capitalize()} {buildingType}";
    
    // ну и вызываем уже привычный нам insert
    SqlCommand command = new($@"INSERT INTO Locations(title) VALUES('{buildingName}')", connection);
    command.ExecuteNonQuery();
}

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

private void btnGenerateLocations_Click(object sender, EventArgs e)
{
    using (var connection = GetConnection())
    {
        connection.Open();

        GenerateBuilding(connection);
    }
}

запускаем:

вроде работает.

Теперь мне надо чтобы в здании сгенерировалось некоторое количество этажей в соответствии с трекбаром

private void GenerateBuilding(SqlConnection connection)
{
    // ...
    command.ExecuteNonQuery();
    
    // выбираем случайное количество этажей
    var floorsCount = random.Next(tbFloors.Minimum, tbFloors.Value + 1);
    // ну и создаем их последовательно
    for(var floor = 1; floor <= floorsCount; ++floor)
    {
        var floorName = $"{floor} этаж";
        command = new($@"INSERT INTO Locations(title) VALUES('{floorName}')", connection);
        command.ExecuteNonQuery();
    }
}

проверяем:

ну что-то добавилось. С самим зданием вроде ок. Этаж тоже вроде как-то добавился, но по нему непонятно к какому зданию он принадлежит. Может он к “Плечному коттеджу” относится.

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

Но есть одна проблема. Надо где-то взять идентификатор здания. А помните мы делали запрос INSERT и добавляли к нему OUTPUT. Мы можем и тут сделать так же

INSERT Locations(title)
OUTPUT inserted.id
VALUES ('...')

правим метод

private void GenerateBuilding(SqlConnection connection)
{
    
    var maleAdjectives = this.adjectives.Where(x => x.gender == "муж").ToList();
    var adjective = maleAdjectives.ElementAt(random.Next(0, maleAdjectives.Count()));
    var buildingType = BUILDING_TYPES.ElementAt(random.Next(0, BUILDING_TYPES.Count()));
    var buildingName = $"{adjective.word.Capitalize()} {buildingType}";

    // добавляем OUTPUT inserted.id
    SqlCommand command = new($@"INSERT INTO Locations(title) OUTPUT inserted.id VALUES('{buildingName}')", connection);
    // тут заменям command.ExecuteNonQuery(); 
    int buildingId = (int)command.ExecuteScalar(); // ExecuteScalar выполняет запрос и возвращает то что идет в OUTPUT

    var floorsCount = random.Next(tbFloors.Minimum, tbFloors.Value + 1);
    for(var floor = 1; floor <= floorsCount; ++floor)
    {
        var floorName = $"{floor} этаж";
        // тут добавляем buildingId
        command = new($@"INSERT INTO Locations(title) VALUES('{floorName}', {buildingId})", connection);
        command.ExecuteNonQuery();
    }
}

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

то есть если кто-то решит запихать в название здания или еще чего запрос к БД это может привести к плачевным последствиям.

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

Делается это так:

private void GenerateBuilding(SqlConnection connection)
{
    
    var maleAdjectives = this.adjectives.Where(x => x.gender == "муж").ToList();
    var adjective = maleAdjectives.ElementAt(random.Next(0, maleAdjectives.Count()));
    var buildingType = BUILDING_TYPES.ElementAt(random.Next(0, BUILDING_TYPES.Count()));
    var buildingName = $"{adjective.word.Capitalize()} {buildingType}";
    
    // в запросе вместо '{buildingName}' пишем например @name, это будет плейсхолдер для параметра
    SqlCommand command = new($@"INSERT INTO Locations(title) OUTPUT inserted.id VALUES(@name)", connection);
    command.Parameters.Add("@name", SqlDbType.Text); // теперь добавляем сам параметр
    command.Parameters["@name"].Value = buildingName; // и передаем в него значение
    

    int buildingId = (int)command.ExecuteScalar();

    var floorsCount = random.Next(tbFloors.Minimum, tbFloors.Value + 1);
    for(var floor = 1; floor <= floorsCount; ++floor)
    {
        var floorName = $"{floor} этаж";
        
        // тут делаем по аналогии, имена параметров в принципе можно выбирать какие хотите
        // желательно чтобы они все-таки отражали суть передаваемых значений
        // я вот использовал прямо как имена переменных
        // вообще один из плюсов такой передачи параметров, помимо экранирования кода
        // является то что не надо больше думать надо оборачивать параметр в кавычки или нет
        // SqlCommand сам определит это по типу
        command = new($@"INSERT INTO Locations(title, parent_location_id) VALUES(@floorName, @buildingId)", connection);
        command.Parameters.Add("@floorName", SqlDbType.Text);
        command.Parameters["@floorName"].Value = floorName;
        command.Parameters.Add("@buildingId", SqlDbType.Int);
        command.Parameters["@buildingId"].Value = buildingId;

        command.ExecuteNonQuery();
    }
}

ну что ж запускам:

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

сработала как надо =)

Теперь надо каждому этажу добавить сколько-нибудь квартир. Тут надо делать вложенный цикл делать, но в принципе устройство тоже самое:

private void GenerateBuilding(SqlConnection connection)
{
    // ...

    var floorsCount = random.Next(tbFloors.Minimum, tbFloors.Value + 1);
    // у меня будет сквозная нумерация квартир, поэтому добавляю поле
    var nextAppartamentNumber = 1;
    for (var floor = 1; floor <= floorsCount; ++floor)
    {
        var floorName = $"{floor} этаж";

        // добавляем OUTPUT inserted.id
        command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@floorName, @buildingId)", connection);
        command.Parameters.Add("@floorName", SqlDbType.Text);
        command.Parameters["@floorName"].Value = floorName;
        command.Parameters.Add("@buildingId", SqlDbType.Int);
        command.Parameters["@buildingId"].Value = buildingId;

        // заменяем command.ExecuteNonQuery();
        int floorId = (int)command.ExecuteScalar();

        // ну и тут все по аналогии
        // сначала рандомно бросаем количество квартир
        var appartamentsCount = random.Next(tbApartaments.Minimum, tbApartaments.Value + 1);
        for (var a = 1; a <= appartamentsCount; ++a)
        {
            // формируем имя квартиры
            var appartamentName = $"{nextAppartamentNumber} квартира";
            // увеличиваем номер квартиры в сквозной нумерации
            nextAppartamentNumber++;

            // ну и формируем запрос, передавая в качестве parent уже номер этажа
            command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@appartamentName, @floorId)", connection);
            command.Parameters.Add("@appartamentName", SqlDbType.Text);
            command.Parameters["@appartamentName"].Value = appartamentName;
            command.Parameters.Add("@floorId", SqlDbType.Int);
            command.Parameters["@floorId"].Value = floorId;
            command.ExecuteNonQuery();
        }
    }
}

запускаем:

Интересно что значит тамбурмажорский…

В общем теперь у нас тут двойная вложенность уже:

ну я думаю дальше понятно, что делать. Добавлять еще один вложенный цикл. И т. д.

У меня получился такой код:

private void GenerateBuilding(SqlConnection connection)
{
    var maleAdjectives = this.adjectives.Where(x => x.gender == "муж").ToList();
    var adjective = maleAdjectives.ElementAt(random.Next(0, maleAdjectives.Count()));
    var buildingType = BUILDING_TYPES.ElementAt(random.Next(0, BUILDING_TYPES.Count()));
    var buildingName = $"{adjective.word.Capitalize()} {buildingType}";

    SqlCommand command = new($@"INSERT INTO Locations(title) OUTPUT inserted.id VALUES(@name)", connection);
    command.Parameters.Add("@name", SqlDbType.Text);
    command.Parameters["@name"].Value = buildingName;

    int buildingId = (int)command.ExecuteScalar();

    var floorsCount = random.Next(tbFloors.Minimum, tbFloors.Value + 1);
    var nextAppartamentNumber = 1;
    for (var floor = 1; floor <= floorsCount; ++floor)
    {
        var floorName = $"{floor} этаж";

        command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@floorName, @buildingId)", connection);
        command.Parameters.Add("@floorName", SqlDbType.Text);
        command.Parameters["@floorName"].Value = floorName;
        command.Parameters.Add("@buildingId", SqlDbType.Int);
        command.Parameters["@buildingId"].Value = buildingId;

        int floorId = (int)command.ExecuteScalar();

        var appartamentsCount = random.Next(tbApartaments.Minimum, tbApartaments.Value + 1);
        for (var a = 1; a <= appartamentsCount; ++a)
        {
            var appartamentName = $"{nextAppartamentNumber} квартира";
            nextAppartamentNumber++;

            command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@appartamentName, @floorId)", connection);
            command.Parameters.Add("@appartamentName", SqlDbType.Text);
            command.Parameters["@appartamentName"].Value = appartamentName;
            command.Parameters.Add("@floorId", SqlDbType.Int);
            command.Parameters["@floorId"].Value = floorId;

            int appartamentId = (int)command.ExecuteScalar();

            var roomsCount = random.Next(tbRooms.Minimum, tbRooms.Value + 1);
            for (var r = 1; r <= roomsCount; ++r)
            {
                var roomName = $"{r} комната";
                command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@roomName, @appartamentId)", connection);
                command.Parameters.Add("@roomName", SqlDbType.Text);
                command.Parameters["@roomName"].Value = roomName;
                command.Parameters.Add("@appartamentId", SqlDbType.Int);
                command.Parameters["@appartamentId"].Value = appartamentId;
                command.ExecuteNonQuery();
            }

            if (chkHasBaclocny.Checked)
            {
                var roomName = $"Балкон";
                command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@roomName, @appartamentId)", connection);
                command.Parameters.Add("@roomName", SqlDbType.Text);
                command.Parameters["@roomName"].Value = roomName;
                command.Parameters.Add("@appartamentId", SqlDbType.Int);
                command.Parameters["@appartamentId"].Value = appartamentId;
                command.ExecuteNonQuery();
            }

            if (chkHasBathroom.Checked)
            {
                var roomName = $"Ванная";
                command = new($@"INSERT INTO Locations(title, parent_location_id) OUTPUT inserted.id VALUES(@roomName, @appartamentId)", connection);
                command.Parameters.Add("@roomName", SqlDbType.Text);
                command.Parameters["@roomName"].Value = roomName;
                command.Parameters.Add("@appartamentId", SqlDbType.Int);
                command.Parameters["@appartamentId"].Value = appartamentId;
                command.ExecuteNonQuery();
            }
        }
    }
}

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

и в реакции на кнопку пропишу:

private void btnGenerateLocations_Click(object sender, EventArgs e)
{
    using (var connection = GetConnection())
    {
        connection.Open();

        pbLocationsGenerator.Value = 0;
        pbLocationsGenerator.Maximum = (int)udLocationsCount.Value;

        for (var i = 0;i<udLocationsCount.Value; ++i)
        {
            GenerateBuilding(connection);
            pbLocationsGenerator.Value++;
        }
    }
}

попробуем запустить, например, на 1000 элементов,

Думаю, пойдет =)

4

Реализовать генерацию данных для иерархической таблицы

  • Обеспечить вложеность не менее чем в три уровня