基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)_網頁設計公司

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)

上篇文章完成了兩個接口:文章列表頁、文章詳情頁,本篇繼續。

分類列表

分析:這裏多了一個統計文章數量的字段,可以直接新建一個模型QueryCategoryDto.cs繼承CategoryDto

//QueryCategoryDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class QueryCategoryDto : CategoryDto
    {
        /// <summary>
        /// 總數
        /// </summary>
        public int Count { get; set; }
    }
}

添加查詢分類列表接口和緩存接口。

//IBlogService.Category.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog
{
    public partial interface IBlogService
    {
        /// <summary>
        /// 查詢分類列表
        /// </summary>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<QueryCategoryDto>>> QueryCategoriesAsync();
    }
}
//IBlogCacheService.Category.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService
    {
        /// <summary>
        /// 查詢分類列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<QueryCategoryDto>>> QueryCategoriesAsync(Func<Task<ServiceResult<IEnumerable<QueryCategoryDto>>>> factory);
    }
}

分別實現這兩個接口。

//BlogCacheService.Category.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static Meowv.Blog.Domain.Shared.MeowvBlogConsts;

namespace Meowv.Blog.Application.Caching.Blog.Impl
{
    public partial class BlogCacheService
    {
        private const string KEY_QueryCategories = "Blog:Category:QueryCategories";

        /// <summary>
        /// 查詢分類列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<QueryCategoryDto>>> QueryCategoriesAsync(Func<Task<ServiceResult<IEnumerable<QueryCategoryDto>>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_QueryCategories, factory, CacheStrategy.ONE_DAY);
        }
    }
}
//BlogService.Category.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService
    {
        /// <summary>
        /// 查詢分類列表
        /// </summary>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<QueryCategoryDto>>> QueryCategoriesAsync()
        {
            return await _blogCacheService.QueryCategoriesAsync(async () =>
            {
                var result = new ServiceResult<IEnumerable<QueryCategoryDto>>();

                var list = from category in await _categoryRepository.GetListAsync()
                           join posts in await _postRepository.GetListAsync()
                           on category.Id equals posts.CategoryId
                           group category by new
                           {
                               category.CategoryName,
                               category.DisplayName
                           } into g
                           select new QueryCategoryDto
                           {
                               CategoryName = g.Key.CategoryName,
                               DisplayName = g.Key.DisplayName,
                               Count = g.Count()
                           };

                result.IsSuccess(list);
                return result;
            });
        }
    }
}

緩存就不說了,查詢分類列表,聯合查詢文章和分類兩張表,關聯字段為CategoryId,然後分組,計算出對應的數量,在BlogController中添加API。

/// <summary>
/// 查詢分類列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("categories")]
public async Task<ServiceResult<IEnumerable<QueryCategoryDto>>> QueryCategoriesAsync()
{
    return await _blogService.QueryCategoriesAsync();
}

標籤列表

分析:和分類列表差不多,新建模型QueryTagDto.cs繼承TagDto

//QueryTagDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class QueryTagDto : TagDto
    {
        /// <summary>
        /// 總數
        /// </summary>
        public int Count { get; set; }
    }
}

添加查詢標籤列表接口和緩存接口。

//IBlogCacheService.Tag.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService
    {
        /// <summary>
        /// 查詢標籤列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<QueryTagDto>>> QueryTagsAsync(Func<Task<ServiceResult<IEnumerable<QueryTagDto>>>> factory);
    }
}
//IBlogService.Tag.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog
{
    public partial interface IBlogService
    {
        /// <summary>
        /// 查詢標籤列表
        /// </summary>
        /// <returns></returns>
        Task<ServiceResult<IEnumerable<QueryTagDto>>> QueryTagsAsync();
    }
}

分別實現這兩個接口。

//BlogCacheService.Tag.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static Meowv.Blog.Domain.Shared.MeowvBlogConsts;

namespace Meowv.Blog.Application.Caching.Blog.Impl
{
    public partial class BlogCacheService
    {
        private const string KEY_QueryTags = "Blog:Tag:QueryTags";

        /// <summary>
        /// 查詢標籤列表
        /// </summary>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<QueryTagDto>>> QueryTagsAsync(Func<Task<ServiceResult<IEnumerable<QueryTagDto>>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_QueryTags, factory, CacheStrategy.ONE_DAY);
        }
    }
}
//BlogService.Tag.cs
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService
    {
        /// <summary>
        /// 查詢標籤列表
        /// </summary>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<QueryTagDto>>> QueryTagsAsync()
        {
            return await _blogCacheService.QueryTagsAsync(async () =>
            {
                var result = new ServiceResult<IEnumerable<QueryTagDto>>();

                var list = from tags in await _tagRepository.GetListAsync()
                           join post_tags in await _postTagRepository.GetListAsync()
                           on tags.Id equals post_tags.TagId
                           group tags by new
                           {
                               tags.TagName,
                               tags.DisplayName
                           } into g
                           select new QueryTagDto
                           {
                               TagName = g.Key.TagName,
                               DisplayName = g.Key.DisplayName,
                               Count = g.Count()
                           };

                result.IsSuccess(list);
                return result;
            });
        }
    }
}

查詢標籤列表需要聯合查詢tags和post_tags,根據TagId進行關聯,然後分組從而獲取標籤下文章的總數,在BlogController中添加API。

/// <summary>
/// 查詢標籤列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("tags")]
public async Task<ServiceResult<IEnumerable<QueryTagDto>>> QueryTagsAsync()
{
    return await _blogService.QueryTagsAsync();
}

分類名稱&文章列表

分析:此頁面下包含兩個接口,查詢分類的名稱和當前分類下的文章列表,和文章列表不同的是,它不帶分頁。分類包含兩個字段,分類名稱和展示名稱,我們要把真正的名稱查詢出來展示在頁面上。

分類名稱

不需要給他添加返回模型,直接返回一個string類型即可,同時給一個查詢參數name,添加獲取分類名稱接口和緩存接口。

//IBlogService.Category.cs
/// <summary>
/// 獲取分類名稱
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
Task<ServiceResult<string>> GetCategoryAsync(string name);
//IBlogCacheService.Category.cs
/// <summary>
/// 獲取分類名稱
/// </summary>
/// <param name="name"></param>
/// <param name="factory"></param>
/// <returns></returns>
Task<ServiceResult<string>> GetCategoryAsync(string name, Func<Task<ServiceResult<string>>> factory);

實現這兩個接口。

//BlogCacheService.Category.cs
...
    public partial class BlogCacheService
    {
        private const string KEY_GetCategory = "Blog:Category:GetCategory-{0}";

        /// <summary>
        /// 獲取分類名稱
        /// </summary>
        /// <param name="name"></param>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<string>> GetCategoryAsync(string name, Func<Task<ServiceResult<string>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_GetCategory.FormatWith(name), factory, CacheStrategy.ONE_DAY);
        }
    }
...
//BlogService.Category.cs
/// <summary>
/// 獲取分類名稱
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public async Task<ServiceResult<string>> GetCategoryAsync(string name)
{
    return await _blogCacheService.GetCategoryAsync(name, async () =>
    {
        var result = new ServiceResult<string>();

        var category = await _categoryRepository.FindAsync(x => x.DisplayName.Equals(name));
        if (null == category)
        {
            result.IsFailed(ResponseText.WHAT_NOT_EXIST.FormatWith("分類", name));
            return result;
        }

        result.IsSuccess(category.CategoryName);
        return result;
    });
}

FormatWith()是擴展方法,ResponseText.WHAT_NOT_EXIST是之前說過的常量,直接查詢是否存在當前name的分類,如果不存在給出錯誤提示,存在的話,則只返回分類名稱,在BlogController中添加API。

/// <summary>
/// 獲取分類名稱
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[HttpGet]
[Route("category")]
public async Task<ServiceResult<string>> GetCategoryAsync(([Required] string name)
{
    return await _blogService.GetCategoryAsync(name);
}

[Required]Attribute 指定參數name必填。

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

文章列表

通過分類名稱查詢文章列表和分頁查詢文章列表返回模型是一樣的,只是不用分頁,所以直接返回一個列表就可以了,添加通過分類名稱查詢文章列表和緩存的接口。

//IBlogService.Post.cs
/// <summary>
/// 通過分類名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByCategoryAsync(string name);
//IBlogCacheService.Post.cs
/// <summary>
/// 通過分類名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <param name="factory"></param>
/// <returns></returns>
Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByCategoryAsync(string name, Func<Task<ServiceResult<IEnumerable<QueryPostDto>>>> factory);

分別實現這兩個接口。

//BlogCacheService.Post.cs
...
    public partial class BlogCacheService
    {
        private const string KEY_QueryPostsByCategory = "Blog:Post:QueryPostsByCategory-{0}";

        /// <summary>
        /// 通過分類名稱查詢文章列表
        /// </summary>
        /// <param name="name"></param>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByCategoryAsync(string name, Func<Task<ServiceResult<IEnumerable<QueryPostDto>>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_QueryPostsByCategory.FormatWith(name), factory, CacheStrategy.ONE_DAY);
        }
    }
...
//BlogService.Post.cs
/// <summary>
/// 通過分類名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public async Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByCategoryAsync(string name)
{
    return await _blogCacheService.QueryPostsByCategoryAsync(name, async () =>
    {
        var result = new ServiceResult<IEnumerable<QueryPostDto>>();

        var list = (from posts in await _postRepository.GetListAsync()
                    join categories in await _categoryRepository.GetListAsync()
                    on posts.CategoryId equals categories.Id
                    where categories.DisplayName.Equals(name)
                    orderby posts.CreationTime descending
                    select new PostBriefDto
                    {
                        Title = posts.Title,
                        Url = posts.Url,
                        Year = posts.CreationTime.Year,
                        CreationTime = posts.CreationTime.TryToDateTime()
                    })
                   .GroupBy(x => x.Year)
                   .Select(x => new QueryPostDto
                   {
                       Year = x.Key,
                       Posts = x.ToList()
                   });

        result.IsSuccess(list);
        return result;
    });
}

這個邏輯和分頁查詢文章列表是差不多的,聯合查詢文章表和分類表,關聯字段為CategoryId,指定查詢條件categories.DisplayName==name,以CreationTime倒序排序,年份分組,篩選出所需字段返回,在BlogController中添加API。

/// <summary>
/// 通過分類名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[HttpGet]
[Route("posts/category")]
public async Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByCategoryAsync([Required] string name)
{
    return await _blogService.QueryPostsByCategoryAsync(name);
}

標籤名稱&文章列表

分析:此頁面和分類頁一樣,包含兩個接口,查詢標籤的名稱和當前標籤下的文章列表。

標籤名稱

添加獲取標籤名稱接口和緩存接口,GetTagAsync()

//IBlogService.Tag.cs
/// <summary>
/// 獲取標籤名稱
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
Task<ServiceResult<string>> GetTagAsync(string name);
//IBlogCacheService.Tag.cs
/// <summary>
/// 獲取標籤名稱
/// </summary>
/// <param name="name"></param>
/// <param name="factory"></param>
/// <returns></returns>
Task<ServiceResult<string>> GetTagAsync(string name, Func<Task<ServiceResult<string>>> factory);

實現這兩個接口。

//BlogCacheService.Tag.cs
...
    public partial class BlogCacheService
    {
        private const string KEY_GetTag = "Blog:Tag:GetTag-{0}";

        /// <summary>
        /// 獲取標籤名稱
        /// </summary>
        /// <param name="name"></param>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<string>> GetTagAsync(string name, Func<Task<ServiceResult<string>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_GetTag.FormatWith(name), factory, CacheStrategy.ONE_DAY);
        }
    }
...
//BlogService.Tag.cs
/// <summary>
/// 獲取標籤名稱
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public async Task<ServiceResult<string>> GetTagAsync(string name)
{
    return await _blogCacheService.GetTagAsync(name, async () =>
    {
        var result = new ServiceResult<string>();

        var tag = await _tagRepository.FindAsync(x => x.DisplayName.Equals(name));
        if (null == tag)
        {
            result.IsFailed(ResponseText.WHAT_NOT_EXIST.FormatWith("標籤", name));
            return result;
        }

        result.IsSuccess(tag.TagName);
        return result;
    });
}

FormatWith()是擴展方法,ResponseText.WHAT_NOT_EXIST是之前說過的常量,直接查詢是否存在當前name的分類,如果不存在給出錯誤提示,存在的話,則只返回分類名稱,在BlogController中添加API。

/// <summary>
/// 獲取標籤名稱
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[HttpGet]
[Route("tag")]
public async Task<ServiceResult<string>> GetTagAsync(string name)
{
    return await _blogService.GetTagAsync(name);
}

[Required]Attribute 指定參數name必填。

文章列表

和上面一模一樣的,添加通過標籤名稱查詢文章列表接口和緩存接口。

//IBlogService.Post.cs
/// <summary>
/// 通過標籤名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByTagAsync(string name);
//IBlogCacheService.Post.cs
/// <summary>
/// 通過標籤名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <param name="factory"></param>
/// <returns></returns>
Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByTagAsync(string name, Func<Task<ServiceResult<IEnumerable<QueryPostDto>>>> factory);

分別實現這兩個接口。

//BlogCacheService.Post.cs
...
    public partial class BlogCacheService
    {
        private const string KEY_QueryPostsByTag = "Blog:Post:QueryPostsByTag-{0}";

        /// <summary>
        /// 通過標籤名稱查詢文章列表
        /// </summary>
        /// <param name="name"></param>
        /// <param name="factory"></param>
        /// <returns></returns>
        public async Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByTagAsync(string name, Func<Task<ServiceResult<IEnumerable<QueryPostDto>>>> factory)
        {
            return await Cache.GetOrAddAsync(KEY_QueryPostsByTag.FormatWith(name), factory, CacheStrategy.ONE_DAY);
        }
    }
...
//BlogService.Post.cs
/// <summary>
/// 通過標籤名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public async Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByTagAsync(string name)
{
    return await _blogCacheService.QueryPostsByTagAsync(name, async () =>
    {
        var result = new ServiceResult<IEnumerable<QueryPostDto>>();

        var list = (from post_tags in await _postTagRepository.GetListAsync()
                    join tags in await _tagRepository.GetListAsync()
                    on post_tags.TagId equals tags.Id
                    join posts in await _postRepository.GetListAsync()
                    on post_tags.PostId equals posts.Id
                    where tags.DisplayName.Equals(name)
                    orderby posts.CreationTime descending
                    select new PostBriefDto
                    {
                        Title = posts.Title,
                        Url = posts.Url,
                        Year = posts.CreationTime.Year,
                        CreationTime = posts.CreationTime.TryToDateTime()
                    })
                    .GroupBy(x => x.Year)
                    .Select(x => new QueryPostDto
                    {
                        Year = x.Key,
                        Posts = x.ToList()
                    });

        result.IsSuccess(list);
        return result;
    });
}

這個查詢有點特殊,聯合查詢了3張表,先查post_tags和tags,關聯字段TagId,再根據PostId查詢posts,指定查詢條件tags.DisplayName==name,以CreationTime倒序排序,年份分組,篩選出所需字段返回,在BlogController中添加API。

/// <summary>
/// 通過標籤名稱查詢文章列表
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[HttpGet]
[Route("posts/tag")]
public async Task<ServiceResult<IEnumerable<QueryPostDto>>> QueryPostsByTagAsync(string name)
{
    return await _blogService.QueryPostsByTagAsync(name);
}

至此,基本上完成了博客前端所需的所有查詢接口,就還剩下友鏈的查詢,大家可以自己完成,後面如果需要什麼新的接口再回頭來寫就好了。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

搭配下方課程學習更佳 ↓ ↓ ↓

http://gk.link/a/10iQ7

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

“造輪運動”之 ORM框架系列(二)~ 說說我心目中的ORM框架,“造輪運動”之 ORM框架系列(一)~談談我在實際業務中的增刪改查_租車

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

ORM概念解析

   首先梳理一下ORM的概念,ORM的全拼是Object Relation Mapping (對象關係映射),其中Object就是面向對象語言中的對象,本文使用的是c#語言,所以就是.net對象;Relation Mapping則是關係映射,在數據庫的系統中指的就是.net對象與數據庫表之間的關係映射。

  為什麼.net對象與數據庫表之間要進行關係映射呢?

  答案當然是顯而易見的,因為數據庫表的結構與對象的結構非常的相似,如果將兩者之間建立起映射關係,我們就可以很容易地在面向對象語言中像操作普通對象一樣去進行數據庫表的操作,那對程序員來說簡直就是福音。那麼這兩者之間的關係由誰來建立呢,畢竟不能讓它倆憑空去產生關係,當中要有“媒人”撮合,這個“媒人”就叫做ORM框架,也可以叫做數據庫持久層框架。

  在上一篇 “造輪運動”之 ORM框架系列(一)~談談我在實際業務中的增刪改查 中,談了一下我遇到的增刪改查,並分析了原生sql、Lambda to sql、存儲過程等方式的利弊,在這其中我也慢慢地形成了自己對ORM框架的認識,也逐漸繪製出了一幅我心目中完美的ORM框架藍圖。

  描繪藍圖之前先給我的ORM框架取個名字,叫做CoffeeSQL,這個名字的由來是因為我希望自己用了這個框架以後可以給我節省出工作中能享受一杯Coffee的時間~

 

 

1、實體映射

   c#的對象想要與數據庫表建立映射關係,那就要記錄映射關係的配置信息,我更喜歡採用一種一目瞭然的方式來進行配置:直接在類的字段上標識Attribute。

  基本使用方式如下代碼所示:

 1  /// <summary>
 2  /// 實體類
 3  /// </summary>
 4  [Table("T_Students")]
 5  public class Student : EntityBase
 6  {
 7      [PrimaryKey]
 8      [Column]
 9      public string Id { get; set; }
10  
11      [Column("Name")]
12      public string StudentName { get; set; }
13  
14      [Column]
15      public int Age { get; set; }
16  }

  我們可以看到在這個實體類中我們使用了三個Attribute,分別為 TableAttribute(標識映射表)、 PrimaryKeyAttribute(標識主鍵)、CloumnAttribute(標識數據列),相信這些都很好理解。

 

2、lambda操作,增刪改查,強類型

   在一些最基礎的信息增刪改查功能中,對於單表的sql操作是非常頻繁的,我建議使用lambda的形式操作,第一是因為方便,第二是因為單表操作Lambda TO SQL轉化后的sql是最簡便的,所以不用擔心它的性能。

  具體的代碼操作如下:

  1)

1  //Add
2  dbContext.Add<Student>(new Student { Id = newId, StudentName = "王二", Age = 20 });

   2)

1  //delete
2  dbContext.Delete<Student>(s => s.Id.Equals(newId));

   3)

1  //update
2  dbContext.Update<Student>(s => new { s.StudentName }, new Student { StudentName = name })  //更新字段
3           .Where(s => s.Id.Equals(newId))                                                   //where條件
4           .Done();

   4)

1  //select
2  dbContext.Queryable<Student>()
3           .Select(s => new { s.StudentName, s.Id })             //字段查詢
4           .Where(s => s.Age > 10 && s.StudentName.Equals(name)) //where條件
5           .Paging(1, 10)                                        //分頁查詢
6           .ToList();

 

3、原生sql操作,弱類型,非實體類字段的存儲的方式 => 索引器

   上面介紹的lambda表達式的操作方式只適用於單表的查詢,如果需要進行聯多表查詢或者需要進行更複雜的查詢,那麼我更傾向於進行原生sql的查詢。當然,原生sql的查詢也提供結果映射到對象的功能,類似於Dapper的用法。

  具體操作如下:

1  //原生sql查詢用法
2  string complexSQL = "select xxx from t_xxx A,t_yyy B where xx = {0} and yy = {1}";
3  object[] sqlParams = new Object[2] { DateTime.Now, 2 };
4  var resList = dbContext.Queryable(complexSQL, sqlParams).ToList<Student>();

   對上面的代碼稍微進行一下解釋,complexSQL 中的 {0}{1} 是參數化查詢參數的佔位符,在原生sql的查詢用法中你可以使用任意c#的基本變量去填充佔位符,最終獲得sql參數化查詢的效果,用法非常方便。

  當然你可能會困惑:假如我查詢的結果中包含了不是Student對象中字段的值怎麼辦?

  答案是,你可以用索引的方式將非Student對象中字段的值取出,下面做一個示範,假如你希望查出 xxx 字段,你可以這樣做:

1  Student resList1 = resList[0];
2  var segmentValue = (string)resList1["xxx"];  //查詢結果中的任何字段值都可以以索引的方式查出,記得轉換為字段的實際類型

   是不是So Easy!

 

4、更複雜的sql邏輯,那得用存儲過程

   製造生產類的企業的業務邏輯離不開流程加表單,表單的查詢與數據寫入就是屬於比較複雜的sql了。

  但是這其實還不算什麼,更複雜的是,由於現在流行的大數據概念,車間的主管們往往也想沾沾邊,所以會在車間的大屏上展現各種數據統計的看板,這其中就涉及到巨多表的查詢邏輯。曾經開發過一個看板功能,特地數了一下,單單一條sql就接近200行的代碼量,一點都不誇張。像這種邏輯很複雜的查詢,還可能涉及到依據不同條件執行不同sql的場景,我當然不會在高級語言中拼接原生sql了,因為那樣容易把自己搞暈哦。這個時候就要祭出最後的大殺技——存儲過程。

  在CoffeeSQL中你可以這樣使用存儲過程:

 1     using (ShowYouDbContext dbContext = new ShowYouDbContext())
 2     {
 3         string strsp = "xxx_storeprocedureName";
 4 
 5         OracleParameter[] paras = new OracleParameter[]
 6         {
 7             new OracleParameter("V_DATE",OracleDbType.Varchar2,50),
 8             new OracleParameter("V_SITE",OracleDbType.Varchar2,50),
 9             new OracleParameter("V_SCREEN",OracleDbType.Varchar2,50),
10             new OracleParameter("V_SHIFT",OracleDbType.Varchar2,50),
11             new OracleParameter("V_CURSOR",OracleDbType.RefCursor)
13 }; 14 15 paras[0].Value = info.date; 16 paras[1].Value = info.site; 17 paras[2].Value = info.screenKey; 18 paras[3].Value = info.shift; 19 paras[4].Direction = ParameterDirection.Output;21 22 DataSet ds = dbContext.StoredProcedureQueryable(strsp, paras).ToDataSet(); 23 24 return ds; 25 }

  這裏的用法並沒有做什麼包裝,值得一提的就是可以將查詢結果轉換為對象。當然,這是貫穿整個ORM框架的一大主要功能,無處不在。

 

5、數據庫連接管理,一主多從

  現如今大多數的數據庫都不是單一部署的,比較流行的數據庫部署方式是“一主多從”的方式,即一台數據庫作為寫入數據的數據庫(主庫),其他多台數據庫作為讀取數據的數據庫(從庫)去同步主庫的數據,從而實現了讀寫分離的功能,降低了主庫的訪問壓力,大大提高了數據庫的訪問性能。當然,我們現在所討論的這個ORM框架就理所當然地要支持這種“一主多從”的數據庫部署方式的數據庫操作。

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

  具體“一主多從”的數據庫連接配置方式如下:

 1     public class ShowYouDbContext : OracleDbContext<ShowYouDbContext>
 2     {
 3         private static string writeConnStr = "xxxxxxx_1";
 4         private static string[] readConnStrs = new string[] {
 5 
 6             "xxxxxxxxx_2",
 7             "xxxxxxxxx_3",
 8             "xxxxxxxxx_4",
 9             "xxxxxxxxx_5",
10             "xxxxxxxxx_6"
11         };
12 
13         public ShowYouDbContext() : base(writeConnStr, readConnStrs)
14         {
15         }
16     }

  對DBContext類對象進行構造時將數據庫連接字符串當做構造參數傳入即可,第一條為主庫(寫數據庫)連接字符串,以後的都為從庫(讀數據庫)連接字符串。 

 

6、實體數據驗證,作為數據格式規範的防線

  實體數據驗證的概念很好理解:一個數據表中的每一個字段都有其長度、大小等限制,那麼每一個與數據表對應的實體也應當有相應的字段約束來作為數據格式規範的防線,讓持久化到數據表中的數據符合其所規定的的格式,也就相當於貨物在進入倉庫前做一個安全檢查。

  我們可以像這樣去配置一個實體類的數據驗證規則:

 1     [Table("B_DICTIONARY")]
 2     public class Dictionary : EntityCommon
 3     {
 4         [Length(1,50)]
 5         [Column]
 6         public string Name { get; set; }
 7         [Column]
 8         public string Value { get; set; }
 9         [Column]
10         public string Type { get; set; }
11     }

  這個實體類中的Name字段就標識了一個LengthAttribute標籤,規定了該字段的長度範圍為1~50,如果不符合則會拋出異常。當然,實體數據的驗證規則不止這一條,後期會根據需求再進行添加,或者用戶可以根據自己的需求進行擴展。

 

7、事務的操作形式,個人習慣,喜歡把transaction明確寫出來,不喜歡過度封裝

  事務是數據庫系統中的重要概念,事務的特性是ACID(原子性、一致性、隔離性、持久性),當然這裏不會去討論事務的概念,我要展示的是在CoffeeSql中如何使用事務:

 1     try
 2     {
 3         dbContext.DBTransaction.Begin();
 4 
 5         dbContext.Update<Machine_Match_Relation>(d => new { d.Del_Flag }, new Machine_Match_Relation { Del_Flag = 1 })
            .Where(d => d.Screen_Machine_Id.Equals(displayDeviceId)).Done(); 6 7 foreach(string bindDeviceId in bindDeviceIds) 8 { 9 dbContext.Add(new Machine_Match_Relation 10 { 11 Id = Utils.GetGuidStr(), 12 Screen_Machine_Id = displayDeviceId, 13 Machine_Id = bindDeviceId, 14 Creater = updater 15 }); 16 } 17 18 dbContext.DBTransaction.Commit(); 19 } 20 catch(Exception ex) 21 { 22 dbContext.DBTransaction.Rollback(); 23 throw ex; 24 }

  有人會說為什麼這裏不封裝一個方法,只要傳入業務操作代碼的委託Action就行了,當然可以,但是說實在的,真沒必要,如果你喜歡就自己封裝去吧。

 

8、可適配擴展多款不同的數據庫 

  作為一個能跟的上潮流的ORM,當然得具備適配多種數據庫的特性了,Oracle、Mysql、SqlServer等等數據庫,想怎麼適配就怎麼適配。

 1     //Mysql
 2     public class WQSDbContext : MysqlDbContext<WQSDbContext>
 3     {
 4         public WQSDbContext() : base(mysqlConnStr)
 5         {
 6             this.OpenQueryCache = false;
 7             this.Log = context =>
 8             {
 9                 Console.WriteLine($"sql:{context.SqlStatement}");
10                 Console.WriteLine($"time:{DateTime.Now}");
11             };
12         }
13     }
14 
15     //Oracle
16     public class WQSDbContext : OracleDbContext<WQSDbContext>
17     {
18         public WQSDbContext() : base(oracleConnStr)
19         {
20             this.OpenQueryCache = false;
21             this.Log = context =>
22             {
23                 Console.WriteLine($"sql:{context.SqlStatement}");
24                 Console.WriteLine($"time:{DateTime.Now}");
25             };
26         }
27     }

   目前CoffeeSQL實現了兩種數據庫的擴展,Oracle與Mysql。當然,如果你還想適配更多的數據庫的話,你也可以嘗試擴展,只不過我目前用到的這兩種數據庫。

 

9、緩存功能,提高性能

  還記得當初一次校招的面試,面試官問我:“你覺得ORM的速度快還是Ado.net的速度快?”

     我傻傻地脫口而出:“那當然是Ado.net更快了,相比於ORM,它少了linq語句到sql的轉換步驟,而且ORM還比直接使用Ado.net多了查詢結果映射到對象的步驟。”

  看面像和發量就知道這個面試官 心()地()善()良(,他和我對視了3秒,然後說:

  “好,今天就到這,回去等通知吧!”

   等通知的後果那也就顯而易見了……

  

  直到後來我深入地了解了ORM框架的原理才知道,那個問題並不是我回答的一句話那麼簡單,因為我忽略了ORM緩存

   CoffeeSql中也會有ORM緩存,ORM的緩存分為表緩存(二級緩存)sql語句緩存(一級緩存):表緩存一般用於數據量較小且經常會進行查詢操作的表,會將整個表的數據緩存到緩存介質中;sql語句緩存可以適用各種類型的表,它是以sql查詢語句作為緩存鍵的。

  當然,你如果並不想要知道太多其中的細節,在使用時你只需要像這樣簡單的配置:

  (ORM緩存開關)

1     public class WQSDbContext : OracleDbContext<WQSDbContext>
2     {
3         public WQSDbContext() : base(oracleConnStr)
4         {
5             this.OpenTableCache = true;
6             this.OpenQueryCache = true;
7         }
8     }

  (表緩存的實體配置)

 1     [TableCaching]
 2     [Table("B_DICTIONARY")]
 3     public class Dictionary : EntityCommon
 4     {
 5         [Column]
 6         public string Name { get; set; }
 7         [Column]
 8         public string Value { get; set; }
 9         [Column]
10         public string Type { get; set; }
11     }

   如果在實體類上標記TableCachingAttribute而且打開了表緩存,那麼就會對當前的數據表進行全表數據的緩存。

  要注意,表緩存一般只用在小數據量且查詢頻繁的表中。因為表緩存功能啟用後會事先去將全表的數據掃描然後存儲到本地緩存,直接在緩存中進行表數據的查詢操作,所以如果你將那種數據量很大且查詢並不是很頻繁的表開啟了表緩存,那你可以想象一下這個肯定會是一個得不償失的行為。

 

   以上的代碼操作是我當初對CoffeeSQL的預想,當然,現在都成為了現實。以上的內容實際上就相當於CoffeeSQL的操作手冊,因為上面的代碼完全是按照實際的CoffeeSQL的框架操作來進行展示的。

     有關於CoffeeSQL更詳細的使用細節我會在源碼的測試代碼中給出,觀眾們可以移步去看源碼:

   https://gitee.com/xiaosen123/CoffeeSqlORM

  本文為作者原創,轉載請註明出處:https://www.cnblogs.com/MaMaNongNong/p/12896757.html

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

Asp.net MVC Razor視圖模版動態渲染PDF,Razor模版生成靜態Html_包裝設計

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

1.前言

 

    上一篇文章我開源了輪子,Asp.net Core 3.1 Razor視圖模版動態渲染PDF,然後,很多小夥伴有很多私信找我了。那麼我下面就簡單的給大家說一下,關於小夥伴問的這些問題。

  • 我項目的电子簽章部分代碼可否開源?

  答:我項目电子簽章也是使用第三方的电子簽章,电子簽章並不是自己實現的,項目裏面的电子簽章代碼無非也是對接第三方的接口。這部分代碼開源出去也沒有什麼意義。我們是使用数字廣東的方案,如果您也是使用該数字簽章,可以私下溝通我看看能不能幫助您。

  • 电子簽章實現難不難,怎麼實現自己的电子簽章?

  答:电子簽章要實現,估計不是太難,按照我的理解,當然我沒有具體深入研究(如果這裏我有妄自菲薄的意思,請諒解,畢竟我能力有限,只是按照我的理解來分析),我個人覺得电子簽章應該就是利用数字證書給PDF簽名,然後加密保護文檔,然後校驗文檔的真偽,就要考慮怎麼驗證這個文檔沒有被刪改,是當初我們簽章的這個文檔,而且這個簽名不能被偽造。個人覺得不是很複雜,但是,电子簽章的法律有效性卻不是這麼簡單的。按照國家法律規定,利用的簽名平台應該有資質的,國家認可的第三方簽章平台,也就是說,私人自己製作的簽章,打起官司來,很難得到法律支持。

  • 項目為什麼CSS樣式不起效?

  答:你是否使用了外鏈的CSS樣式,因為渲染Razor視圖是在後台渲染,無法找到外鏈的文件路徑,就使用不了外鏈的CSS樣式,內嵌和內聯CSS樣式都沒啥問題的。

  • 用word或者excel模版他不香嗎?為什麼要搞個這個東西?

  答:無非是多一個方案,具體你使用什麼完全是你自己說了算,你覺得其他方案好就用,你覺得本方案能幫助你就用,不好就不用,我又不收你半毛線,還是想說的是,你其他的方案,能有用CSS那麼容易做出來漂亮的表單效果嗎?

  • 圖片支持嗎?

  答:圖片要轉Base64編碼,不支持外連接圖片。

  • 前端預覽PDF用什麼插件?

  答:我目前不用插件,新一代瀏覽器都支持PDF直接預覽,直接就能渲染成PDF呈現,當然你也可以自己集成PDF.JS.我大概看了下,集成也很方便。我之所以不集成,是考慮到我的項目有可能使用IE低版本的情況,PDF.JS可能不支持。所以乾脆直接把PDF流推送給瀏覽器,瀏覽器要是能預覽,就直接呈現,不能預覽就下載。

  • 項目支持net Framework嗎?為啥報錯?

  答:這個問題問的有點多,所以本文後續就以這個再說一下本輪子在net45下的使用。還是那句話,有不對的,歡迎您指正,覺得對你有用的就用,無用的就直接忽視,我又不收你半毛線。

  • 可以用本項目生成靜態Html嗎?容易被搜索引擎抓取。

  答:可以,後面演示

 

  2.依賴項目

  <PackageReference Include=”TuesPechkin” Version=”2.1.1″ />
  <PackageReference Include=”TuesPechkin.Wkhtmltox.Win32″ Version=”0.12.2.1″ />
  <PackageReference Include=”RazorEngine” Version=”3.9.3″ />
  <PackageReference Include=”System.ComponentModel.Annotations” Version=”4.5.0″ />
  <PackageReference Include=”Microsoft.AspNet.Mvc” Version=”5.2.3″ />
  <PackageReference Include=”Nito.AsyncEx” Version=”4.0.1″ />
  <PackageReference Include=”Newtonsoft.Json” Version=”12.0.3″ />
  <Reference Include=”Nito.AsyncEx.Concurrent” Version=”4.0.1″ />
  <Reference Include=”Nito.AsyncEx.Enlightenment” Version=”4.0.1″ />

 

     3.核心代碼

    TuesPechkin插件,首先說一下這個TuesPechkin插件,他其實是利用TuesPechkin.Wkhtmltox進程來轉換的。這個插件使用還是要小心的,使用不當可能有線程安全問題,會使得當前工作進程掛起。在IIS下面使用也要注意使用32位的插件。具體使用請看作者的說明:https://github.com/tuespetre/TuesPechkin/blob/develop/README.md

 

 

 

 插件初始化代碼:

 

  private static readonly IConverter PdfConverter = new ThreadSafeConverter(new RemotingToolset<PdfToolset>(
            new Win32EmbeddedDeployment(
                new TempFolderDeployment())));

  

 切記IIS和多線程一定要靜態單例。使用

 Win32EmbeddedDeployment

ThreadSafeConverter
這兩個類。其他的可能讓你進程掛起的成為可能。
還要用到遠程工具集的PDF工具集。


Razor 轉Html代碼,主要有兩種方式:


第一種使用RazorEngine來轉換:這個主要是傳遞Razor模版進去,轉換。

   protected string RunCompileRazorTemplate(object model,string razorTemplateStr)
        {
            if(string.IsNullOrWhiteSpace(razorTemplateStr)) throw new ArgumentException("Razor模版不能為空"); var htmlString= Engine.Razor.RunCompile(razorTemplateStr, razorTemplateStr.GetHashCode().ToString(), null, model); return htmlString; }

 

第二種使用ViewEngine。這個主要是自動查找Asp.net MVC裏面的View下面的Razor,目前我們項目就是使用這個。

 

var viewName = context.RouteData.Values["action"].ToString();
            var result = ViewEngines.Engines.FindView(context, viewName, null); IExportPdfByHtmlTemplate exportPdfByHtmlTemplate = new PdfByHtmlTemplateExporter (); #endif if (result.View == null) throw new ArgumentException($"名稱為:{viewName}的視圖不存在,請檢查!"); context.HttpContext.Response.ContentType = "application/pdf"; //context.HttpContext.Response.Headers.Add("Content-Disposition", "attachment; filename=test.pdf"); var html = ""; using (var stringWriter = new StringWriter()) { #if !NET45 var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = Value }; var viewContext = new ViewContext(context, result.View, viewDictionary, new TempDataDictionary(context.HttpContext, tempDataProvider), stringWriter, new HtmlHelperOptions()); await result.View.RenderAsync(viewContext); #else var viewDictionary = new ViewDataDictionary(new ModelStateDictionary()) { Model = Value }; var viewContext = new ViewContext(context, result.View, viewDictionary, context.Controller.TempData, stringWriter); result.View.Render(viewContext, stringWriter); result.ViewEngine.ReleaseView(context, result.View); #endif html = stringWriter.ToString(); }

 

推送PDF流給客戶端,預覽PDF主要代碼

 

          context.HttpContext.Response.ContentType = "application/pdf";
    context.HttpContext.Response.AddHeader("Content-Length", buff.Length.ToString());
            context.HttpContext.Response.AddHeader("Content-Disposition", "filename=电子簽章PDF-"+DateTime.Now.ToString()+".pdf");
            context.HttpContext.Response.BinaryWrite(buff);
            context.HttpContext.Response.Flush();
            context.HttpContext.Response.Close();
            context.HttpContext.Response.End();

  

 這裏要注意三個地方,不然一定會踩坑。

Content-Length要設置,不然谷歌瀏覽器可能無法下載預覽的PDF。
Content-Disposition不能要attachment,否則可能直接下載不是預覽。
ContentType 要設置"application/pdf"


Razor轉靜態Html

還有一部分人問我怎麼利用本插件Razor模版動態生成靜態Html,這樣容易被百度爬蟲錄取。
其實這部分核心代碼就是幾句代碼,非常簡單。本項目直接用下面接口即可生成html字符轉,自行保存就可以了。

 public interface IHtmlByRazorTemplateExporter
    {
        Task<string> ExportHtmlByRazorTemplateAsync<T>(T data, string htmlTemplate) where T : class;
        string ExportHtmlByRazorTemplate<T>(T data, string htmlTemplate) where T : class;
    }

  

核心:

 

   protected string RunCompileRazorTemplate(object model,string razorTemplateStr)
        {
            if(string.IsNullOrWhiteSpace(razorTemplateStr)) throw new ArgumentException("Razor模版不能為空"); var htmlString= Engine.Razor.RunCompile(razorTemplateStr, razorTemplateStr.GetHashCode().ToString(), null, model); return htmlString; }

 

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

  4.使用方式

  •  目前本項目已經打包成nuget,並上傳,使用可以直接項目右鍵->管理NuGet程序包,查找,然後下載安裝。

 

  •  也可以使用命令安裝。install-package JESAI.HtmlTemplate.Pdf.net45

 

  • 或者直接fork本倉庫自己打包,並根據自己情況修改使用。

 

自定打包可以修改項目目標框架。項目右鍵->屬性->應用程序,目標框架,修改

 

 

 

如發現不能修改,可以,項目->右鍵->編輯項目文件

 

 

 

然後編譯,就可以使用了。

 

具體使用

方式一:

 

 

 方式二:

 

 

 

 

Razor視圖模版代碼:

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>
    <meta charset="utf-8" />
    <title></title>
</head>

<body>
    <table border="1" style="width:800px;height:500px;">
        <tr>
            <td>姓名</td>
            <td>@Model.Name</td>
            <td>性別</td>
            <td>@Model.Sex</td>
        </tr>
        <tr>
            <td>年齡</td>
            <td>@Model.Age</td>
            <td>班級</td>
            <td>@Model.Class</td>
        </tr>
        <tr>
            <td>住址</td>
            <td>@Model.Address</td>
            <td>電話</td>
            <td>@Model.Tel</td>
        </tr>
        <tr>
            <td clospan="2">住址</td>
            <td>@Model.Des</td>
        </tr>
    </table>
</body>
</html>

 

 

 

  5.運行效果

 

 

 

  6.項目代碼

 代碼託管:https://gitee.com/Jesai/JESAI.HtmlTemplate.Pdf

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

Spring AOP學習筆記01:AOP概述_台中搬家

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

1. AOP概述

  軟件開發一直在尋求更加高效、更易維護甚至更易擴展的方式。為了提高開發效率,我們對開發使用的語言進行抽象,走過了從彙編時代到現在各種高級語言繁盛之時期;為了便於維護和擴展,我們對某些相同的功能進行歸類並使之模塊化,衝出了最初的”原始部落”,走過了從過程化編程到面向對象編程(OOP)的”短暫而漫長”的歷程。但不管走過的路有多長,多麼坎坷,我們一直沒有停止尋找更加完美、更加高效的軟件開發方法,過去如此,現在亦然。

  當OOP被提出來,以取代過去基於過程化編程的開發方法時,或許那個時代的人都會以為,面向對象編程和面向對象的軟件開發就是我們一直追求的那顆能夠搞定一切的”銀彈”。但不得不承認的是,即使面向對象的軟件開發模式,依然不能很好地解決軟件開發中的所有問題。

  軟件開發的目的,最終是為了解決各種需求,包括業務需求和系統需求。使用面向對象方法,我們可以對業務需求等普通關注點進行很好的抽象和封裝,並且使之模塊化。但對於系統需求(比如日誌記錄、權限驗證、事務管理等)一類的關注點來說,情況卻有所不同。

  對於業務需求而言,需求與其具體實現之間的關係基本上是一對一的。我們可以在系統中某一個確定的點找到針對這種需求的實現,無論從開發還是維護的角度,都比較方便。比如電商系統中的賬戶管理模塊、訂單模塊、支付模塊等,可以很容易地按照功能劃分模塊並完成開發。

  但是,事情並沒有結束!開發中為了調試或在進入生產環境後為了對系統進行監控,我們需要為這些業務需求的實現對象添加日誌記錄功能;或者,業務方法的執行需要一定的權限限制,那麼方法執行前肯定需要有相應的安全檢查功能。而這些則屬於系統需求的範疇。雖然需求都很明確(加入日誌記錄、加入安全檢查),但是要將這些需求以面向對象的方式實現並集成到整個的系統中去,可就不是一個需求對應一個實現那麼簡單了,系統中的每個業務對象都需要加入日誌記錄,加入相應的安全檢查,那麼,這些需求的實現代碼就會遍及所有業務對象。

  對於系統中普通的業務關注點,OOP可以很好地對其進行分解並使之模塊化,但卻無法更好地避免類似於系統需求的實現在系統中各處散落這樣的問題。所以,我們要尋求一種更好的方法,它可以在OOP的基礎上更上一層樓,提出一套全新的方法論來避免以上問題,也可以提供某種方法對基於OOP的開發模式做一個補足,幫助OOP以更好的方式解決以上問題。迄今為止,我們還找不到比OOP更加有效的軟件開發模式。不過,我們找到了後者,那就是AOP,對OOP的補足。

  AOP全稱為Aspect-Oriented Programming,中文通常翻譯為面向方面編程。使用AOP,我們可以對類似於Logging和Security等系統需求進行模塊化的組織,簡化系統需求與實現之間的對比關係,進而使得整個系統的實現更具模塊化。

  對於一個軟件系統而言,日誌記錄、安全檢查、事務管理等系統需求就像一把把刀“惡狠狠”地橫切到我們組織良好的各個業務功能模塊之上。以AOP的行話來說,這些系統需求是系統中的橫切關注點(cross-cutting concern)。使用傳統方法,我們無法更好地以模塊化的方式,對這些橫切關注點進行組織和實現。所以AOP引入了Aspect的概念,用來以模塊化的形式對系統中的橫切關注點進行封裝。Aspect 之對於AOP,就相當於Class之對於OOP。我們說過AOP僅是對OOP方法的一種補足,當我們把以Class形式模塊化的業務需求和以Aspect形式模塊化的系統需求拼裝到一起的時候,整個系統就算完成了。

 

2. AOP相關概念

  在進一步學習Spring AOP之前,我們還需要了解一下AOP涉及的相關概念:

2.1 切點(JoinPoint)

  在系統運行之前,AOP的功能模塊都需要織入到OOP的功能模塊中。所以,要進行這種織入過程,我們需要知道在系統的哪些執行點上進行織入操作,這些將要在其之上進行織入操作的系統執行點就稱之為切點(Joinpoint)。對應到spring中可以理解為具體攔截的某個業務點。

  以下是一些較為常見的Joinpoint類型

  • 方法調用(Method Call)。當某個方法被調用的時候所處的程序執行點。
  • 方法調用執行(Method Call execution)。也可以稱之為方法執行,該Joinpoint類型代表的是某個方法內部執行開始時點,這需要與上面的方法調用類型的Jointpoint進行區分。方法調用(method call)是在調用對象上的執行點,而方法執行(method execution)則是在被調用到的方法邏輯執行的時點,對於同一對象,方法調用要先於方法執行。
  • 構造方法調用(Constructor Call)。程序執行過程中對某個對象調用其構造方法進行初始化的時點。
  • 構造方法執行(Constructor Call Execution)。構造方法執行和構造方法調用之間的關係類似於方法執行和方法調用之間的關係,指的是某個對象構造方法內部執行的開始時點。
  • 字段設置(Field Set)。對象的某個屬性通過setter方法被設置或者直接被設置的時點。
  • 字段獲取(Field Get)。對象的某個屬性通過getter方法獲取或者直接訪問的時點。
  • 異常處理(Exception Handler Execution)。在某些類型異常拋出后,對應的異常處理邏輯執行的時點。
  • 類初始化(Class initialization)。類中某些靜態類型或者靜態塊的初始化時點。

  基本上程序執行過程中你認為必要的執行時點都可以作為Joinpoint,但是對於一些位置,具體的AOP實現產品在捕捉的時候可能存在一定的困難,或者能夠實現但付出太多卻可能收效甚微。在Spring AOP中最常見的就是前面的方法執行類型的Joinpoint。

2.2 切面(Pointcut)

  Pointcut概念代表的是JointPoint的表述方式。將橫切邏輯織入當前系統的過程中,需要參照Pointcut規定的Jointpoint信息,才可以知道應該往系統的哪些Joinpoint上織入橫切邏輯。

一個Pointcut可以指定系統中符合條件的一組Joinpoint,但是其是如何來指定的呢?通常有如下幾種方式:

  • 直接指定Joinpoint所在方法名稱。這種形式的Pointcut表述方式比較簡單,而且功能單一,通常只限於支持方法級別Joinpoint的AOP框架。並且這種方式只能一個一個指定,所以通常只限於Joinpoint較少且較為簡單的情況。

  • 正則表達式。這是比較普遍的Pointcut表達方式,可以充分利用正則表達式的強大功能來歸納表述符合某種條件的多組Joinpoint。幾乎現在大部分的Java平台的AOP產品都支持這種形式的Pointcut表達形式,包括Jboss AOP、Spring AOP以及AspectWerkz等。

  • 使用特定的Pointcut表述語言。這是一種最為強大的表達Pointcut的方式,很靈活,但具體實現起來可能會很複雜,需要設計該表述語言的語法,實現相應的解釋器等許多工作。AspectJ使用這種方式來指定Pointcut,它提供了一種類似於正則表達式的針對Pointcut的表述語言,在表達Pointcut方面支持比較完善,而且Spring 2.0之後也是支持這種方式。

2.3 通知(Advice)

  Advice是單一橫切關注點邏輯的載體,它代表將會織入到Joinpoint的橫切邏輯。如果將Aspect比作OOP中的Class,那麼Advice就相當於Class中的Method。

  按照Advice在Jointpoint位置執行時機的差異或者完成功能的不同,Advice可以分成多種具體形式。

  • Before Advice

  Before Advice是在Joinpoint指定位置之前執行的Advice類型。通常,它不會中斷程序執行流程,但如果必要,可以通過在Before Advice中拋出異常的方式來中斷當前程序流程。如果當前Before Advice將被織入到方法執行類型的Joinpoint,那麼這個Before Advice就會先於方法執行而執行。   通常,可以使用Before Advice做一些系統的初始化工作,比如設置系統初始值,獲取必要系統資源。

  • After Advice

  顧名思義,After Advice就是在相應連接點之後執行的Advice類型,但該類型的Advice還可以細分為三種:

  After returning Advice。只有當前Joinpoint處執行流程正常完成后,After returning Advice才會執行。

  After throwing Advice。又稱Throws Advice,只有在當前Joinpoint執行過程中拋出異常的情況下,才會執行。比如某個方法執行類型的Joinpoint拋出某異常而沒有正常返回。

  After Advice。或許叫After (Finally) Advice更為確切,該類型Advice不管Joinpoint處執行流程是正常終了還是拋出異常都會執行,就好像Java中的finally塊一樣。

  • Around Advice

  Around Advice對附加其上的Joinpoint進行”包裹”,可以在Joinpoint之前和之後都指定相應的邏輯,甚至於中斷或者忽略Joinpoint處原來程序流程的執行。

2.4 Aspect

  Aspect是對系統中的橫切關注點邏輯進行模塊化封裝的AOP概念實體,可以理解為攔截器類,其中會定義切點以及攔截處理邏輯。通常情況下,Aspect可以包含多個Pointcut以及相關Advice定義。在Spring中,是通過使用@AspectJ註解並結合普通POJO來聲明Aspect的。

@AspectJ
public class AspectClass{
    // pointcut 定義

    // advice 定義
}

2.5 目標對象

  符合Pointcut所指定的條件,將在織入過程中被織入橫切邏輯的對象,稱為目標對象(Target Object)。

 

3. Spring AOP

  AOP只是一種理念,要實現這種理念,通常需要一種現實的方式。Spring AOP就是一款AOP的實現產品,Spring AOP是Spring核心框架的重要組成部分,通常認為它與Spring的IoC容器以及Spring框架對其他JavaEE服務的集成共同組成了Spring框架的”質量三角”,足見其地位之重要。

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

  在Java語言的基礎之上,Spring AOP對AOP的概念進行了適當的抽象和實現,使得每個AOP的概念都可以落到實處,在詳細學習Spring AOP概念實體之前,我們有必要先看一下其是如何運作的。

  Spring AOP從最初發布以來,一直延續了最初的設計,也就是採用動態代理機制和字節碼生成技術來實現基於Java語言的簡單而強大的AOP框架。與最初的AspectJ採用編譯器將橫切邏輯織入目標對象不同,動態代理機制和字節碼生成都是在運行期間為目標對象生成一個代理對象,再將橫切邏輯織入到這個代理對象中,系統最終使用的是織入了橫切邏輯的代理對象,而不是真正的目標對象。

  要理解這種差別以及最終可以達到的效果,有必要先從動態代理機制的根源–代理模式(Proxy Pattern)開始說起。。。

 

3.1 設計模式之代理模式

  說到代理,舉幾個簡單的例子,比如房地產中介就是一種代理,我們偶爾使用的網絡代理也是一種代理,類似例子很多,就不一一列舉了。代理處於訪問者與被訪問者之間,可以隔離這兩者之間的直接交互,訪問者與代理打交道就好像在跟被訪問者在打交道一樣,因為代理通常幾乎會全權擁有被代理者的職能,代理能夠處理的訪問請求就不必要勞煩被訪問者來處理了。從這個角度來講,有兩個好處:

  • 代理可以減少被訪問者的負擔;
  • 即使代理最終要將訪問請求轉發給真正的被訪問者,它也可以在轉發訪問請求之前或者之後加入特定的邏輯,比如安全訪問限制;

  在軟件系統中,代理機制的實現有現成的設計模式支持,即代理模式。在代理模式中通常涉及4種角色:

  • ISubject。該接口是對被訪問者或者被訪問資源的抽象。在嚴格的設計模式中,這樣的抽象接口是必須的。
  • SubjectImpl。這是被訪問者或者被訪問資源的具體實現類。如果你要訪問某位明星,那麼SubjectImpl就是你想要訪問的明星;如果你想要買房子,那麼SubjectImpl就是房主。
  • SubjectProxy。這是被訪問者或者被訪問資源的代理實現類,該類持有一個ISubject接口的具體實例。在這個場景中,我們要對SubjectImpl進行代理,那麼SubjectProxy現在持有的就是SubjectImpl的實例。
  • Client。這代表訪問者的抽象角色,Client將會訪問ISubject類型的對象或者資源。在這個場景中,Client將會請求具體的SubjectImpl實例,但Client無法直接請求其真正要訪問的資源SubjectImpl,而是必須通過ISubject資源的訪問代理類SubjectProxy進行。

  SubjectImpl和SubjectProxy都實現了相同的接口ISubject,而SubjectProxy內部持有SubjectImpl的引用。當Client通過request()請求服務的時候,SubjectProxy將轉發該請求給SubjectImpl。從這個角度來說,SubjectProxy反而有多此一舉之嫌了,不過SubjectProxy的作用不只局限於請求的轉發,更多時候是對請求添加更多訪問限制。SubjectImpl和SubjectProxy之間的調用關係如下代碼所示:

public class SubjectProxy implements ISubject{
    private ISubject subject;   // Inject SubjectImpl to SubjectProxy
    public String request(){
        // add pre-process logic if necessary

        String originalResult = subject.request();

        // add post process logic if necessary

        return "Proxy:" + originalResult;
    }
    public ISubject getSubject(){
        return subject;
    }
    public void setSubject(ISubject subject){
        this.subject = subject;
    }
}

public class SubjectImpl implements ISubject{
    public String request(){
        // process logic
        return "OK";
    }
}

  在將請求轉發給被代理對象SubjectImpl之前或者之後,都可以根據情況插入其他處理邏輯,比如在轉發之前記錄方法執行開始時間,在轉發之後記錄結束時間,這樣就能夠對SubjectImpl的request()執行的時間進行檢測。或者,可以只在轉發之後對SubjectImpl的request()方法返回結果進行覆蓋,返回不同的值。甚至,可以不做請求轉發,這樣,就不會有SubjectImpl的訪問發生。

  代理對象SubjectProxy就像是SubjectImpl的影子,只不過這個影子通常擁有更多的功能。如果SubjectImpl是系統中Jointpoint所在的對象(即目標對象),那麼就可以為這個目標對象創建一個代理對象,然後將橫切邏輯添加到這個代理對象中。當系統使用這個代理對象的時候,原有邏輯的實現和橫切邏輯就完全融合到一個系統中。

  Spring AOP本質上就是採用這種代理機制實現的,但是,具體實現細節上有所不同。我們來看一下上面的代理實現,我們是將代理類直接寫好,然後在代碼中手動初始化代理類並通過調用代理類來實現代理功能,發現沒有,如果系統裏面有很多類需要代理相同的能,那麼我們就要寫很多的代理類,儘管它們代理的內容是一樣的,這樣是有問題的。上面這種為對應的目標對象創建靜態代理的方法,原理上是可行的,但具體應用上存在問題,所以要尋找其他方法,那有沒有呢,答案是肯定有的,就是接下來我們要講的動態代理。

 

3.2 動態代理

  JDK1.3之後,引入了動態代理(Dynamic Proxy)機制,可以在運行期間,為相應的接口(Interface)動態生成對應的代理對象,從而幫助我們走出最初使用靜態代理實現AOP的窘境。

  動態代理機制的實現主要由一個類和一個接口組成,即java.lang.reflect.Proxy類和java.lang.reflect.InvocationHandler接口。InvacationHandler就是我們實現橫切邏輯的地方,它是橫切邏輯的載體,作用跟Advice是一樣的。所以在使用動態代理機制實現AOP的過程中,我們可以在InvocationHandler的基礎上細化程序結構,根據Advice的類型,分化出對應不同的Advice類型的程序結構。

  所以,我們可以將橫切關注點邏輯封裝到動態代理的InvocationHandler中,然後在系統運行期間,根據橫切關注點需要織入的模塊位置,將橫切邏輯織入到相應的代理類中。以動態代理類為載體的橫切邏輯,現在當然就可以與系統其他實現模塊一起工作了。

  動態代理雖好,但不能滿足所有的需求,這種方式實現的唯一缺點或者說優點就是,所有需要織入橫切關注點邏輯的模塊類都得實現相應的接口,因為動態代理機制只針對接口有效。如果某個類沒有實現任何的接口,就無法使用動態代理機製為其生成相應的動態代理對象。對於沒有實現任何接口的目標對象我們需要尋找其他方式為其動態的生成代理對象。

  默認情況下,Spring AOP發現目標對象實現了相應接口,則採用動態代理機製為其生成代理對象實例。而如果目標對象沒有實現任何接口,Spring AOP則會嘗試使用一個稱為CGLIB(Code Generation Library)的開源的動態字節碼生成類庫,為目標對象生成動態的代理對象實例。

 

3.3 動態字節碼增強

  使用動態字節碼生成技術擴展對象行為的原理是,我們可以對目標對象進行繼承擴展,為其生成相應的子類,而子類可以通過覆寫來擴展父類的行為,只要將橫切邏輯的實現放到子類中,然後讓系統使用擴展后的目標對象的子類,就可以達到與代理模式相同的效果了。

  但是使用繼承的方式來擴展對象定義,也不能像靜態代理模式那樣,為每個不同類型的目標對象都單獨創建相應的擴展子類。所以,我們要藉助於CGLIB這樣的動態字節碼生成庫,在系統運行期間動態地為目標對象生成相應的擴展子類。

  我們知道,Java虛擬機加載的文件都是符合一定規範的,所以,只要交給Java虛擬機運行的文件符合Java class規範,程序的運行就沒有問題。通常的class文件都是從Java源代碼文件使用Javac編譯器編譯而成的,但只要符合Java class規範,我們也可以使用ASM或者CGLiB等Java工具庫,在程序運行期間,動態構建字節碼的class文件。

  在這樣的前提下,我們可以為需要織入橫切邏輯的模塊類在運行期間,通過動態字節碼增強技術,為這些系統模塊類生成相應的子類,而將橫切邏輯加到這些子類中,讓應用程序在執行期間使用從這些動態生成的子類,從而達到將橫切邏輯織入系統的目的。   使用動態字節碼增強技術,即使模塊類沒有實現相應的接口,我們依然可以對其進行擴展,而不用像動態代理那樣受限於接口。不過,這種實現機制依然存在不足,如果需要擴展的類以及類中的實例方法等聲明為final的話,則無法對其進行子類化的擴展。

 

3.4 一個spring aop示例

  上面說了這麼多,下面就來看一個簡單的例子,體會一下aop的魔法吧。如果只是引用了spring-context,那麼還需要引入spring-aspects:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>3.2.18.RELEASE</version>
</dependency>

  這裏我們採用xml配置的方式來開啟aop功能,在resources目錄下添加一個xml配置文件,其中<aop:aspectj-autoproxy/>是用來開啟aop的:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop = "http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
     
     <aop:aspectj-autoproxy/>
     
     <bean id = "test" class = "spring.aop.TestAopBean"/>
     <bean class = "spring.aop.AspectJTest"/>
</beans>

  添加Aspect:

@Aspect
public class AspectJTest {

    @Pointcut("execution(* *.test(..))")
    public void test(){
        
    }

    @Before("test()")
    public void beforeTest(){
        System.out.println("beforeTest");
    }
    
    @After("test()")
    public void afterTest(){
        System.out.println("afterTest");
    }

    @Around("test()")
    public Object aroundTest(ProceedingJoinPoint p){
        System.out.println("before1");
        Object o = null;
        try{
            o = p.proceed();
        }catch (Throwable e){
            e.printStackTrace();
        }
        System.out.println("after1");
        return o;
    }
}

  添加測試類:

public class TestAopBean {

    private String testStr = "testStr";

    public String getTestStr(){
        return testStr;
    }

    public void setTestStr(String testStr){
        this.testStr = testStr;
    }

    public void test(){
        System.out.println("hello test");
    }
    
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("aspectJTest.xml");
        TestAopBean test = (TestAopBean)ctx.getBean("test");
        test.test();
    }
}

  可以看到輸出結果:

before1
beforeTest
hello test
after1
afterTest

  這是一個aop簡單示例,我們寫了一個切面(Pointcut),用來指定Joinpoint的位置在執行test()方法時;同時分別定義了三個Advice(Before、After、Around)用來指定要織入的動作,最後將Pointcut和Advice封裝到一個Aspect中,這樣就完成了橫切邏輯的織入。

 

4. 總結

  在深入學習Spring AOP之前,我們先對AOP的概況進行了介紹,接着一起探索了Spring AOP的實現機制,包括最原始的代理模式,直至最終的動態代理與動態字節碼生成技術。

  • AOP是能夠讓我們在不影響系統原有功能前提下,為軟件系統橫向擴展功能;
  • Spring AOP通過兩種方式實現:JDK動態代理、動態字節碼增強;

  在了解了這些內容之後,我們將繼續深入學習Spring AOP。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

基於GTID搭建主從MySQL_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

目錄

  • 基於gtid搭建主從MySQL
    • 一、GTID的使用
    • 二、GTID的簡介
    • 三、GTID的構成
    • 四、查看GTID的執行情況
      • 4.1 gtid_executed
      • 4.2 gtid_own
      • 4.3 gtid_purged
    • 五、MySQL的冪等性
    • 六、拓展:
    • 七、實驗:
      • 小實驗1:
      • 小實驗2:

基於gtid搭建主從MySQL

一、GTID的使用

想讓主從之間使用gtid的方式同步數據,需要我們在配置文件中開啟mysql對gtid相關的配置信息

找到my.cnf ,在mysqld模塊中加入如下的配置。(主庫從庫都這樣)

# on表示開啟,OFF表示關閉
gtid-mode = ON
# 下面的兩個變量必須開啟,否則MySQL拒絕啟動
log-slave-updates = 1
log-bin = MySQL-bin
# 必須開啟,否則MySQL不啟動,因為MySQL的SQL和gtid
# 許多MySQL的SQL和GTID是不兼容的。比如開啟ROW 格式時,CREATE TABLE … SELECT
# 在binlog中會形成2個不同的事務,GTID無法唯一。
# 另外在事務中更新MyISAM表也是不允許的。
enforce_gtid_consistency = 1  #
log-bin-index = MySQL-bin.index

對主庫來說依然需要創建一個用於同步數據的賬號

mysql> grant replication slave on *.* to MySQLsync@"10.123.123.213" identified by "MySQLsync123";
Query OK, 0 rows affected, 1 warning (0.00 sec)

對從庫中執行如下命令:即可完成主從同步

CHANGE MASTER TO
    MASTER_HOST='10.123.123.123',
    MASTER_USER='MySQLsync',
    MASTER_PASSWORD='MySQLsync123',
    MASTER_PORT=8882,
    MASTER_AUTO_POSITION = 1;

這種自動找點對方式,相對於之前使用bin-log+position找點就顯得及其方便了。

省去了手動查看master執行到那個binlog,以及binlog的position。

做了如上的配置后,還是可以繼續使用fileName和position找點,但是不推薦這樣做了,如果非要這樣做,設置MASTER-AUTO-POSITION=0

假設我們想在現有的主從集群上新加一個從庫,如果這個主庫已經運行很久了,binlog肯定曾經被purge過,所以如果主庫中原來的數據不重要,不介意主從數據不一致,在從庫中執行:

reset master; 

# 缺失的GTID集合設置為purged ,執行這個命令請確保從庫:@@global.gtid_executed為空
set global gtid_purged = ‘主庫中曾經purge過的gtid記錄’

# 然後通過MASTER_AUTO_POSITION=1完成自動找點

如果介意主從數據強一致可以考慮使用可以熱備份的工具從主庫拷貝數據到從庫,完成數據的同步再在從庫執行如上set global gtid_purged = ‘xxx’

熱備份工具如:xtrabackup , 它可以拷貝物理文件和redolog來支持熱備份

二、GTID的簡介

GTID (global transcation identifier)

GTID是MySQL5.6版本中添加進來的新特性 ,通過GTID取代同步模式1中手動查找fileName和position, 實現了自動找點

比如一條update有語句進入MySQL之後經歷如下過程:

1. 寫undolog 
2. 寫redolog(prepare)
3. 寫binlog 
4. 寫redolog(commit)

MySQL5.6之後加入了GTID新特性后,update語句經歷如下過程

1. 寫undolog # 回滾
2. 寫redolog(prepare)# 保證提交的不會丟失
3. 寫一個特殊的Binlog Event,類型為GTID_Event,指定下一個事務的GTID 
4. 寫binlog # 主從同步事物使用
5. 寫redolog(commit)

這個GTID的作用就是用於去唯一的標示一個事物的id。

主從之間,之所以能完成數據的同步,是因為從庫會dump主庫記錄的binlog, 主庫將自己成功執行過的事物都寫在binlog用於給從庫回放。當我們在mysql的配置文件中將上面的配置都打開時,主庫在記錄binlog的同時在binlog中會混雜着gtid的信息,這個gtid和當前事物唯一對應。

當從庫向主庫發送同步數據當請求時:bin-log和gtid都會傳送到slave端,slave在回放日誌同步數據時,同樣會使用gtid寫bin-log,這樣主庫和從庫之間的數據,就通過GTID強制性的關聯並且保持同步了。

下圖中淺色的背景是一條完整的binlog,從binlog的記錄中可以發現,gtid也寫在binlog中,當前事物提交commit后,還會為下一個事物生成一個gtid待使用。

三、GTID的構成

gtid由兩部分組成,server_uuid:transaction_id

  • server_uuid是一個只讀變量,保存在 MySQL/var/auto.cnf, 也可以通過命令查看show global variables like 'server_uuid' ;

    第一種查看方式:

cat auto.cnf

第二種查看方式:

  • transaction_id是事物id, 一般他會遞增。

四、查看GTID的執行情況

在主庫中查看gtid的執行情況:

如果不出意外的話,從庫中的gtid執行情況和主庫是一樣的。

這時如果我們往主庫插入一條信息,主庫的gtid_executed則會變成:

f6b65f0b-a251-11ea-8921-b8599f2ef058:1-5

同樣去從庫中查看,從庫的gtid信息和主庫是一樣的。

4.1 gtid_executed

它既可以是一個global類型的變量,也可以是一個session級別的變量。是只讀的,記錄著曾經執行過的gtid集合。比如:f6b65f0b-a251-11ea-8921-b8599f2ef058:1-5 表示曾經執行過1~5共五個事物。

global和session之間的區別:如果在global級別下,我們打開一個會話然後設置值,關掉這個窗口,打開一個新的窗口,依然能看在上一個被關閉的窗口設置的值。 如果在session級別下,打開一個窗口A設置值,然後打開一個新的窗口重新查看是看不到在繪畫A中修改過的值的。

4.2 gtid_own

它既可以是一個global類型的變量,也可以是一個session級別的變量。是只讀的,它記錄的是當前實例正在執行的gtid,以及對應的線程id。

4.3 gtid_purged

它是一個全局的變量,purge就是丟棄的意思,而bin-log是實現主從之間的數據同步,主要是起到一个中間者的作用,主從數據同步之後,binlog其實就可以被清除了,線上的日誌文件被清理最勤的日誌文件恐怕就binlog,可能每隔幾個小時或者1天清理一次。

這個gtid_purged所裝載的就是被丟棄的bin-log對應的gtid集合, gtid_purged是gtid_executed的子集,是不能被隨意更改的,只有在@@global.gtid_executed為空的情況下才能修改這個值。

認識了上面的幾個變量后:整理一下整個流程:

  • 開啟GTID模式后,我們指定MASTER_AUTO_POSITION=1。然後 start slave時,從庫會計算Retrieved_Gtid_SetExecuted_Gtid_Set的並集(通過show slave status可以查看),然後把這個GTID並集發送給主庫。主庫將從庫請求的GTID集合和自己的gtid_executed比較,把從庫GTID集合里缺失的事務全都發送給從庫。從庫再拿着這些gtid在自己本地回放事物,同步數據。

  • gtid是有冪等性的,從庫碰到原來使用過的gtid會直接跳過。

  • 如果從庫缺失的GTID已經被主庫pruge了,那麼從庫報1236錯誤,IO線程中斷。

    ※推薦台中搬家公司優質服務,可到府估價

    台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

此外,從庫的Retrieved_Gtid_SetExecuted_Gtid_Set在哪裡查看呢?

通過show slave status 查看

再看這張圖:

這張圖是從庫的gtid相關信息:

從張圖乍一看gtid的信息是比較亂的:

server_uuid後綴為058結尾的是從庫同步的主庫數據時產生的。

server_uuid後綴以b38(從庫自己的server_uuid)結尾的記錄,其實是我直接在從庫insert數據產生的。

五、MySQL的冪等性

默認情況下MySQL冪等級別是:strict , 表示嚴格模式。 舉個例子,在這種情況下:假設id為主鍵,假設數據庫中已經存在了一條id=1 ,value = 1 的數據,我們重複的插入id=1 , value=2的新數據mysql會爆出 主鍵衝突的錯誤。

如果我們將冪等模式調整成:idemptent ,再往MySQL中插入一條id=1,value = 2的數據,此時不會爆出主鍵衝突,這條新數據會將原來value = 1 覆蓋成value=2。

開啟主從的模式下從庫的: slave_exec_mode 參數一般會被設置成:idemptent  , 這樣可以保證從庫中的數據和主庫中的數據保持一致。 並且不會發生這種問題:比如主庫中沒有id = 100的數據,故主庫中可以順利寫入id=100的數據, 從庫中有id=100的數據,但是因為開啟冪等模式,從庫不會爆出主鍵衝突的錯誤而中斷主從關係,而是使用主庫binlog中的新值去覆蓋當前存在的舊值。

六、拓展:

假設存在這樣一種場景:

有一對主從正常運行保持數據同步狀態。然後意外發生了:從庫在回放主庫binlog時有一個gtid對應的事物執行失敗了。具體一點,比如主庫現在 excuted_gtid = 1-20 , 然後從庫回放到第10條事物時還沒問題,但是回放到第11條事物時因為數據對不起來,執行失敗了,緊接着斷開了主從同步到關係。

如果是使用 binlog+position 實現的主從同步,我們可以設置 sql_slave_skip_counter 讓從庫跳過失敗的事物完成再恢復同步關係。

但是使用gtid構建的主從同步就不能skip事物了,它是自動找點的。

並且你通過show variables like '%gtid%' 可以看一下gtid的信息, gtid_next是原子自增的。

mysql> show variables like '%gtid%' ;
+----------------------------------+------------------------------------------+
| Variable_name                    | Value                                    |
+----------------------------------+------------------------------------------+
| binlog_gtid_simple_recovery      | ON                                       |
| enforce_gtid_consistency         | ON                                       |
| gtid_executed_compression_period | 1000                                     |
| gtid_mode                        | ON                                       |
| gtid_next                        | AUTOMATIC                                |
| gtid_owned                       |                                          |
| gtid_purged                      | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
| session_track_gtids              | OFF                                      |
+----------------------------------+------------------------------------------+
8 rows in set (0.01 sec)

查看主庫的執行狀態:也能看到gtid的序號是連續的。


mysql> show master status;
+------------------+----------+--------------+------------------+------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+------------------+----------+--------------+------------------+------------------------------------------+
| mysql-bin.000006 |      194 |              |                  | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
+------------------+----------+--------------+------------------+------------------------------------------+

所以如果出現了上面的情景,可以像下面這樣做:

# 第gtid == 11 出問題了,就跳過它
set global sql_slave_skip_counter = 11;

# 執行begin commit 不會讓現有的gtid+1,僅僅是交一個空事物,佔據這個gtid=11的位置
BIGIN;COMMIT;

七、實驗:

小實驗1:

可以做一個小實驗:

先往主庫中寫入一條數據,然後刷新日誌落盤

mysql> INSERT INTO `xxx`.`yyy` (`id` , `val`) VALUES (NULL , '123');
Query OK, 1 row affected (0.01 sec)

mysql> flush logs;
Query OK, 0 rows affected (0.00 sec)

mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000005 |       506 |
| mysql-bin.000006 |       194 |
+------------------+-----------+
2 rows in set (0.00 sec)

退出mysql查看所有的日誌:

然後我們手動執行 rm -rf mysql-bin.00000*

再登陸mysql查看gtid_pured的變化情況:(你會發現,當我手動刪除主庫的binlog時,gtid_purged中沒有任何記錄)

mysql> show global variables like 'gtid%';
+----------------------------------+------------------------------------------+
| Variable_name                    | Value                                    |
+----------------------------------+------------------------------------------+
| gtid_executed                    | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-7 |
| gtid_executed_compression_period | 1000                                     |
| gtid_mode                        | ON                                       |
| gtid_owned                       |                                          |
| gtid_purged                      |                                          |
+----------------------------------+------------------------------------------+
5 rows in set (0.00 sec)

當然上面手動rm -rf刪除日誌的方式是在開玩笑,真實的purged操作如下:

mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000005 |       506 |
| mysql-bin.000006 |       194 |
+------------------+-----------+
2 rows in set (0.00 sec)

mysql> purge binary logs to 'mysql-bin.000006';
Query OK, 0 rows affected (0.00 sec)

mysql> show binary logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000006 |       194 |
+------------------+-----------+
1 row in set (0.00 sec)

mysql> show global variables like 'gtid%';
+----------------------------------+------------------------------------------+
| Variable_name                    | Value                                    |
+----------------------------------+------------------------------------------+
| gtid_executed                    | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
| gtid_executed_compression_period | 1000                                     |
| gtid_mode                        | ON                                       |
| gtid_owned                       |                                          |
| gtid_purged                      | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-9 |
+----------------------------------+------------------------------------------+
5 rows in set (0.00 sec)

命令中的purge binary logs to 'xxx' 是通過mysql的機制去刪除binlog。刪除的範圍是 mysql-bin00000x之前的所有binlog,而不包含 mysql-bin00000x。

小實驗2:

假設我現在有AB兩台主從MySQL,兩台都開啟gtid, 我先通過gtid 完成主從之間的同步關係,插入幾條數據后,主從的gtid值都為 1-10 。

然後我開始搞事情:斷開主從,先往主庫寫入一條數據(gtid=11)。

mysql> INSERT INTO `dktest00`.`testaaa00` (`id` , `val`)VALUES (NULL , '123');
Query OK, 1 row affected (10.00 sec)

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                         |
+------------------+----------+--------------+------------------+-------------------------------------------+
| mysql-bin.000006 |      732 |              |                  | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-11 |
+------------------+----------+--------------+------------------+-------------------------------------------+

**再通過 binlog + position 完成兩者都同步關係。再往主庫中插入一條數據:

# set position = 732 
# binlog = f6b65f0b-a251-11ea-8921-b8599f2ef058:1-11

mysql> INSERT INTO `dktest00`.`testaaa00` (`id` ,`val`)VALUES (NULL , '123');
Query OK, 1 row affected (0.00 sec)

然後我去查看從庫的gtid信息:

mysql> show  global variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name                    | Value                                        |
+----------------------------------+----------------------------------------------+
| gtid_executed                    | f6b65f0b-a251-11ea-8921-b8599f2ef058:1-10:12 |
| gtid_executed_compression_period | 1000                                         |
| gtid_mode                        | ON                                           |
| gtid_owned                       |                                              |
| gtid_purged                      |                                              |
+----------------------------------+----------------------------------------------+
5 rows in set (0.00 sec)

我們會發現,雖然是使用position+binlog同步數據,但是只要gtid開啟了,gtid就會記錄曾經執行的事物的信息

那並且如果我們定位potion = 732 ,從庫回放的事物就不會包含 gtid = 11, 這一點我們可以通過查看binlog作證

mysqlbinlog --no-defaults -vv /binlog的絕對路徑。

這說明啥事呢? 比如現在主庫中有1,2,3條數據,然後你執行show master status; 看到這個position = 732

那這個732其實不是不包含第三條數據的。換句話說從庫 從732同步數據,不會同步上第3條數據。

然後我們在此基礎上繼續搞事情:我們斷開主從,重新設置從庫position 為 732的上一個事物的位點(459),然後觀察從庫的 slave staus 情況。

開始同步后查看從庫中的數據,可以發現剛才跳過的gtid=11對應的數據已經被同步回來了。

如果我們不開啟gtid的話,在最後一步的操作中會導致主從中斷,因為從庫的slave_exec_mode會處於嚴格格式,肯定會發生主鍵衝突。但是當我們開啟了gtid,在最後一步的同步的過程中,因為開啟了gtid,從庫只會將剛剛會同步剛才跳過的gtid=12的數據,其他已經存在的不再重複同步。

通過命令 show variables like ‘%slave%’ 可以查看到和slave相關的所有配置信息

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

十幾萬預算 如何用最少的錢買到最好的車_網頁設計公司

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

這些都是給大家的參考標準,不一定都要滿足以上條件,畢竟人無完人,車也一樣嘛。下面就介紹幾台實用可靠的家用車,希望供大家一個好的選擇。10萬級別家用車:轎車:卡羅拉、思域、帝豪SUV:繽智、博越、瑞虎720萬級別家用車:轎車:雅閣、君越、博瑞SUV:昂科威、奇駿、途觀。

買車之前得選車,

選車之前得看指標,

指標喜歡但並不一定能下得了手,

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

這不都是錢在作怪嘛!

今天就不拋開錢來說,

說點實際一點的東西,

就是如何用最少的錢買到最好的車,

十幾二十萬的家用車我們該如何選?

這些都是給大家的參考標準,

不一定都要滿足以上條件,

畢竟人無完人,車也一樣嘛!

下面就介紹幾台實用可靠的家用車,

希望供大家一個好的選擇。

10萬級別家用車:

轎車:卡羅拉、思域、帝豪

SUV:繽智、博越、瑞虎7

20萬級別家用車:

轎車:雅閣、君越、博瑞

SUV:昂科威、奇駿、途觀本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

這些SUV男人們都好想買 但是……_如何寫文案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

所以在城市道路開着一輛硬派SUV,說得不好聽就是折磨啊。油耗肯定是個大頭所謂多個香爐多隻鬼,更何況硬派SUV身上多了那麼多的部件,車身的重量相對城市SUV肯定會更大,大家都知道車子的油耗跟車重是脫不了干係的,車重了,油耗肯定就更大了。

和很多車迷一樣,也是一名SUV愛好者,每次看到微博大V或者各種越野愛好者開着他們的LC、霸道,開着他們的牧馬人、奔馳G去攀山涉水,進西藏,走新疆的時候,難免都會腦袋一熱,要不,我也買一輛硬派SUV然後像他們一樣去馳騁野外吧,那應該就是我想要的生活。

然而,事實上那些我們所青睞的硬派SUV,銷量其實都不樂觀,標杆產品普拉多還算好,9月份賣出了3737輛,但對比動輒月銷上萬的城市SUV,普拉多的銷量並不閃亮。至於自主品牌,賣得最好的硬派SUV要數馭勝的S350,但銷量也就2943輛,這已經是賣得最好的自主硬派SUV了。

就納悶了,我們都那麼喜歡這些硬派SUV(別不認哈,在硬派SUV的文章里看過你們的留言,一個個都是垂涎欲滴的),為什麼到我們要掏錢買車的時候,卻不願意選擇它們?其實這現象的出現並不是沒有道理的,整理了一下,硬派SUV不受待見,主要是以下的原因:

先天就有制約條件

硬派SUV的動力、底盤等的調校都是圍繞越野來做的,所以在城市道路的行駛質感、乘坐體驗都不及轎車或者城市SUV,說白了,就是舒適性不夠。

由於硬派SUV多數都是非承載式車身,而且是帶有前後整體橋的,

※教你寫出一流的銷售文案?

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

底盤的部件本來就多,所以大部分的硬派SUV車身都會比較高。車身較高的硬派SUV在城市道路中進行頻繁的啟動停止和轉彎切線等等的動作時,側傾就會相對明顯;同時這麼高的車身也不利於空氣動力學的設計,從而造成了較差的駕乘體驗。

硬派SUV並不適合城市駕駛

除了車身的高度需要妥協,硬派SUV的油門響應會調校得比較遲滯,因為太過於靈敏的油門響應不利於越野工況下的脫困;再有一個就是剎車,因為車子本身的重心較高,太靈敏的剎車不利於車身的穩定行駛,所以硬派SUV的剎車都會調校的比較軟,每次剎車都需要深踩,在市區啟停幾次之後,右腳還是會蠻累的。

最後要說一個的就是底盤,為了維持車身的整體性和剛度,防止重心較高的車子發生側翻,所以硬派SUV的底盤調校都會偏硬,當然這也有利於在越野過程中減少多餘的跳動。而這種硬朗的底盤調校風格,放到城市道路上就會造成不舒適,路面很小的顛簸都會傳遞到車內形成明顯的震動,乘坐感並不友好。所以在城市道路開着一輛硬派SUV,說得不好聽就是折磨啊。

油耗肯定是個大頭

所謂多個香爐多隻鬼,更何況硬派SUV身上多了那麼多的部件,車身的重量相對城市SUV肯定會更大,大家都知道車子的油耗跟車重是脫不了干係的,車重了,油耗肯定就更大了。像3.5L排量的行業標杆普拉多,官方百公里油耗都去到11.4L,更別說這已經是理想化的数字了,也更別說你在市區經常要走走停停了。算下來,每天通勤的油費都夠嗆。

重點是這個:硬派SUV都不便宜啊

其實普拉多還好,懸挂的前段都會有一點軟,所以在城市道路也不至於太難受,而普拉多的入門價格都去到36.98萬了。至於越野粉充值信仰的良物,Jeep牧馬人,最便宜的車型也要去到42.95萬元。要知道這個價格足夠買一輛操控一流的寶馬3系有餘了。唯有自主品牌的馭勝S350比較便宜,15萬元的級別,然而這輛車子的“越野”,更多的體現在它的外觀,而不是性能。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

※別再煩惱如何寫文案,掌握八大原則!

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

為什麼要找教授買車?看完這裏給您答案_網頁設計公司

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

粉絲“小珊”是一名廣州在讀的女研究生,老家在廣東陽江,這次購買的車是皇冠2。0T,車是幫她爸爸買的,找到我們幫忙是因為爸爸在老家當地一直談不到滿意的價格,恰巧小珊一直有關注玩車並且知道可以幫忙購車,就抱着試一試的心態找到我們,結果是給到了小珊爸爸一個很滿意的價格,並且從訂車到提車只用了一周的時間。

又是新一天的開始,迅速進入工作狀態,回復用戶諮詢訂單;接聽用戶諮詢電話;幫用戶找車談價格;邀約用戶到店看車;幫助用戶成功買車;協助用戶驗車提車。

以上是每天在重複的工作,這既有規律又重複的事情在很多人看來是枯燥而乏味的,但是卻每天都樂在其中,因為每當看到用戶充滿笑容的提車照片時所有的辛苦都可以拋之腦後,而為了讓大家也能感受到和車主的那份喜悅,我們特意邀請了三位粉絲車主錄了一段視頻專訪來給大家展示。

粉絲“小珊”是一名廣州在讀的女研究生,老家在廣東陽江,這次購買的車是皇冠2.0T,車是幫她爸爸買的,找到我們幫忙是因為爸爸在老家當地一直談不到滿意的價格,恰巧小珊一直有關注玩車並且知道可以幫忙購車,

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

就抱着試一試的心態找到我們,結果是給到了小珊爸爸一個很滿意的價格,並且從訂車到提車只用了一周的時間。

粉絲“阿全”是一名土生土養的廣州90后,目標車型是鋒范,作為一名“老粉”,阿全一開始就找到了,當時剛好是9月初,各大經銷商為了十一黃金周沖銷量9月份的車價都開始收緊了,為了滿足阿全近期提車的要求,找到了合作較好的廣本經銷商,在市場都收緊價格的情況下為阿全爭取到了接近收價前的優惠,並且在原本沒現車的情況下也和經銷商協調回來了一台現車。

粉絲“阿曾”是一名廣州當地醫院的內科醫生,平時工作十分忙,根本就沒有時間去看車談價,所以從一開始就找到了幫忙找車談價,目標車型是雅閣,卻在糾結2.0L還是2.4L版本,然後分析過阿曾平時大部分時間是在市區用車,因為工作忙跑高速的機會是少之又少,所以給推薦了2.0L版本的雅閣,市區用車省油之餘保養費用也較2.4L版本要經濟,最後阿曾接納了的建議,也在取得滿意的價格后一周左右的時間就提車了

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

※想知道最厲害的網頁設計公司嚨底家"!

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

疫情期網購夯 新加坡民眾愛烘焙與珍奶材料_網頁設計公司

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

摘錄自2020年5月25日中央社報導

為遏止疫情擴散,星國政府自4月7日起實施「阻斷措施」,許多民眾改成在家上班,專賣手搖飲料等店家也都暫時關閉。突如其來的疫情改變了許多民眾的生活習慣。「海峽時報」(The Straits Times)報導,啞鈴、珍奶材料可能是許多人之前不會想購買的商品,但現在都成為熱搜品。

生活必需品、食物、室內運動器材都是現在新加坡熱門網購商品。專家分析,這股網購潮流在防疫措施放寬後仍會持續,很多人也預期,電商在「新常態」生活中將扮演更重要的角色。疫情興起民眾對烹飪的興趣,促使麵粉成為網路搜尋熱門關鍵字。

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

另一方面,疫情期間長時間待在家也激起民眾對「安慰食品」的興趣,珍奶就是其中一項。外送平台Grab在4月收到的珍奶訂單較前一個月大增60%。

除了仰賴外送服務,不少民眾也對珍奶DIY很感興趣,越來越多人上網搜尋食譜,製作珍奶所需要的粉圓材料即是蝦皮的熱門商品之一。隨著在家時間變長,烘焙成為民眾新興的消遣活動,許多烘焙材料店都出現排隊人潮。

生活環境
國際新聞
新加坡
武漢肺炎
疫情
網購食品
疫情下的食衣住行

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

印度洪災逾630人死 莫迪:建洪水預警系統_如何寫文案

※別再煩惱如何寫文案,掌握八大原則!

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

摘錄自2020年8月10日中央社報導

印度雨季豪雨成災,影響1750萬人、至少造成630人喪生,總理莫迪今天(10日)與受災的6省省長視訊討論災情,認為中央與地方應建立一套永久洪水預報系統。

雨季帶來的豪雨和洪水重創印度阿薩姆省(Assam)、北方省(UP)、克勒拉省(Kerala)、卡納塔卡省(Karnataka)和馬哈拉什特拉省(Maharashtra)等6省,根據紅十字會與紅新月會國際聯合會(IFRC)8月初公布的資料,迄今已有近1750萬人受到影響,超過630人死於洪水帶來的相關災害。

※教你寫出一流的銷售文案?

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

莫迪(Narendra Modi)在會議中說,所有中央機構和地方政府之間應加強協調聯繫,應建立一套永久的洪水預報系統,並採用創新技術改善預報和預警系統。

莫迪說,過去幾年,中央氣象局(IMD)和中央水資源委員會(Central Water Commission)等預報機構一直共同努力,以制定更好、更有用的洪水預報系統,目前正在試點採用人工智慧等創新科技改善具體地點的洪水預報。

國際新聞
印度
暴雨
洪水

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。