天天看點

開發筆記:基于EntityFramework.Extended用EF實作指定字段的更新

今天在将一個項目中使用存儲過程的遺留代碼遷移至新的架構時,遇到了一個問題——如何用EF實作資料庫中指定字段的更新(根據UserId更新Users表中的FaceUrl與AvatarUrl字段)?最終驗證了,添加IUnitOfWork.UpadteAsync()接口,基于EntityFramework.Extended,用EF實作資料庫中指定字段的更新,這種方法在實際開發中使用完全可行。

今天在将一個項目中使用存儲過程的遺留代碼遷移至新的架構時,遇到了一個問題——如何用EF實作資料庫中指定字段的更新(根據UserId更新Users表中的FaceUrl與AvatarUrl字段)?

原先調用存儲過程的代碼:

public bool UpdateAvatar(Guid userId, string faceUrl, string avatarUrl)
{
    DbCommand command = _db.GetStoredProcCommand("User_UpdateFaceAvatar");
    _db.AddInParameter(command, "@FaceUrl", DbType.String, faceUrl);
    _db.AddInParameter(command, "@AvatarUrl", DbType.String, avatarUrl);
    _db.AddInParameter(command, "@UserId", userId);
    return _db.ExecuteNonQuery(command) > 0;
}      

存儲過程中所使用的SQL語句:

UPDATE Users 
SET FaceUrl=@FaceUrl,AvatarUrl=@AvatarUrl
WHERE [UserId]=@UserId      

在新的架構中,資料庫通路用的是Entity Framework,并且用IUnitOfWork接口進行了封裝,Application層對資料庫的操作是通過IUnitOfWork接口完成的。

IUnitOfWork接口是這麼定義的:

public interface IUnitOfWork : IDisposable
{
    IQueryable<T> Set<T>() where T : class;

    T Add<T>(T entity) where T : class;

    IEnumerable<T> AddRange<T>(IEnumerable<T> entities) where T : class;

    T Attach<T>(T entity) where T : class;

    T Remove<T>(T entity) where T : class;

    IEnumerable<T> RemoveRange<T>(IEnumerable<T> entities) where T : class;

    bool Commit();

    Task<bool> CommitAsync();
}      

如果不給IUnitOfWork添加新的接口方法,可以使用EF的change tracking完成指定字段的更新操作。

Application.Services中的實作代碼: 

public async Task<bool> UpdateFaceAvatar(Guid userId, string faceUrl, string avatarUrl)
{
    var user = await _userRepository.GetByUserId(userId);
    user.FaceUrl = faceUrl;
    user.AvatarUrl = avatarUrl;
    return await _unitOfWork.CommitAsync();
}      

使用ef change tracking的優點是如果屬性的值沒有被改變,就不會觸發資料庫更新操作,缺點是每次更新前都要進行1次查詢操作。

而對于這裡的更新FaceUrl與AvatarUrl的應用場景,更新前就明确知道資料已經改變,直接更新資料庫即可。ef change tracking的更新前查詢不僅沒有必要,而且增加了額外的開銷。

于是,嘗試尋找新的解決方法。

開始嘗試的是EF的DbEntityEntry,未抽象的實作代碼:

public class EfUnitOfWork : DbContext, IUnitOfWork
{
    public async Task<bool> UpdateAsync(User user) 
    {
        base.Set<User>().Attach(user);            
        base.Entry<User>(user).Property(x => x.FaceUrl).IsModified = true;
        base.Entry<User>(user).Property(x => x.AvatarUrl).IsModified = true;
        return (await base.SaveChangesAsync()) > 0;
    }     
}      

調用代碼:

await UpdateAsync(new User { UserId = userId, FaceUrl = faceUrl, AvatarUrl = avatarUrl });      

但是基于這個實作,很難抽象出一個好用的接口方法。

後來突然想到前一段時間在一個項目中用到的EntityFramework.Extended。針對Update操作,它實作了一個優雅的EF擴充,為何不直接用它呢?

//example of using an IQueryable as the filter for the update
var users = context.Users.Where(u => u.FirstName == "firstname");
context.Users.Update(users, u => new User {FirstName = "newfirstname"});      

于是,如獲珍寶地基于EntityFramework.Extended進行實作。

首先在IUnitOfWork中添加一個UpdateAsync()的接口方法:

public interface IUnitOfWork : IDisposable
{
    Task<bool> UpdateAsync<T>(IQueryable<T> source, Expression<Func<T, T>> updateExpression) where T : class;
}      

然後在IUnitOfWork.UpdateAsync()的實作中,調用EntityFramework.Extended的UpdateAsync擴充方法,完成Update操作:

using EntityFramework.Extensions;
public class EfUnitOfWork : DbContext, IUnitOfWork
{  
    async Task<bool> IUnitOfWork.UpdateAsync<T>(IQueryable<T> source, 
        Expression<Func<T, T>> updateExpression)
    {
        return (await source.UpdateAsync<T>(updateExpression)) > 0;
    }     
}      

随之,Application.Services中的實作代碼改為這樣:

public async Task<bool> UpdateFaceAvatar(Guid userId, string faceUrl, string avatarUrl)
{            
    IQueryable<User> userQuery = _userRepository.GetByUserId(userId);
    return await _unitOfWork.UpdateAsync<User>(
        userQuery,
        x => new User { FaceUrl = faceUrl, AvatarUrl = avatarUrl }
        );
}      

(注:這裡的_userRepository.GetByUserId的傳回類型需要改為IQueryable,再一次驗證了Repository傳回IQueryable的必要性,相關博文:開發筆記:用不用UnitOfWork以及Repository傳回什麼集合類型)

跑單元測試驗證一下。

單元測試代碼:

[Fact]
public async Task UpdateFaceAvatar()
{
    var result = await _userService.UpdateFaceAvatar(userId, faceUrl, avatarUrl);
    Assert.True(result);
}      

測試結果:

1 passed, 0 failed, 0 skipped, took 11.38 seconds (xUnit.net 1.9.1 build 1600).      

測試通過!

用SQL Profiler檢視一下資料庫中實際執行的SQL語句:

exec sp_executesql N'UPDATE [dbo].[Users] SET 
[FaceUrl] = @p__update__0, 
[AvatarUrl] = @p__update__1 
FROM [dbo].[Users] AS j0 INNER JOIN (
SELECT 
    1 AS [C1], 
    [Extent1].[UserId] AS [UserId]
    FROM [dbo].[Users] AS [Extent1]
    WHERE [Extent1].[UserId] = @p__linq__0
) AS j1 ON (j0.[UserId] = j1.[UserId])',N'@p__linq__0 uniqueidentifier,@p__update__0 nvarchar(24),@p__update__1 nvarchar(24)',
@p__linq__0='BD42420B-63CF-DD11-9E4D-001CF0CD104B',@p__update__0=N'20150810180742.png',@p__update__1=N'20150810180743.png'      

就是我們期望的SQL語句。

最終驗證了,添加IUnitOfWork.UpadteAsync()接口,基于EntityFramework.Extended,用EF實作資料庫中指定字段的更新,這種方法在實際開發中使用完全可行。

于是,又少了一個使用存儲過程的理由。