自己动手写个ORM实现(3)
在前面两节 自己动手写个ORM实现(1) , 自己动手写个ORM实现(2) 里我们分别实现了接口规范以及初步的查询功能.
接下来,我们将继续完成EntityManager的实现.
----------------------------------------------------------------------------------------------------------
![](http://222.173.176.83:88/orm/ientitymanager.jpg)
来到Delete<T>方法,功能也非常简单,根据传入的ID值来删除一个对象,实现如下
然后是新增对象的方法 Create<T>
我们来看到上面代码的第7行,我们通过调用GetCreatedProperties<T>方法传入的对象实例,判断得出有那些属性已经赋值
这里需要注意一点的是: 如果实体对象的某个实例是DateTime类型即使没有被复制,它也有一个初始值DateTime.MinValue,因此不能简单判断它是否为null.
接着,我们通过得到的PropertyInfo集合来生成执行sql操作所必要的字段和参数值.
Code
如此,新增实体的功能就初步实现了.
然后是更新的方法 Update<T>
而通过GetUpdatedProperties<T>方法, 我们可以得到该对象对比已持久化在数据库中的状态有哪些属性做过了更改操作, 这样一来就可以有针对性的只更新改变过值的属性.
实现如下
通过得到的已更改过的属性集合,生成执行sql时所需要的参数值,如下
---------------------------------------------------------------------------------------------------------------------------------------------
经过前三节的工作,我们一起完成了最简单的crud操作. 在接下来的章节我们将就性能优化,实体关联,事务等对已有的功能实现进行扩展.
接下来,我们将继续完成EntityManager的实现.
----------------------------------------------------------------------------------------------------------
![](http://222.173.176.83:88/orm/ientitymanager.jpg)
来到Delete<T>方法,功能也非常简单,根据传入的ID值来删除一个对象,实现如下
1
public void Delete<T>(int id) where T : IEntity
2
{
3
Type type = typeof(T);
4
string tableName = GetTableName<T>();
5
string sql = "DELETE FROM {0} WHERE [ID]={1}";
6
sql = string.Format(sql, tableName, id);
7
Database db = DatabaseFactory.CreateDatabase();
8
DbCommand cmd = db.GetSqlStringCommand(sql);
9![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
try
11
{
12
db.ExecuteNonQuery(cmd);
13
}
14
catch (Exception ex)
15
{
16
throw ex;
17
}
18
}
这段代码大家应该一看就明白,就不多说了. ![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
然后是新增对象的方法 Create<T>
1
public T Create<T>(T t) where T : IEntity
2
{
3
int i = 0;
4
Type type = typeof(T);
5
string tableName = GetTableName<T>();
6
IEnumerable<PropertyInfo> properties = GetCreatedProperties<T>(t);
7
IEnumerable<string> fields = GetFields(properties);
8
string sql = "INSERT INTO {0}({1}) VALUES({2});SELECT @@IDENTITY;";
9
string sqlFields = GetSqlFields(fields);
10
string sqlParams = GetSqlParams(fields);
11
sql = string.Format(sql, tableName, sqlFields, sqlParams);
12![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
Database db = DatabaseFactory.CreateDatabase();
14
DbCommand cmd = db.GetSqlStringCommand(sql);
15![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
foreach (PropertyInfo p in properties)
17
{
18
string fieldName = GetFieldName(p);
19
DbType dbType = GetDbType(p.PropertyType.Name);
20
object value = p.GetValue(t, null);
21![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
22
db.AddInParameter(cmd, fieldName, dbType, value);
23
}
24![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
25
try
26
{
27
i = Convert.ToInt32(db.ExecuteScalar(cmd)) ;
28
}
29
catch (Exception ex)
30
{
31
throw ex;
32
}
33![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
34
return Load<T>(i);
35
}
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
19
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
21
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
22
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
23
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
24
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
25
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
26
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
27
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
28
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
29
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
30
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
31
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
32
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
33
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
34
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
35
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
我们来看到上面代码的第7行,我们通过调用GetCreatedProperties<T>方法传入的对象实例,判断得出有那些属性已经赋值
1
/// <summary>
2
/// 获取新增数据时需要持久化的属性
3
/// </summary>
4
/// <typeparam name="T">实体类型</typeparam>
5
/// <param name="t"></param>
6
/// <returns>属性集合</returns>
7
protected IEnumerable<PropertyInfo> GetCreatedProperties<T>(T t) where T : IEntity
8
{
9
Type type = typeof(T);
10
foreach (PropertyInfo p in GetMappedProperties<T>())
11
{
12
if (p.Name.ToUpper() == "ID")
13
continue;
14
object value = p.GetValue(t, null);
15
if (value == null)
16
continue;
17
if (p.PropertyType == typeof(DateTime) && Convert.ToDateTime(value) == DateTime.MinValue)
18
continue;
19![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
yield return p;
21
}
22
}
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
19
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
21
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
22
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
这里需要注意一点的是: 如果实体对象的某个实例是DateTime类型即使没有被复制,它也有一个初始值DateTime.MinValue,因此不能简单判断它是否为null.
接着,我们通过得到的PropertyInfo集合来生成执行sql操作所必要的字段和参数值.
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
然后是更新的方法 Update<T>
1
public void Update<T>(T t) where T : IEntity
2
{
3
if (!t.IsPersisted)
4
return;
5![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
PropertyInfo[] properties = GetUpdatedProperties<T>(t);
7
if (properties.Length == 0)
8
return;
9![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
Type type = typeof(T);
11
string tableName = GetTableName<T>();
12
string updateParams = GetUpdateParams(properties);
13
string sql = "UPDATE {0} SET {1} WHERE [ID]={2}";
14
sql = string.Format(sql, tableName, updateParams, t.ID);
15![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
Database db = DatabaseFactory.CreateDatabase();
17
DbCommand cmd = db.GetSqlStringCommand(sql);
18![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
19
foreach (PropertyInfo p in properties)
20
{
21
string fieldName = GetFieldName(p);
22
DbType dbType = GetDbType(p.PropertyType.Name);
23
object value = p.GetValue(t, null);
24![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
25
db.AddInParameter(cmd, fieldName, dbType, value);
26
}
27![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
28
try
29
{
30
db.ExecuteNonQuery(cmd);
31
}
32
catch (Exception ex)
33
{
34
throw ex;
35
}
36
}
首先得判断传入的实体对象是否已持久化,也就是说这里的实例是否在数据库中存在一条记录与其对应, 如果没有的话则返回.![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
19
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
21
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
22
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
23
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
24
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
25
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
26
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
27
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
28
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
29
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
30
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
31
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
32
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
33
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
34
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
35
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
36
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
而通过GetUpdatedProperties<T>方法, 我们可以得到该对象对比已持久化在数据库中的状态有哪些属性做过了更改操作, 这样一来就可以有针对性的只更新改变过值的属性.
实现如下
1
/// <summary>
2
/// 获取实体对象需要更新的属性集合
3
/// </summary>
4
/// <typeparam name="T">实体类型</typeparam>
5
/// <param name="t"></param>
6
/// <returns>属性集合</returns>
7
private PropertyInfo[] GetUpdatedProperties<T>(T t) where T : IEntity
8
{
9
Type type = typeof(T);
10
T raw = (T)t.Raw;
11
List<PropertyInfo> properties = new List<PropertyInfo>();
12
foreach (PropertyInfo p in GetMappedProperties<T>())
13
{
14
object source = p.GetValue(t, null);
15
object target = p.GetValue(raw, null);
16![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
if (source == null && target == null)
18
continue;
19![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
if ((source == null || target == null))
21
{
22
properties.Add(p);
23
continue;
24
}
25![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
26
if (!source.Equals(target))
27
properties.Add(p);
28
}
29![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
30
return properties.ToArray();
31
}
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
18
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
19
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
20
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
21
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
22
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
23
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
24
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
25
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
26
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
27
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
28
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
29
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
30
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
31
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
通过得到的已更改过的属性集合,生成执行sql时所需要的参数值,如下
1
/// <summary>
2
/// 获取需要更新属性对应的sql字符串
3
/// </summary>
4
/// <param name="properties"></param>
5
/// <returns>SET字符串</returns>
6
private string GetUpdateParams(PropertyInfo[] properties)
7
{
8
StringBuilder builder = new StringBuilder();
9![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
foreach (PropertyInfo p in properties)
11
{
12
string fieldName = GetFieldName(p);
13
builder.AppendFormat("{0} = {1},", fieldName, ParamTag + fieldName);
14
}
15![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
return builder.ToString().TrimEnd(',');
17
}
最后借助企业库的数据模块执行生成好的sql语句,我们的更新功能也将完成.![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
2
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
3
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
4
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
5
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
6
![](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
7
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockStart.gif)
8
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
9
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
10
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
11
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
12
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
13
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
14
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
15
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
16
![](https://www.cnblogs.com/Images/OutliningIndicators/InBlock.gif)
17
![](https://www.cnblogs.com/Images/OutliningIndicators/ExpandedBlockEnd.gif)
---------------------------------------------------------------------------------------------------------------------------------------------
经过前三节的工作,我们一起完成了最简单的crud操作. 在接下来的章节我们将就性能优化,实体关联,事务等对已有的功能实现进行扩展.
posted on 2008-05-28 18:29 yyliuliang 阅读(3165) 评论(9) 编辑 收藏 举报