Значение по умолчанию для обязательных полей в миграции Entity Framework?


92

Я добавил [Required]аннотацию данных к одной из моих моделей в приложении ASP.NET MVC . После создания миграции выполнение Update-Databaseкоманды приводит к следующей ошибке:

Невозможно вставить значение NULL в столбец «Директор», таблица «MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies»; столбец не допускает значений NULL. ОБНОВЛЕНИЕ не удается. Заявление было прекращено.

Это связано с тем, что некоторые записи содержат NULL в своих Directorстолбцах. Как я могу автоматически изменить эти значения на какой-нибудь директор по умолчанию (скажем, «Джон Доу»)?

Вот моя модель:

  public class Movie
    {
        public int ID { get; set; }
        [Required]
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Required]
        public string Genre { get; set; }

        [Range(1,100)]
        [DataType(DataType.Currency)]
        public decimal Price { get; set; }

        [StringLength(5)]
        public string Rating { get; set; }

        [Required]     /// <--- NEW
        public string Director { get; set; }
    }

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

public partial class AddDataAnnotationsMig : DbMigration
{
    public override void Up()
    {
        AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false));
        AlterColumn("dbo.Movies", "Genre", c => c.String(nullable: false));
        AlterColumn("dbo.Movies", "Rating", c => c.String(maxLength: 5));
        AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false));
    }

    public override void Down()
    {
        AlterColumn("dbo.Movies", "Director", c => c.String());
        AlterColumn("dbo.Movies", "Rating", c => c.String());
        AlterColumn("dbo.Movies", "Genre", c => c.String());
        AlterColumn("dbo.Movies", "Title", c => c.String());
    }
}

Ответы:


74

Если я правильно помню, должно работать что-то вроде этого:

AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, defaultValueSql: "'John Doe'"));

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


9
Я тоже так думал, но для существующих записей это не работает. Так что я все равно получаю ошибку.
Андрей Дроздюк

@drozzy Возможно, это ошибка, как здесь: EF 4.3.1 Исключение миграции - AlterColumn defaultValueSql создает одно и то же имя ограничения по умолчанию для разных таблиц. Вы можете обновлять строки с IS NULLпроверкой по вашему запросу.
webdeveloper

Интересно, но я не уверен, что понимаю, о чем они говорят. Однако, если это ошибка, тогда да, это имеет смысл.
Андрей Дроздюк

6
Думаю, должно быть: "'John Doe'"- нужно использовать кавычки SQL.
Шон

1
@webdeveloper, не думаю, что это ошибка, зачем AlterColumnобновлять текущие значения? Это команда DDL (не DML).
Антон

110

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

public override void Up()
{
    Sql("UPDATE [dbo].[Movies] SET Title = 'No Title' WHERE Title IS NULL");
    AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false,defaultValue:"MyTitle"));
}

Это связано с тем, что AlterColumnпроизводит DDL для установки по умолчанию для столбца определенного значения в спецификации таблицы. DDL не влияет на существующие строки в базе данных.

Фактически вы вносите два изменения одновременно (устанавливаете значение по умолчанию и делаете столбец NOT NULL), и каждое из них действует индивидуально, но, поскольку вы вносите два изменения одновременно, вы можете ожидать, что система разумно 'реализовать свое намерение и установить для всех NULLзначений значения по умолчанию, но это не то, что ожидается постоянно.

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

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


17
Для людей, которые нашли этот ответ через Google: я только что попробовал это в EF6, и оператор обновления, похоже, не нужен (больше). Думаю, они все-таки посчитали это ошибкой.
EPLKleijntjens 03

3
Я тоже могу поручиться за это. Если вам нужно значение по умолчанию даже для поля, допускающего значение NULL, просто сначала измените его на значение, не допускающее значения NULL, со значением по умолчанию, а затем снова измените его на значение NULL. Очень удобно, когда вы добавили в дочерний класс поле, не допускающее значения NULL :)
Воутер Шут

1
Пятно на объяснении. AlterColumn () просто изменяет определение столбца. Это никак не влияет на существующие рекорды
Кораем

10
public partial class AddDataAnnotationsMig : DbMigration
{
    public override void Up()
    {
        AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false,defaultValue:"MyTitle"));
        AlterColumn("dbo.Movies", "Genre", c => c.String(nullable: false,defaultValue:"Genre"));
        AlterColumn("dbo.Movies", "Rating", c => c.String(maxLength: 5));
        AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false,defaultValue:"Director"));

    }

    public override void Down()
    {       
        AlterColumn("dbo.Movies", "Director", c => c.String());
        AlterColumn("dbo.Movies", "Rating", c => c.String());
        AlterColumn("dbo.Movies", "Genre", c => c.String());
        AlterColumn("dbo.Movies", "Title", c => c.String());       
    }
}

2
Эм ... спасибо, но чем это отличается от ответа @webdeveloper?
Андрей Дроздюк

1
он не сообщает вам, где вы должны добавить параметр значения по умолчанию
Pushpendra

1
@Pushpendra, забавно, как разработчики часто забывают, что когда-то они мало что знали. Мне нравятся подробные ответы, удовлетворяющие все уровни. Отличная работа!
полезноBee

5

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

defaultValueSql: "'NY'"

Я получил сообщение об ошибке, когда предоставленное значение было "NY"тогда, я понял, что они ожидают значение SQL, как "GETDATE()"я пытался, "'NY'"и это помогло

вся строка выглядит так

AddColumn("TABLE_NAME", "State", c => c.String(maxLength: 2, nullable: false, defaultValueSql: "'NY'"));

Благодаря этому ответу я на правильном пути


2

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

Например:

public class Thing {
    public bool IsBigThing { get; set; } = false;
}

2
Это хороший ответ (помог мне), но он не добавляет значение по умолчанию в базу данных, а устанавливает значение в коде.
chris31389

верно, он не добавил значение по умолчанию в базу данных после изменений миграции
Четан Чаудхари

2

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

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

  1. Измените определение столбца, включив в него оператор defaultValue или defaultSql:
    AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));

  2. Вставьте оператор SQL, чтобы предварительно заполнить существующие столбцы перед AlterColumn:
    Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");

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

ПРИМЕЧАНИЕ. EF не делает это автоматически, потому что реализация значения по умолчанию будет отличаться для каждого поставщика СУБД, а также потому, что значения по умолчанию имеют меньшее значение в чистой среде выполнения EF, поскольку каждая вставка строки будет предоставлять текущее значение для каждого свойства, даже если он равен нулю, поэтому ограничение значения по умолчанию никогда не оценивается.
Этот оператор AlterColumn - единственный раз, когда в игру вступает ограничение по умолчанию, я думаю, это стало более низким приоритетом для команды, которая разработала реализацию миграции SQL Server.

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


  1. Объявление значения по умолчанию.
    Создайте или измените назначение существующего атрибута для определения значения по умолчанию для использования. В этом примере мы создадим новый атрибут с именем DefaultValue, который наследуется от ComponentModel.DefaultValueAttribute, так как использование интуитивно понятно и существует вероятность того, что существующие базы кода уже реализуют этот атрибут. В этой реализации вам нужно использовать только этот конкретный атрибут для доступа к DefaultValueSql, который полезен для дат и других пользовательских сценариев.

    Реализация

    [DefaultValue("Insert DefaultValue Here")]
    [Required]     /// <--- NEW
    public string Director { get; set; }
    
    // Example of default value sql
    [DefaultValue(DefaultValueSql: "GetDate()")]
    [Required]
    public string LastModified { get; set; }
    

    Определение атрибута

    namespace EFExtensions
    {
        /// <summary>
        /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/>
        /// </summary>
        public class DefaultValueAttribute : System.ComponentModel.DefaultValueAttribute
        {
            /// <summary>
            /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/>
            /// </summary>
            public DefaultValueAttribute() : base("")
            {
            }
    
            /// <i
            /// <summary>
            /// Optional SQL to use to specify the default value.
            /// </summary>
            public string DefaultSql { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a Unicode character.
            /// </summary>
            /// <param name="value">
            /// A Unicode character that is the default value.
            /// </param>
            public DefaultValueAttribute(char value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using an 8-bit unsigned integer.
            /// </summary>
            /// <param name="value">
            /// An 8-bit unsigned integer that is the default value.
            /// </param>
            public DefaultValueAttribute(byte value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a 16-bit signed integer.
            /// </summary>
            /// <param name="value">
            /// A 16-bit signed integer that is the default value.
            /// </param>
            public DefaultValueAttribute(short value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a 32-bit signed integer.
            /// </summary>
            /// <param name="value">
            /// A 32-bit signed integer that is the default value.
            /// </param>
            public DefaultValueAttribute(int value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a 64-bit signed integer.
            /// </summary>
            /// <param name="value">
            /// A 64-bit signed integer that is the default value.
            /// </param>
            public DefaultValueAttribute(long value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a single-precision floating point number.
            /// </summary>
            /// <param name="value">
            /// A single-precision floating point number that is the default value.
            /// </param>
            public DefaultValueAttribute(float value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a double-precision floating point number.
            /// </summary>
            /// <param name="value">
            /// A double-precision floating point number that is the default value.
            /// </param>
            public DefaultValueAttribute(double value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a System.Boolean value.
            /// </summary>
            /// <param name="value">
            /// A System.Boolean that is the default value.
            /// </param>
            public DefaultValueAttribute(bool value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class using a System.String.
            /// </summary>
            /// <param name="value">
            /// A System.String that is the default value.
            /// </param>
            public DefaultValueAttribute(string value) : base(value) { }
    
            /// <summary>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class.
            /// </summary>
            /// <param name="value">
            /// An System.Object that represents the default value.
            /// </param>
            public DefaultValueAttribute(object value) : base(value) { }
    
            /// /// <inheritdoc/>
            /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute
            /// class, converting the specified value to the specified type, and using an invariant
            /// culture as the translation context.
            /// </summary>
            /// <param name="type">
            /// A System.Type that represents the type to convert the value to.
            /// </param>
            /// <param name="value">
            /// A System.String that can be converted to the type using the System.ComponentModel.TypeConverter
            /// for the type and the U.S. English culture.
            /// </param>
            public DefaultValueAttribute(Type type, string value) : base(value) { }
        }
    }
    
  2. Создайте соглашение о добавлении значения по умолчанию в аннотации столбцов. Аннотации
    столбцов используются для передачи настраиваемых метаданных о столбцах в генератор сценария миграции.
    Использование соглашения для этого демонстрирует мощь нотации атрибутов, позволяющую упростить определение и управление плавными метаданными для многих свойств, вместо того, чтобы указывать их индивидуально для каждого поля.

    namespace EFExtensions
    {
    
        /// <summary>
        /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute
        /// </summary>
        public class DefaultValueConvention : Convention
        {
            /// <summary>
            /// Annotation Key to use for Default Values specified directly as an object
            /// </summary>
            public const string DirectValueAnnotationKey = "DefaultValue";
            /// <summary>
            /// Annotation Key to use for Default Values specified as SQL Strings
            /// </summary>
            public const string SqlValueAnnotationKey = "DefaultSql";
    
            /// <summary>
            /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute
            /// </summary>
            public DefaultValueConvention()
            {
                // Implement SO Default Value Attributes first
                this.Properties()
                        .Where(x => x.HasAttribute<EFExtensions.DefaultValueAttribute>())
                        .Configure(c => c.HasColumnAnnotation(
                            c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeKey(),
                            c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeValue()
                            ));
    
                // Implement Component Model Default Value Attributes, but only if it is not the SO implementation
                this.Properties()
                        .Where(x => x.HasAttribute<System.ComponentModel.DefaultValueAttribute>())
                        .Where(x => !x.HasAttribute<MiniTuber.DataAnnotations.DefaultValueAttribute>())
                        .Configure(c => c.HasColumnAnnotation(
                            DefaultValueConvention.DirectValueAnnotationKey, 
                            c.GetAttribute<System.ComponentModel.DefaultValueAttribute>().Value
                            ));
            }
        }
    
        /// <summary>
        /// Extension Methods to simplify the logic for building column annotations for Default Value processing
        /// </summary>
        public static partial class PropertyInfoAttributeExtensions
        {
            /// <summary>
            /// Wrapper to simplify the lookup for a specific attribute on a property info.
            /// </summary>
            /// <typeparam name="T">Type of attribute to lookup</typeparam>
            /// <param name="self">PropertyInfo to inspect</param>
            /// <returns>True if an attribute of the requested type exists</returns>
            public static bool HasAttribute<T>(this PropertyInfo self) where T : Attribute
            {
                return self.GetCustomAttributes(false).OfType<T>().Any();
            }
    
            /// <summary>
            /// Wrapper to return the first attribute of the specified type
            /// </summary>
            /// <typeparam name="T">Type of attribute to return</typeparam>
            /// <param name="self">PropertyInfo to inspect</param>
            /// <returns>First attribuite that matches the requested type</returns>
            public static T GetAttribute<T>(this System.Data.Entity.ModelConfiguration.Configuration.ConventionPrimitivePropertyConfiguration self) where T : Attribute
            {
                return self.ClrPropertyInfo.GetCustomAttributes(false).OfType<T>().First();
            }
    
            /// <summary>
            /// Helper to select the correct DefaultValue annotation key based on the attribute values
            /// </summary>
            /// <param name="self"></param>
            /// <returns></returns>
            public static string GetDefaultValueAttributeKey(this EFExtensions.DefaultValueAttribute self)
            {
                return String.IsNullOrWhiteSpace(self.DefaultSql) ? DefaultValueConvention.DirectValueAnnotationKey : DefaultValueConvention.SqlValueAnnotationKey;
            }
    
            /// <summary>
            /// Helper to select the correct attribute property to send as a DefaultValue annotation value
            /// </summary>
            /// <param name="self"></param>
            /// <returns></returns>
            public static object GetDefaultValueAttributeValue(this EFExtensions.DefaultValueAttribute self)
            {
                return String.IsNullOrWhiteSpace(self.DefaultSql) ? self.Value : self.DefaultSql;
            }
        }
    
    }
    
  3. Добавление соглашения в DbContext
    Есть много способов добиться этого, я люблю объявлять соглашения как первый настраиваемый шаг в моей логике ModelCreation, это будет в вашем классе DbContext.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        // Use our new DefaultValueConvention
        modelBuilder.Conventions.Add<EFExtensions.DefaultValueConvention>();
    
        // My personal favourites ;)
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
    
    }
    
  4. Переопределение MigrationCodeGenerator
    Теперь, когда эти аннотации были применены к определениям столбцов в модели, нам нужно изменить генератор сценария миграции, чтобы использовать эти аннотации. Для этого мы унаследуем от, System.Data.Entity.Migrations.Design.CSharpMigrationCodeGeneratorпоскольку нам нужно только внести минимальное количество изменений.
    После того, как мы обработали нашу настраиваемую аннотацию, нам нужно удалить ее из определения столбца, чтобы предотвратить ее сериализацию в окончательный вывод.

    См. Код базового класса, чтобы изучить другие варианты использования: http://entityframework.codeplex.com/sourcecontrol/latest#src/EntityFramework/Migrations/Design/CSharpMigrationCodeGenerator.cs

    namespace EFExtensions
    {
        /// <summary>
        /// Implement DefaultValue constraint definition in Migration Scripts.
        /// </summary>
        /// <remarks>
        /// Original guide that provided inspiration for this https://romiller.com/2012/11/30/code-first-migrations-customizing-scaffolded-code/
        /// </remarks>
        public class CustomCodeGenerator : System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator
        {
            /// <summary>
            /// Inject Default values from the DefaultValue attribute, if the DefaultValueConvention has been enabled.
            /// </summary>
            /// <seealso cref="DefaultValueConvention"/>
            /// <param name="column"></param>
            /// <param name="writer"></param>
            /// <param name="emitName"></param>
            protected override void Generate(ColumnModel column, IndentedTextWriter writer, bool emitName = false)
            {
                var annotations = column.Annotations?.ToList();
                if (annotations != null && annotations.Any())
                {
                    for (int index = 0; index < annotations.Count; index ++)
                    {
                        var annotation = annotations[index];
                        bool handled = true;
    
                        try
                        {
                            switch (annotation.Key)
                            {
                                case DefaultValueConvention.SqlValueAnnotationKey:
                                    if (annotation.Value?.NewValue != null)
                                    {
                                        column.DefaultValueSql = $"{annotation.Value.NewValue}";
                                    }
                                    break;
                                case DefaultValueConvention.DirectValueAnnotationKey:
                                    if (annotation.Value?.NewValue != null)
                                    {
                                        column.DefaultValue = Convert.ChangeType(annotation.Value.NewValue, column.ClrType);
                                    }
                                    break;
                                default:
                                    handled = false;
                                    break;
                            }
                        }
                        catch(Exception ex)
                        {
                            // re-throw with specific debug information
                            throw new ApplicationException($"Failed to Implement Column Annotation for column: {column.Name} with key: {annotation.Key} and new value: {annotation.Value.NewValue}", ex);
                        }
    
                        if(handled)
                        {
                            // remove the annotation, it has been applied
                            column.Annotations.Remove(annotation.Key);
                        }
                    }
                }
                base.Generate(column, writer, emitName);
            }
    
            /// <summary>
            /// Generates class summary comments and default attributes
            /// </summary>
            /// <param name="writer"> Text writer to add the generated code to. </param>
            /// <param name="designer"> A value indicating if this class is being generated for a code-behind file. </param>
            protected override void WriteClassAttributes(IndentedTextWriter writer, bool designer)
            {
                writer.WriteLine("/// <summary>");
                writer.WriteLine("/// Definition of the Migration: {0}", this.ClassName);
                writer.WriteLine("/// </summary>");
                writer.WriteLine("/// <remarks>");
                writer.WriteLine("/// Generated Time: {0}", DateTime.Now);
                writer.WriteLine("/// Generated By: {0}", Environment.UserName);
                writer.WriteLine("/// </remarks>");
                base.WriteClassAttributes(writer, designer);
            }
    
    
        }
    }
    
  5. Зарегистрируйте CustomCodeGenerator
    Последний шаг, в файле конфигурации DbMigration нам нужно указать используемый генератор кода, по умолчанию ищите Configuration.cs в папке Migration ...

    internal sealed class Configuration : DbMigrationsConfiguration<YourApplication.Database.Context>
    {
        public Configuration()
        {
            // I recommend that auto-migrations be disabled so that we control
            // the migrations explicitly 
            AutomaticMigrationsEnabled = false;
            CodeGenerator = new EFExtensions.CustomCodeGenerator();
        }
    
        protected override void Seed(YourApplication.Database.Context context)
        {
            //   Your custom seed logic here
        }
    }
    

2

Начиная с EF Core 2.1, вы можете использовать MigrationBuilder.UpdateDataдля изменения значений перед изменением столбца (чище, чем использование необработанного SQL):

protected override void Up(MigrationBuilder migrationBuilder)
{
    // Change existing NULL values to NOT NULL values
    migrationBuilder.UpdateData(
        table: tableName,
        column: columnName,
        value: valueInsteadOfNull,
        keyColumn: columnName,
        keyValue: null);

    // Change column type to NOT NULL
    migrationBuilder.AlterColumn<ColumnType>(
        table: tableName,
        name: columnName,
        nullable: false,
        oldClrType: typeof(ColumnType),
        oldNullable: true);
}

0

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

Он работал в другом приложении, в том, над которым я работаю, - нет.

Итак, альтернативным, но довольно неэффективным решением было бы переопределить метод SaveChanges (), как показано ниже. Этот метод должен быть в классе Context.

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries().Where(entry => entry.Entity.GetType().GetProperty("ColumnName") != null))
        {
            if (entry.State == EntityState.Added)
            {
                entry.Property("ColumnName").CurrentValue = "DefaultValue";
            }
        }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.