【问题标题】:Performing Inserts and Updates with Dapper使用 Dapper 执行插入和更新
【发布时间】:2011-08-22 21:37:35
【问题描述】:

我对使用 Dapper 很感兴趣——但据我所知,它只支持查询和执行。我没有看到 Dapper 包含插入和更新对象的方法。

鉴于我们的项目(大多数项目?)需要进行插入和更新,与 dapper 一起进行插入和更新的最佳做法是什么?

最好我们不必求助于 ADO.NET 的参数构建方法等。

此时我能想到的最佳答案是使用 LinqToSQL 进行插入和更新。有更好的答案吗?

【问题讨论】:

标签: c# orm dapper


【解决方案1】:

我们正在考虑构建一些帮助程序,仍在决定 API 以及这是否属于核心。请参阅:https://code.google.com/archive/p/dapper-dot-net/issues/6 了解进度。

与此同时,您可以执行以下操作

val = "my value";
cnn.Execute("insert into Table(val) values (@val)", new {val});

cnn.Execute("update Table set val = @val where Id = @id", new {val, id = 1});

等等

另见我的博文:That annoying INSERT problem

更新

正如 cmets 中所指出的,Dapper.Contrib 项目中现在有几个扩展可用,这些扩展以这些 IDbConnection 扩展方法的形式出现:

T Get<T>(id);
IEnumerable<T> GetAll<T>();
int Insert<T>(T obj);
int Insert<T>(Enumerable<T> list);
bool Update<T>(T obj);
bool Update<T>(Enumerable<T> list);
bool Delete<T>(T obj);
bool Delete<T>(Enumerable<T> list);
bool DeleteAll<T>();

【讨论】:

  • 嗨,Sam,在 google 上找到了你的 SO 答案,我想知道最后一行代码是否应该包含 set 作为 cnn.Execute("update Table SET val = @val where Id = @id", new {val, id = 1}); 或者这个小巧玲珑?我是 dapper 的新手,正在寻找更新示例 :)
  • @JPHellemons 我试过这个var updateCat = connection.Execute("UPDATE tCategories SET sCategory = @val WHERE iCategoryID = @id", new { val = "dapper test", id = 23 });,它成功了。如果不使用 SET,我会在 sCategory 附近收到 SQLException 语法错误。
  • @RosdiKasim 这不是违背了使用 Dapper 的目的吗?我想使用 SQL。这对其进行了抽象。我错过了什么?
  • @johnny 这只是帮助类......有些人希望他们的代码尽可能简洁......如果你不想要它,你不必使用它。
【解决方案2】:

使用 Dapper 执行 CRUD 操作是一项简单的任务。我已经提到了以下示例,它们应该可以帮助您进行 CRUD 操作。

CRUD 的代码:

方法#1:当您插入来自不同实体的值时使用此方法。

using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDbConnection"].ConnectionString))
{
    string insertQuery = @"INSERT INTO [dbo].[Customer]([FirstName], [LastName], [State], [City], [IsActive], [CreatedOn]) VALUES (@FirstName, @LastName, @State, @City, @IsActive, @CreatedOn)";

    var result = db.Execute(insertQuery, new
    {
        customerModel.FirstName,
        customerModel.LastName,
        StateModel.State,
        CityModel.City,
        isActive,
        CreatedOn = DateTime.Now
    });
}

方法 #2: 当您的实体属性与 SQL 列具有相同的名称时,使用此方法。因此,作为 ORM 的 Dapper 将实体属性映射到匹配的 SQL 列。

using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDbConnection"].ConnectionString))
{
    string insertQuery = @"INSERT INTO [dbo].[Customer]([FirstName], [LastName], [State], [City], [IsActive], [CreatedOn]) VALUES (@FirstName, @LastName, @State, @City, @IsActive, @CreatedOn)";

    var result = db.Execute(insertQuery, customerViewModel);
}

CRUD 的代码:

using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDbConnection"].ConnectionString))
{
    string selectQuery = @"SELECT * FROM [dbo].[Customer] WHERE FirstName = @FirstName";

    var result = db.Query(selectQuery, new
    {
        customerModel.FirstName
    });
}

CR的代码UD:

using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDbConnection"].ConnectionString))
{
    string updateQuery = @"UPDATE [dbo].[Customer] SET IsActive = @IsActive WHERE FirstName = @FirstName AND LastName = @LastName";

    var result = db.Execute(updateQuery, new
    {
        isActive,
        customerModel.FirstName,
        customerModel.LastName
    });
}

CRU 代码D

using (IDbConnection db = new SqlConnection(ConfigurationManager.ConnectionStrings["myDbConnection"].ConnectionString))
{
    string deleteQuery = @"DELETE FROM [dbo].[Customer] WHERE FirstName = @FirstName AND LastName = @LastName";

    var result = db.Execute(deleteQuery, new
    {
        customerModel.FirstName,
        customerModel.LastName
    });
}

【讨论】:

    【解决方案3】:

    你可以这样做:

    sqlConnection.Open();
    
    string sqlQuery = "INSERT INTO [dbo].[Customer]([FirstName],[LastName],[Address],[City]) VALUES (@FirstName,@LastName,@Address,@City)";
    sqlConnection.Execute(sqlQuery,
        new
        {
            customerEntity.FirstName,
            customerEntity.LastName,
            customerEntity.Address,
            customerEntity.City
        });
    
    sqlConnection.Close();
    

    【讨论】:

    • 您应该使用using-statement,这样即使出现异常,连接也会关闭。
    • 你可以直接传递 customerEntity 而不是使用匿名类型...
    • @ThomasLevesque 你这是什么意思?你能提供一个小代码示例来说明你的意思吗?
    • @iaacp,我的意思是:sqlConnection.Execute(sqlQuery, customerEntity);
    • @ThomasLevesque 我们也可以使用相同的模式进行更新吗?即sqlConnection.Execute(sqlQuery, customerEntity);
    【解决方案4】:

    使用Dapper.Contrib就这么简单:

    插入列表:

    public int Insert(IEnumerable<YourClass> yourClass)
    {
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            return conn.Insert(yourClass) ;
        }
    }
    

    插入单曲:

    public int Insert(YourClass yourClass)
    {
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            return conn.Insert(yourClass) ;
        }
    }
    

    更新列表:

    public bool Update(IEnumerable<YourClass> yourClass)
    {
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            return conn.Update(yourClass) ;
        }
    }
    

    更新单曲:

    public bool Update(YourClass yourClass)
    {
        using (SqlConnection conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            return conn.Update(yourClass) ;
        }
    }
    

    来源:https://github.com/StackExchange/Dapper/tree/master/Dapper.Contrib

    【讨论】:

    • 使用上述方法插入单个对象,您可以获得新的身份编号并将其放回您的模型中......但是如何插入对象列表 - 对象列表中没有标识字段。您是否必须遍历列表,然后一次插入一个,每次都取出新 ID?
    • @harag 如果您在其他地方需要新 ID,我想您必须这样做。实体框架处理引用类型,如类,没有插入问题,但如果这是你的角度,我不知道 Dapper.Contrib 如何使用它。
    • @Ogglas,谢谢。我注意到如果我只是插入一个对象,“connection.Insert(myObject)”将更新“myObject”的“[key]”属性,但如果我使用相同的对象插入一个说 5 个对象的列表"connection.Insert(myObjectList)" 然后没有任何 [keys] 属性被更新,所以我必须手动执行列表中的每个项目并一次插入一个。
    • conn.Update(yourClass) 中,如果某些属性为空,那么将字段更新为空?不工作。 将字段更新为 NULLNot partials updates
    【解决方案5】:

    您还可以将 dapper 与存储过程和通用方式一起使用,从而轻松管理所有内容。

    定义你的连接:

    public class Connection: IDisposable
    {
        private static SqlConnectionStringBuilder ConnectionString(string dbName)
        {
            return new SqlConnectionStringBuilder
                {
                    ApplicationName = "Apllication Name",
                    DataSource = @"Your source",
                    IntegratedSecurity = false,
                    InitialCatalog = Database Name,
                    Password = "Your Password",
                    PersistSecurityInfo = false,
                    UserID = "User Id",
                    Pooling = true
                };
        }
    
        protected static IDbConnection LiveConnection(string dbName)
        {
            var connection = OpenConnection(ConnectionString(dbName));
            connection.Open();
            return connection;
        }
    
        private static IDbConnection OpenConnection(DbConnectionStringBuilder connectionString)
        {
            return new SqlConnection(connectionString.ConnectionString);
        }
    
        protected static bool CloseConnection(IDbConnection connection)
        {
            if (connection.State != ConnectionState.Closed)
            {
                connection.Close();
                // connection.Dispose();
            }
            return true;
        }
    
        private static void ClearPool()
        {
            SqlConnection.ClearAllPools();
        }
    
        public void Dispose()
        {
            ClearPool();
        }
    }
    

    创建一个接口来定义你真正需要的 Dapper 方法:

     public interface IDatabaseHub
        {
       long Execute<TModel>(string storedProcedureName, TModel model, string dbName);
    
            /// <summary>
            /// This method is used to execute the stored procedures with parameter.This is the generic version of the method.
            /// </summary>
            /// <param name="storedProcedureName">This is the type of POCO class that will be returned. For more info, refer to https://msdn.microsoft.com/en-us/library/vstudio/dd456872(v=vs.100).aspx. </param>
            /// <typeparam name="TModel"></typeparam>
            /// <param name="model">The model object containing all the values that passes as Stored Procedure's parameter.</param>
            /// <returns>Returns how many rows have been affected.</returns>
            Task<long> ExecuteAsync<TModel>(string storedProcedureName, TModel model, string dbName);
    
            /// <summary>
            /// This method is used to execute the stored procedures with parameter. This is the generic version of the method.
            /// </summary>
            /// <param name="storedProcedureName">Stored Procedure's name. Expected to be a Verbatim String, e.g. @"[Schema].[Stored-Procedure-Name]"</param>
            /// <param name="parameters">Parameter required for executing Stored Procedure.</param>        
            /// <returns>Returns how many rows have been affected.</returns>         
            long Execute(string storedProcedureName, DynamicParameters parameters, string dbName);
    
            /// <summary>
            /// 
            /// </summary>
            /// <param name="storedProcedureName"></param>
            /// <param name="parameters"></param>
            /// <returns></returns>
            Task<long> ExecuteAsync(string storedProcedureName, DynamicParameters parameters, string dbName);
    }
    

    实现接口:

         public class DatabaseHub : Connection, IDatabaseHub
            {
    
     /// <summary>
            /// This function is used for validating if the Stored Procedure's name is correct.
            /// </summary>
            /// <param name="storedProcedureName">Stored Procedure's name. Expected to be a Verbatim String, e.g. @"[Schema].[Stored-Procedure-Name]"</param>
            /// <returns>Returns true if name is not empty and matches naming patter, otherwise returns false.</returns>
    
            private static bool IsStoredProcedureNameCorrect(string storedProcedureName)
            {
                if (string.IsNullOrEmpty(storedProcedureName))
                {
                    return false;
                }
    
                if (storedProcedureName.StartsWith("[") && storedProcedureName.EndsWith("]"))
                {
                    return Regex.IsMatch(storedProcedureName,
                        @"^[\[]{1}[A-Za-z0-9_]+[\]]{1}[\.]{1}[\[]{1}[A-Za-z0-9_]+[\]]{1}$");
                }
                return Regex.IsMatch(storedProcedureName, @"^[A-Za-z0-9]+[\.]{1}[A-Za-z0-9]+$");
            }
    
         /// <summary>
                /// This method is used to execute the stored procedures without parameter.
                /// </summary>
                /// <param name="storedProcedureName">Stored Procedure's name. Expected to be a Verbatim String, e.g. @"[Schema].[Stored-Procedure-Name]"</param>
                /// <param name="model">The model object containing all the values that passes as Stored Procedure's parameter.</param>
                /// <typeparam name="TModel">This is the type of POCO class that will be returned. For more info, refer to https://msdn.microsoft.com/en-us/library/vstudio/dd456872(v=vs.100).aspx. </typeparam>
                /// <returns>Returns how many rows have been affected.</returns>
    
                public long Execute<TModel>(string storedProcedureName, TModel model, string dbName)
                {
                    if (!IsStoredProcedureNameCorrect(storedProcedureName))
                    {
                        return 0;
                    }
    
                    using (var connection = LiveConnection(dbName))
                    {
                        try
                        {
                            return connection.Execute(
                                sql: storedProcedureName,
                                param: model,
                                commandTimeout: null,
                                commandType: CommandType.StoredProcedure
                                );
    
                        }
                        catch (Exception exception)
                        {
                            throw exception;
                        }
                        finally
                        {
                            CloseConnection(connection);
                        }
                    }
                }
    
                public async Task<long> ExecuteAsync<TModel>(string storedProcedureName, TModel model, string dbName)
                {
                    if (!IsStoredProcedureNameCorrect(storedProcedureName))
                    {
                        return 0;
                    }
    
                    using (var connection = LiveConnection(dbName))
                    {
                        try
                        {
                            return await connection.ExecuteAsync(
                                sql: storedProcedureName,
                                param: model,
                                commandTimeout: null,
                                commandType: CommandType.StoredProcedure
                                );
    
                        }
                        catch (Exception exception)
                        {
                            throw exception;
                        }
                        finally
                        {
                            CloseConnection(connection);
                        }
                    }
                }
    
                /// <summary>
                /// This method is used to execute the stored procedures with parameter. This is the generic version of the method.
                /// </summary>
                /// <param name="storedProcedureName">Stored Procedure's name. Expected to be a Verbatim String, e.g. @"[Schema].[Stored-Procedure-Name]"</param>
                /// <param name="parameters">Parameter required for executing Stored Procedure.</param>        
                /// <returns>Returns how many rows have been affected.</returns>
    
                public long Execute(string storedProcedureName, DynamicParameters parameters, string dbName)
                {
                    if (!IsStoredProcedureNameCorrect(storedProcedureName))
                    {
                        return 0;
                    }
    
                    using (var connection = LiveConnection(dbName))
                    {
                        try
                        {
                            return connection.Execute(
                                sql: storedProcedureName,
                                param: parameters,
                                commandTimeout: null,
                                commandType: CommandType.StoredProcedure
                                );
                        }
                        catch (Exception exception)
                        {
                            throw exception;
                        }
                        finally
                        {
                            CloseConnection(connection);
                        }
                    }
                }
    
    
    
                public async Task<long> ExecuteAsync(string storedProcedureName, DynamicParameters parameters, string dbName)
                {
                    if (!IsStoredProcedureNameCorrect(storedProcedureName))
                    {
                        return 0;
                    }
    
                    using (var connection = LiveConnection(dbName))
                    {
                        try
                        {
                            return await connection.ExecuteAsync(
                                sql: storedProcedureName,
                                param: parameters,
                                commandTimeout: null,
                                commandType: CommandType.StoredProcedure
                                );
    
                        }
                        catch (Exception exception)
                        {
                            throw exception;
                        }
                        finally
                        {
                            CloseConnection(connection);
                        }
                    }
                }
    
        }
    

    您现在可以根据需要从模型中调用:

    public class DeviceDriverModel : Base
        {
     public class DeviceDriverSaveUpdate
            {
                public string DeviceVehicleId { get; set; }
                public string DeviceId { get; set; }
                public string DriverId { get; set; }
                public string PhoneNo { get; set; }
                public bool IsActive { get; set; }
                public string UserId { get; set; }
                public string HostIP { get; set; }
            }
    
    
            public Task<long> DeviceDriver_SaveUpdate(DeviceDriverSaveUpdate obj)
            {
    
                return DatabaseHub.ExecuteAsync(
                        storedProcedureName: "[dbo].[sp_SaveUpdate_DeviceDriver]", model: obj, dbName: AMSDB);//Database name defined in Base Class.
            }
    }
    

    您也可以传递参数:

    public Task<long> DeleteFuelPriceEntryByID(string FuelPriceId, string UserId)
            {
    
    
                var parameters = new DynamicParameters();
                parameters.Add(name: "@FuelPriceId", value: FuelPriceId, dbType: DbType.Int32, direction: ParameterDirection.Input);
                parameters.Add(name: "@UserId", value: UserId, dbType: DbType.String, direction: ParameterDirection.Input);
    
                return DatabaseHub.ExecuteAsync(
                        storedProcedureName: @"[dbo].[sp_Delete_FuelPriceEntryByID]", parameters: parameters, dbName: AMSDB);
    
            }
    

    现在从您的控制器调用:

    var queryData = new DeviceDriverModel().DeviceInfo_Save(obj);
    

    希望它可以防止您的代码重复并提供安全性;

    【讨论】:

      【解决方案6】:

      我宁愿建议您自己编写查询,而不是使用任何 3rd 方库进行查询操作。因为使用任何其他 3rd 方包都会带走使用 dapper 的主要优势,即编写查询的灵活性。

      现在,为整个对象编写插入或更新查询时出现问题。为此,可以简单地创建如下助手:

      插入查询构建器:

       public static string InsertQueryBuilder(IEnumerable < string > fields) {
      
      
        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();
      
      
        foreach(string columnName in fields) {
         columns.Append($ "{columnName}, ");
         values.Append($ "@{columnName}, ");
      
        }
        string insertQuery = $ "({ columns.ToString().TrimEnd(',', ' ')}) VALUES ({ values.ToString().TrimEnd(',', ' ')}) ";
      
        return insertQuery;
       }
      

      现在,只需传递要插入的列的名称,整个查询就会自动创建,如下所示:

      List < string > columns = new List < string > {
       "UserName",
       "City"
      }
      //QueryBuilder is the class having the InsertQueryBuilder()
      string insertQueryValues = QueryBuilderUtil.InsertQueryBuilder(columns);
      
      string insertQuery = $ "INSERT INTO UserDetails {insertQueryValues} RETURNING UserId";
      
      Guid insertedId = await _connection.ExecuteScalarAsync < Guid > (insertQuery, userObj);
      

      您还可以通过传递 TableName 参数来修改函数以返回整个 INSERT 语句。

      确保类属性名称与数据库中的字段名称匹配。然后只有你可以传递整个 obj(就像我们的例子中的 userObj)并且值将被自动映射。

      同样的,你也可以拥有 UPDATE 查询的辅助函数:

        public static string UpdateQueryBuilder(List < string > fields) {
         StringBuilder updateQueryBuilder = new StringBuilder();
      
         foreach(string columnName in fields) {
          updateQueryBuilder.AppendFormat("{0}=@{0}, ", columnName);
         }
         return updateQueryBuilder.ToString().TrimEnd(',', ' ');
        }
      

      并像这样使用它:

      List < string > columns = new List < string > {
       "UserName",
       "City"
      }
      //QueryBuilder is the class having the UpdateQueryBuilder()
      string updateQueryValues = QueryBuilderUtil.UpdateQueryBuilder(columns);
      
      string updateQuery =  $"UPDATE UserDetails SET {updateQueryValues} WHERE UserId=@UserId";
      
      await _connection.ExecuteAsync(updateQuery, userObj);
      

      尽管在这些辅助函数中,您也需要传递要插入或更新的字段的名称,但至少您可以完全控制查询,并且还可以在需要时包含不同的 WHERE 子句。

      通过这个辅助函数,您将保存以下代码行:

      对于插入查询:

       $ "INSERT INTO UserDetails (UserName,City) VALUES (@UserName,@City) RETURNING UserId";
      

      对于更新查询:

      $"UPDATE UserDetails SET UserName=@UserName, City=@City WHERE UserId=@UserId";
      

      看似几行代码的区别,但是当对一个字段超过10个的表进行插入或更新操作时,就可以感受到区别了。

      可以使用nameof操作符在函数中传递字段名,避免拼写错误

      代替:

      List < string > columns = new List < string > {
       "UserName",
       "City"
      }
      

      你可以写:

      List < string > columns = new List < string > {
      nameof(UserEntity.UserName),
      nameof(UserEntity.City),
      }
      

      【讨论】:

        【解决方案7】:

        你可以试试这个:

         string sql = "UPDATE Customer SET City = @City WHERE CustomerId = @CustomerId";             
         conn.Execute(sql, customerEntity);
        

        【讨论】:

          【解决方案8】:

          这是一个简单的例子,Repository Pattern

          public interface IUserRepository
          {
              Task<bool> CreateUser(User user);
              Task<bool> UpdateUser(User user);
          }
          

          UserRepository

          public class UserRepository: IUserRepository
              {
                  private readonly IConfiguration _configuration;
          
                  public UserRepository(IConfiguration configuration)
                  {
                      _configuration = configuration;
                  }
          
                  public async Task<bool> CreateUser(User user)
                  {
                      using var connection = new NpgsqlConnection(_configuration.GetValue<string>("DatabaseSettings:ConnectionString"));
          
                      var affected =
                          await connection.ExecuteAsync
                              ("INSERT INTO User (Name, Email, Mobile) VALUES (@Name, @Email, @Mobile)",
                                      new { Name= user.Name, Email= user.Email, Mobile = user.Mobile});
          
                      if (affected == 0)
                          return false;
          
                      return true;
                  }
          
                  public async Task<bool> UpdateUser(User user)
                  {
                      using var connection = new NpgsqlConnection(_configuration.GetValue<string>("DatabaseSettings:ConnectionString"));
          
                      var affected = await connection.ExecuteAsync
                              ("UPDATE User SET Name=@Name, Email= @Email, Mobile = @Mobile WHERE Id = @Id",
                                      new { Name= user.Name, Email= user.Email, Mobile  = user.Mobile , Id = user.Id });
          
                      if (affected == 0)
                          return false;
          
                      return true;
                  }
              }
          

          注意:NpgsqlConnection用于获取PostgreSQL数据库的ConnectionString

          【讨论】:

            【解决方案9】:

            存储过程 + Dapper 方法或 SQL 插入语句 + Dapper 做的工作,但它并不能完美地实现 ORM 的概念,即动态映射数据模型与 SQL 表列,因为如果使用上述两种方法之一,你仍然需要在存储过程参数或 SQL 插入语句中硬编码一些列名值。

            为了解决最小化代码修改的顾虑,可以使用Dapper.Contrib支持SQL插入,这里是official guide,下面是示例设置和代码

            第 1 步

            使用 Dapper.Contrib.Extensions 在 C# 中设置您的类模型: [Table] 属性将指向 SQL 框中所需的表名,[ExplicitKey] 属性将告诉 Dapper 此模型属性是 SQL 表中的主键。

            [Table("MySQLTableName")]
            public class UserModel
            {
                [ExplicitKey]
                public string UserId { get; set; }
                public string Name { get; set; }
                public string Sex { get; set; }
            }
            

            第 2 步

            像这样设置你的 SQL 数据库/表:

            第 3 步

            现在构建您的 C# 代码,如下所示,您需要使用这些命名空间:

            using Dapper.Contrib.Extensions;
            using System.Data;
            

            代码:

            string connectionString = "Server=localhost;Database=SampleSQL_DB;Integrated Security=True";
            
            UserModel objUser1 = new UserModel { UserId = "user0000001" , Name = "Jack", Sex = "Male" };
            UserModel objUser2 = new UserModel { UserId = "user0000002", Name = "Marry", Sex = "female" };
            UserModel objUser3 = new UserModel { UserId = "user0000003", Name = "Joe", Sex = "male" };
            
            List<UserModel> LstUsers = new List<UserModel>();
            LstUsers.Add(objUser2); LstUsers.Add(objUser3);
            
            try
            {
                using (IDbConnection connection = new System.Data.SqlClient.SqlConnection(connectionString))
                {
                    connection.Open();
            
                    using (var trans = connection.BeginTransaction())
                    {
                        try
                        {
                            //  insert single record with custom data model
                            connection.Insert(objUser1, transaction: trans);
            
                            // insert multiple record with List<Type>
                            connection.Insert(LstUsers, transaction: trans);
            
                            // Only save to SQL database if all require SQL operation completed successfully 
                            trans.Commit();
                        }
                        catch (Exception e)
                        {
                            // If one of the SQL operation fail , roll back the whole transaction
                            trans.Rollback();
                        }
                    }
                }
            }
            catch (Exception e) { }
            

            【讨论】:

              猜你喜欢
              • 2016-10-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-01-06
              • 2018-05-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多