¿Valor predeterminado para los campos obligatorios en las migraciones de Entity Framework?


91

Agregué la [Required]anotación de datos a uno de mis modelos en una aplicación ASP.NET MVC . Después de crear una migración, ejecutar el Update-Databasecomando da como resultado el siguiente error:

No se puede insertar el valor NULL en la columna 'Director', tabla 'MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies'; la columna no permite valores nulos. ACTUALIZAR falla. La instrucción se ha terminado.

Esto se debe a que algunos registros tienen NULL en sus Directorcolumnas. ¿Cómo puedo cambiar automáticamente esos valores a algún director predeterminado (por ejemplo, "John Doe")?

Aquí está mi modelo:

  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; }
    }

y aquí está mi última migración:

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());
    }
}

Respuestas:


74

Si recuerdo bien, algo como esto debería funcionar:

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

Nota: El valor del parámetro defaultValueSql se trata como una declaración SQL literal, por lo que si el valor requerido es efectivamente una cadena, como el ejemplo de John Doe, entonces se requieren comillas simples alrededor del valor.


9
Yo también lo pensé, pero eso no parece funcionar para los registros existentes. Entonces todavía recibo un error.
Andriy Drozdyuk

@drozzy Tal vez sea un error, como aquí: EF 4.3.1 Excepción de migración - AlterColumn defaultValueSql crea el mismo nombre de restricción predeterminado para diferentes tablas . Puede actualizar filas con la IS NULLverificación de su consulta.
desarrollador web

Interesante, pero no estoy seguro de entender de qué están hablando. Sin embargo, si esto es un error, entonces sí, tendría sentido.
Andriy Drozdyuk

6
Creo que debería ser: "'John Doe'"- necesitas usar comillas SQL.
Sean

1
@webdeveloper, no creo que sea un error, ¿por qué AlterColumnactualizaría los valores actuales? Es un comando DDL (no DML).
Anton

110

Además de la respuesta de @webdeveloper y @Pushpendra, debe agregar actualizaciones manualmente a su migración para actualizar las filas existentes. Por ejemplo:

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"));
}

Esto se debe a que AlterColumnproduce DDL para establecer el valor predeterminado de la columna en algún valor específico en la especificación de la tabla. El DDL no afecta las filas existentes en la base de datos.

En realidad, está realizando dos cambios al mismo tiempo (estableciendo el valor predeterminado y haciendo que la columna NO sea NULL) y cada uno de ellos es válido individualmente, pero como está haciendo los dos al mismo tiempo, puede esperar que el sistema ' de forma inteligente 'darse cuenta de su intención y establecer todos los NULLvalores en el valor predeterminado, pero esto no es lo que se espera todo el tiempo.

Suponga que solo establece el valor predeterminado para la columna y no lo convierte en NO NULO. Obviamente, no espera que todos los registros NULL se actualicen con el valor predeterminado que proporciona.

Entonces, en mi opinión, esto no es un error, y no quiero que EF actualice mis datos de la manera en que no le digo explícitamente que lo haga. El desarrollador es responsable de instruir al sistema sobre qué hacer con los datos.


17
Para las personas que encuentran esta respuesta a través de Google: acabo de probar esto en EF6 y la declaración de actualización ya no parece ser necesaria (ya). Supongo que lo consideraron un error después de todo.
EPLKleijntjens

3
También puedo dar fe de eso. Si necesita un valor predeterminado incluso para un campo anulable, simplemente cámbielo a no anulable primero con un valor predeterminado, y luego cámbielo nuevamente a anulable. Muy útil para cuando agregó un campo que no acepta valores NULL a una clase secundaria :)
Wouter Schut

1
Explicación puntual. AlterColumn () simplemente altera la definición de la columna. No afecta los registros existentes en absoluto
Korayem

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
Um ... gracias, pero ¿en qué se diferencia eso de la respuesta de @ webdeveloper?
Andriy Drozdyuk

1
no le dice dónde debe agregar el parámetro de valor predeterminado
Pushpendra

1
@Pushpendra, es curioso cómo los desarrolladores tienden a olvidar que alguna vez no sabían mucho. Me gustan las respuestas detalladas que satisfacen todos los niveles. ¡Excelente trabajo!
útil Abeja

5

No estoy seguro de si esta opción siempre estuvo disponible, pero encontré un problema similar, descubrí que pude establecer el valor predeterminado sin ejecutar ninguna actualización manual usando lo siguiente

defaultValueSql: "'NY'"

Recibí un error cuando el valor proporcionado fue "NY"entonces me di cuenta de que estaban esperando un valor de SQL como "GETDATE()"así lo intenté"'NY'" y eso funcionó

toda la línea se ve así

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

Gracias a esta respuesta , me encaminó por el camino correcto


2

Descubrí que solo usar Auto-Property Initializer en la propiedad de la entidad es suficiente para hacer el trabajo.

Por ejemplo:

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

2
Es una buena respuesta (me ayudó), pero esto no agrega un valor predeterminado en la base de datos, establece el valor en el código.
chris31389

correcto, no agregó un valor predeterminado en la base de datos después de los cambios de migración
Chetan Chaudhari

2

Muchas de las otras respuestas se centran en cómo intervenir manualmente cuando ocurren estos problemas.

Después de generar la migración, realice cualquiera de los siguientes cambios en la migración:

  1. Modifique la definición de columna para incluir una instrucción defaultValue o defaultSql:
    AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));

  2. Inyecte una declaración SQL para rellenar previamente las columnas existentes, antes de AlterColumn:
    Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");

Tenga en cuenta que los cambios manuales aplicados a una secuencia de comandos de migración se sobrescribirán si reestructura la migración. Para la primera solución, es bastante fácil extender EF para definir un valor predeterminado en un campo automáticamente como parte de la generación de migración.

NOTA: EF no hace esto automáticamente porque la implementación del valor predeterminado sería diferente para cada proveedor de RDBMS, pero también porque los valores predeterminados tienen menos significado en un tiempo de ejecución de EF puro porque cada inserción de fila proporcionará el valor actual para cada propiedad, incluso si es nulo, por lo que la restricción de valor predeterminado nunca se evalúa.
Esta declaración de AlterColumn es la única vez que entra en juego la restricción predeterminada, supongo que se convierte en una prioridad menor para el equipo que diseñó la Implementación de migración de SQL Server.

La siguiente solución combina la notación de atributos, las convenciones de configuración del modelo y las anotaciones de columna para pasar los metadatos a un generador de código de migración personalizado. Los pasos 1 y 2 se pueden reemplazar con notación fluida para cada campo afectado si no está utilizando la notación de atributos.
Hay muchas técnicas en juego aquí, siéntete libre de usar algunas o todas, espero que haya valor para todos aquí.


  1. Declarar el valor predeterminado
    Cree o reutilice un atributo existente para definir el valor predeterminado que se utilizará, para este ejemplo crearemos un nuevo atributo llamado DefaultValue que hereda de ComponentModel.DefaultValueAttribute, ya que el uso es intuitivo y existe la posibilidad de que exista las bases de código ya implementan este atributo. Con esta implementación, solo necesita usar este atributo específico para acceder a DefaultValueSql, que es útil para fechas y otros escenarios personalizados.

    Implementación

    [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; }

    Definición de atributo

    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. Cree una convención para inyectar el valor predeterminado en las anotaciones de columna Las anotaciones de
    columna se utilizan para pasar metadatos personalizados sobre columnas al generador de secuencias de comandos de migración.
    El uso de una convención para hacer esto demuestra el poder detrás de la notación de atributos para simplificar cómo se pueden definir y manipular metadatos fluidos para muchas propiedades en lugar de especificarlos individualmente para cada campo.

    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. Agregue la convención al DbContext
    Hay muchas formas de lograr esto, me gusta declarar las convenciones como el primer paso personalizado en mi lógica ModelCreation, esto estará en su clase 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. Reemplazar el
    generador de código de migración Ahora que esas anotaciones se han aplicado a las definiciones de columna dentro del modelo, debemos modificar el generador de secuencias de comandos de migración para usar esas anotaciones. Para esto, heredaremos del System.Data.Entity.Migrations.Design.CSharpMigrationCodeGeneratorya que solo necesitamos inyectar una cantidad mínima de cambio.
    Una vez que hayamos procesado nuestra anotación personalizada, debemos eliminarla de la definición de columna para evitar que se serialice en la salida final.

    Consulte el código de la clase base para explorar otros usos: 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. Registre el CustomCodeGenerator
    Último paso, en el archivo de configuración de DbMigration necesitamos especificar el generador de código a usar, busque Configuration.cs en su carpeta de migración por defecto ...

    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

Desde EF Core 2.1, puede usar MigrationBuilder.UpdateDatapara cambiar valores antes de alterar la columna (más limpio que usar SQL sin formato):

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

Por alguna razón, que no pude explicarme, la respuesta aprobada ya no me funciona.

Funcionó en otra aplicación, en la que estoy trabajando, no.

Por lo tanto, una solución alternativa, pero bastante ineficiente , sería anular el método SaveChanges () como se muestra a continuación. Este método debe estar en la clase 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";
            }
        }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.