Toyota加入純電動車戰場,日產Leaf仍佔銷售鰲頭

日系車商豐田(Toyota)看好純電動車(EV)的市場,正在計畫跨入此一領域,拓展油電混和車、燃料電池車(FCV)之外的新市場,並與BMW、Tesla等國際知名車廠一較高下,並挑戰日廠Leaf的龍頭地位。

《日經》中文網報導,Toyota預計最快在2017年新設EV的開發與設計部門,並整合集團資源,盡快投入量產。Toyota將整合油電混和車Prius和經典車款Corolla的平台技術,以推出一次充電續航力300公尺的純電動車為首要目標。此外,由於SUV車款頗受歡迎,Toyota也會考慮推出純電動車版的SUV車款。

同時,Toyota將加速今年1月成立的電池材料技術部門的研發進度,也會考慮向外採購核心零組件,以強化電動車用電池的性能。

市場競爭者多,日產Leaf仍占銷售龍頭

Toyota曾於2012~2014年間在美國與Tesla共同開發過SUV純電動車,但考量到電池的成本與性能限制等問題,並未積極發展純電動車事業。然而,隨著Tesla、BMW、Volkswagen、BYD等國際大廠加入戰局,電動車的各項零件性能與續航力都有顯著的提升,也使Toyota決定重新加入開發純電動車的行列。

MoneyDJ引述日媒Sankei Biz的報導表示,截至2016年8月為止,日產Leaf仍然是全球最暢銷的電動車,累計銷售量達23.4萬輛。不過,Leaf正與Tesla展開激烈的銷售戰。

報導指出,日本市調機構富士經濟預測,插電式油電混和車(PHEV)和純電動車的需求在2025年以後將會加速,2030年時將能與油電混和車廂庭抗禮;2035年時,PHEV和電動車的市場將超越油電混和車。屆時全球電動車的市場規模將達567萬輛,較2015年成長16倍之多。

(照片來源:)

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

【其他文章推薦】

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

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

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

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

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

聚甘新

對 JsonConvert 的認識太膚淺了,終於還是遇到了問題

一:背景

1. 講故事

在開始本文之前,真的好想做個問卷調查,到底有多少人和我一樣,對 JsonConvert 的認識只局限在 SerializeObjectDeserializeObject 這兩個方法上(┬_┬), 這樣我也好結伴同行,不再孤單落魄,或許是這兩個方法基本上能夠解決工作中 80% 的場景,對於我來說確實是這樣,但隨着編碼的延續,終究還是會遇到那剩下的 20% ,所以呀。。。

我的場景是這樣的:前段時間寫業務代碼的時候,我有一個自定義的客戶算法類型的Model,這個Model中有這種算法類型下的客戶群以及Report統計信息,還用了 HashSet 記錄了該類型下的 CustomerID集合,為了方便講述,我把Model簡化如下:


    class CustomerAlgorithmModel
    {
        public string DisplayName { get; set; }

        public int CustomerType { get; set; }

        public ReprotModel Report { get; set; }

        public HashSet<int> CustomerIDHash { get; set; }
    }

    class ReprotModel
    {
        public int TotalCustomerCount { get; set; }

        public int TotalTradeCount { get; set; }
    }

那有意思的就來了,我個人是有記日誌的癖好,就想着以後不會出現死無對證的情況,然後就理所當然的使用 JsonConvert.SerializeObject, 這一下就出問題了,日誌送入到了 ElasticSearch ,然後通過 Kibana 查不出來,為啥呢? 看完上面的 Model 我想你也猜到了原因,json體太大了哈,好歹 CustomerIDHash 中也有幾十萬個撒,這一下全導出成json了,這 size 還能小嗎? 要不我寫段代碼看一看。


        static void Main(string[] args)
        {
            var algorithModel = new CustomerAlgorithmModel()
            {
                CustomerType = 1,
                DisplayName = "",
                Report = new ReprotModel()
                {
                    TotalCustomerCount = 1000,
                    TotalTradeCount = 50
                },
                CustomerIDHash = new HashSet<int>(Enumerable.Range(1, 500000))
            };

            var json = JsonConvert.SerializeObject(algorithModel);

            File.WriteAllText("1.txt", json, Encoding.UTF8);

            Console.WriteLine("寫入完成!");
        }

可以看到,僅一個json就 3.3M,這樣的記錄多來幾打后,在 kibana 上一檢索,瀏覽器就卡的要死,其實 CustomerIDHash 這個字段對我來說是可有可無的,就算存下來了也沒啥大用,所以需求就來了,如何屏蔽掉 CustomerIDHash

二:尋求解決方案

1. 使用 JsonIgnore

有問題就網上搜啊,這一搜馬上就有人告訴你可以使用 JsonIgnoreAttribute 忽略特性,加好這個特性後繼續跑一下程序。


    [Newtonsoft.Json.JsonIgnore]
    public HashSet<int> CustomerIDHash { get; set; }

太好了,終於搞定了,但是靜下心來想一想,總感覺心裏有那麼一點不舒服,為什麼這麼說,一旦你給這個 CustomerIDHash 套上了 JsonIgnore ,這就意味着它在 JsonConvet 的世界中從此消失,也不管是誰在使用這個Model, 但這並不是我的初衷,我的初衷僅僅是為了在記錄日誌的時候踢掉 CustomerIDHash,可千萬不要影響在其他場景下的使用哈,現在這種做法就會給自己,給別人挖坑,埋下了不可預知的bug,我想你應該明白我的意思,還得繼續尋找下一個方案。

2. 使用自定義的 JsonConverter

真的,Newtonsoft 太強大了,我都想寫一個專題好好彌補彌補我的知識盲區,其實在這個場景中不就是想把 HashSet<int> 給屏蔽掉嘛,Newtonsoft 中專門提供了一個針對特定類型的自定義處理類,接下來我就寫一段:


   /// <summary>
   /// 自定義一個 針對 HashSet<int> 的轉換類
   /// </summary>
   public class HashSetConverter : Newtonsoft.Json.JsonConverter<HashSet<int>>
   {
       public override HashSet<int> ReadJson(JsonReader reader, Type objectType, HashSet<int> existingValue, bool hasExistingValue, JsonSerializer serializer)
       {
           return existingValue;
       }

       public override void WriteJson(JsonWriter writer, HashSet<int> value, JsonSerializer serializer)
       {
           writer.WriteNull();
       }
   } 

就是這麼簡單,然後就可以在 SerializeObject 的時候指定下自定義的 HashSetConverter 即可,然後再將程序跑起來看一下。


 var json = JsonConvert.SerializeObject(algorithModel, Formatting.Indented, new HashSetConverter());

從圖中看,貌似也是解決了,但我突然發現自己要鑽牛角尖了,如果我的實體中又來了一個頂級優質客戶群的 TopNCustomerIDHash,但因為這個CustomerID 比較少,我希望在 Json 中能保留下來,然後就是踢掉的那個 CustomerIDHash 我要保留 CustomerIDHash.Length ,哈哈,搞事情哈,那接下來怎麼解決呢?

  • 修改 Model 實體

    class CustomerAlgorithmModel
    {
        public HashSet<int> CustomerIDHash { get; set; }

        // topN 優質客戶群
        public HashSet<int> TopNCustomerIDHash { get; set; }
    }

  • HashSetConverter 增加邏輯鑒別是否為保留字段

        public override void WriteJson(JsonWriter writer, HashSet<int> value, JsonSerializer serializer)
        {
            if (writer.Path == "TopNCustomerIDHash")
            {
                writer.WriteStartArray();

                foreach (var item in value)
                {
                    writer.WriteValue(item);
                }

                writer.WriteEndArray();
            }
            else
            {
                writer.WriteValue(value.Count);
            }
        }

  • 最後給 TopNCustomerIDHash 賦值

            var algorithModel = new CustomerAlgorithmModel()
            {
                CustomerType = 1,
                DisplayName = "",
                Report = new ReprotModel()
                {
                    TotalCustomerCount = 1000,
                    TotalTradeCount = 50
                },
                CustomerIDHash = new HashSet<int>(Enumerable.Range(1, 500000)),
                TopNCustomerIDHash = new HashSet<int>(Enumerable.Range(1, 10)),
            };

三塊都搞定后就可以把程序跑起來了,如下圖:

貌似鑽牛角尖的問題是解決了,既然鑽牛角尖肯定要各種鄙視,比如這裏的 ReportModel 我是不需要的,CustomerType 我也是不需要的,我僅僅需要看一下 DisplayNameTotalCustomerCount 這兩個字段就可以了, 那這個要怎麼解決呢?

3. 使用 匿名類型

確實很多時候記日誌,就是為了跟蹤 Model 中你特別關心的那幾個字段,所以摻雜了多餘的字段確實也是沒必要的,這裏可以用匿名來解決,我就來寫一段代碼:


    var json = JsonConvert.SerializeObject(new
    {
        algorithModel.DisplayName,
        algorithModel.Report.TotalCustomerCount
    }, Formatting.Indented);

三: 總結

雖然阻擊了幾個回合,但同時也發現了 Newtonsoft 中還有特別多的未挖掘功能,真的需要好好研究研究,源碼已下好,接下來準備做個系列來解剖一下,值得一提的是 .Net中已自帶了 System.Text.Json.JsonSerializer 類,目前來看功能還不算太豐富,簡單用用還是可以的,本篇就說到這裏,希望對您有幫助。

如您有更多問題與我互動,掃描下方進來吧~

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

聚甘新

深入理解 EF Core:EF Core 寫入數據時發生了什麼?

閱讀本文大概需要 14 分鐘。

原文:https://bit.ly/2C67m1C
作者:Jon P Smith
翻譯:王亮
聲明:我翻譯技術文章不是逐句翻譯的,而是根據我自己的理解來表述的。其中可能會去除一些本人實在不知道如何組織但又不影響理解的句子。

這是深入理解 EF Core 系列的第二篇文章。第一篇是關於 EF Core 如何從數據庫讀取數據的;而這一篇是關於 EF Core 如何向數據庫寫入數據的。這是四種數據庫操作 CRUD(新增、讀取、更新和刪除)中的 CUD 部分。

我假設你對 EF Core 已經有了一定的認識,但在深入學習之前,我們先來了解一下如何使用 EF Core,以確保我們已經掌握了一些基本知識。這是一個“深入研究”的課題,所以我準備大量的技術細節,希望我的描述方式你能理解。

本文是“深入理解 EF Core”系列中的第二篇。以下是本系列文章列表:

  • 深入理解 EF Core:當 EF Core 從數據庫讀取數據時發生了什麼?
  • 深入理解 EF Core:當 EF Core 寫入數據到數據庫時發生了什麼?(本文)
  • 深入理解 EF Core:使用查詢過濾器軟刪除數據(敬請期待)

概要

∮. EF Core 可以通過新的或已存在的關聯關係創建一個新的實體。為此,它必須以正確的順序來組織實體類,以便能夠建立各類之間的關聯。這使得開發人員很容易寫出具有複雜關聯關係的類。

∮. 當你調用 EF Core 的 Add 命令來添加一個新條目時,會發生很多事情:

  • EF Core 查找添加的類和其他類的所有關聯。對於每個關聯的類,它也會判斷是否需要在數據庫中創建一個新行,或者僅僅鏈接到數據庫中現有的行。
  • 它使用現有行的主鍵或偽主鍵為新添加的條目填充外鍵信息。

∮. EF Core 可以監測你從數據庫讀取的類的屬性的變化。它通過已讀入的類的隱藏副本來實現這一點。當你調用 SaveChanges 時,它會將每個讀入的屬性值與其原始值進行比較,並且會創建相應的數據更新命令。

∮. EF Core 的 Remove 方法將刪除參數提供的實體類的主鍵所指向的數據行。如果被刪除的類有外鍵關聯,那麼數據庫會自動進行相關的操作(比如級聯刪除),但你可以更改刪除的規則。

數據寫入基礎

提示:如果你已經對 EF Core 有一定的了解,那麼你可以跳過這一部分,這隻是一個簡單的 EF Core 寫入數據的例子。

在這一節的介紹中,我將描述一下本文用到的數據庫結構,然後給出一個簡單的數據庫寫入示例。下面是類/表的基本關係圖:

這些表被映射到具有類似名稱的類,例如 Book、BookAuthor、Author,這些類的屬性名稱與表的字段名稱相同。由於篇幅有限,我不打算展開來講這些類,但您可以在我的 GitHub 倉庫[1]中查看這些類。

和讀取數據一樣,EF Core 將數據寫入數據庫也是五部分:

  1. 數據庫服務器,如 SQL server, Sqlite, PostgreSQL…
  2. 映射到數據庫的一個類或多個類—我將它們稱為實體類
  3. 一個繼承 EF Core 的 DbContext 的類,該類包含 EF Core 的配置
  4. 一個創建數據庫的方法
  5. 最後,向數據庫寫入數據的命令

下面的單元測試代碼來自我的 GitHub 創庫[2],展示了一個簡單的示例,它從現有數據庫中讀取 4 個 Book 實體及其關聯的 BookAuthor 和 Authors 實體。

[Fact]
public void TestWriteTestDataSqliteInMemoryOk()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<EfCoreContext>();
    using (var context = new EfCoreContext(options))
    {
        context.Database.EnsureCreated();

        //ATTEMPT
        var book = new Book
        {
            Title = "Test",
            Reviews = new List<Review>()
        };
        book.Reviews.Add(new Review { NumStars = 5 });
        context.Add(book);
        context.SaveChanges();

        //VERIFY
        var bookWithReview = context.Books
            .Include(x => x.Reviews).Single()
        bookWithReview.Reviews.Count.ShouldEqual(1);
    }
}

現在,如果我們將單元測試代碼對應到上面的 5 部分,結果是這樣的:

  1. 數據庫服務器——第 5 行:我選擇了一個 Sqlite 數據庫服務器,在本例中是 SqliteInMemory.CreateOptions 方法,它使用我的一個 NuGet 包 EfCore.TestSupport 創建了一個內存數據庫(內存中的數據庫對於單元測試非常有用,因為你可以為這個測試建立一個新的空數據庫)。
  2. 實體類——和上一篇結構差不多,但是多了一個與 Book 關聯的 Review 實體類。
  3. 一個繼承 DbContext 的類——第 6 行:EfCoreContext 類繼承了 DbContext 類並配置了從類到數據庫的映射關係(你可以在我的 GitHub 倉庫[3] 中查看該類)。
  4. 一個創建數據庫的方法——第 8 行:第一次執行時,這句代碼會創建一個新的數據庫,包括創建正確的表、鍵、索引等。EnsureCreated 方法用於單元測試,但對於真實的應用程序,你最好手動執行 EF Core 的 Migration 命令。
  5. 向數據庫寫入數據的命令——第 17 到 18 行:
    • 第 17 行:Add 方法告訴 EF Core 需要將一個 Book 實體及其關係(在本例中,只是一個 Review 實體)寫入數據庫。
    • 第 18 行:SaveChange 方法將在數據庫中的 Books 和 Reviews 表中創建新行。

在 //VERIFY 註釋之後的最後幾行用來檢查數據是否已經被寫入數據庫。

在本例中,你向數據庫添加了新的記錄(SQL 的 INSERT INTO 命令)。EF Core 也可以處理更新和刪除數據庫的數據,下一節介紹這個新增示例,然後介紹其他新增、更新和刪除的示例。

寫入數據時數據庫端發生了什麼

我將從創建一個新的 Book 實體類和新的 Review 實體類開始。這兩個類的關係比較簡單。使用上面單元測試的例子,主要代碼如下:

var book = new Book
{
    Title = "Test",
    Reviews = new List<Review>()
};
book.Reviews.Add(new Review { NumStars = 1 });
context.Add(book);
context.SaveChanges();

為了將這兩個實體添加到數據庫,EF Core 需要這樣做:

  1. 確定它應該以什麼順序創建這些新行——在本例中,它必須在 Books 表中創建一行,這樣它就擁有 Books 的主鍵。
  2. 將主鍵複製到與其關聯的外鍵——在本例中,它將 Books 中的主鍵 BookId 複製到 Review 的外鍵。
  3. 複製數據庫中新創建的數據,以便實體類正確表示數據庫的數據——在這種情況下,它必須複製 BookId 並更新 BookId 屬性,包括 Book 和 Review 實體類以及 Review 實體類的 ReviewId。

下面我們看看上面代碼生成的 SQL 語句:

-- 第一次訪問數據庫
SET NOCOUNT ON;
-- 向數據庫的 Books 表生成一條新數據.
-- 數據庫生成 Books 的主鍵值
INSERT INTO [Books] ([Description], [Title], ...)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);

-- 返回主鍵值,檢查並確認數據行是否已添加
SELECT [BookId] FROM [Books]
WHERE @@ROWCOUNT = 1 AND [BookId] = scope_identity();

-- 第二次訪問數據庫
SET NOCOUNT ON;
-- 向數據庫的 Review 表生成一條新數據.
-- 數據庫生成 Review 的主鍵值
INSERT INTO [Review] ([BookId], [Comment], ...)
VALUES (@p7, @p8, @p9, @p10);

-- 返回主鍵值,檢查並確認數據行是否已添加
SELECT [ReviewId] FROM [Review]
WHERE @@ROWCOUNT = 1 AND [ReviewId] = scope_identity();

重要的一點是,EF Core 是按正確的順序處理實體類的,這樣它就可以填充外鍵。這是簡單的例子,但我遇到一個客戶項目的例子是,我不得不建立一個非常複雜的數據組成的 15 個不同的實體類,一些實體類是新增,一些是更新和刪除,EF Core 通過一個 SaveChanges 將把所有工作有序地完成了庫。因此,EF Core 使開發者可以很容易地將複雜的數據寫入數據庫。

我之所以提到這一點,是因為我看到過在 EF Core 代碼中,開發人員多次調用 SaveChanges 方法來從第一個新增命令中獲得主鍵,並把它設置為相關實體的外鍵。例如:

var book = new Book
{
    Title = "Test"
};
context.Add(book);
context.SaveChanges();
var review = new Review { BookId = book.BookId, NumStars = 1 }
context.Add(review);
context.SaveChanges();

雖然這代碼效果是一樣的,但它有一個缺陷——如果第二 SaveChanges 失敗,那麼就會發生部分數據更新到數據庫的情況。在某種情況下,這可能不是個問題,但對於像我客戶那種需要保證數據一致的情況,就非常糟糕了。

因此,從中得到的收穫是,您不需要將主鍵複製到外鍵中,因為你可以設置導航屬性,EF Core 將為您挑選出外鍵。因此,如果你認為需要調用兩次 SaveChanges,那麼通常意味着你沒有設置正確的導航屬性來處理這種情況。

寫數據時 DbContext 做了什麼

在上一節中,你看到了 EF Core 在數據庫端做了什麼,現在你要看看在 EF Core 中發生了什麼。大多數情況,你不需要知道,但有時候知道這些是非常重要的。例如,你只能在 SaveChanges 之前捕獲數據的狀態。而對於自增主鍵,你只有在 SaveChanges 被調用之後才能拿到主鍵的值。

與上一個示例相比,這個示例稍微複雜一些。在這個示例中,我想向你展示 EF Core 通過從數據庫中讀取的已有實體類的實例來處理另一個實體類的新實例。下面的代碼創建了一個新的 Book,但 Author 已經在數據庫中了。代碼註明了階段 1、階段 2 和階段 3,然後我用圖表描述每個階段發生的事情。

// 階段 1
var author = context.Authors.First();
var bookAuthor = new BookAuthor { Author = author };
var book = new Book
{
    Title = "Test Book",
    AuthorsLink = new List<BookAuthor> { bookAuthor }
};

// 階段 2
context.Add(book);

// 階段 3
context.SaveChanges();

接下來的三個圖向你展示了實體類及其跟蹤數據在每個階段內發生的事情。每個圖显示了其階段結束時的以下數據:

  • 流程的每個階段中每個實例的狀態。
  • Book 和 BookAuthor 類是棕色的,表示它們是類的新實例,需要添加到數據庫中,而 Author 實體類是藍色的,表示從數據庫中讀取的實例。
  • 主鍵和外鍵旁邊的括號是其當前的值。如果一個鍵是 (0),那麼它還沒有被設值。
  • 箭頭連線連接的是從導航屬性到其相應實體類。
  • 每個階段之間的變化通過粗體文本或箭頭連線的粗線显示。

下圖显示了階段 1 完成后的情況。用於設置一個新的 Book 實體類(左)和一個新的 BookAuthor 實體類(中),後者將 Book 連接接到一個現有的 Author 實體類(右)。

階段 1 這是調用任何 EF Core 方法之前的起點。

下一個圖显示了執行 context.Add(book) 之後的情況。更改部分以粗體显示。

你可能會驚訝於執行 Add 方法時所發生的事情。它將作為參數提供的實體的狀態設置為 Added(在本例中為 Book 實體)。然後通過導航屬性或外鍵值查看與該實體連接的所有實體。對於每個被連接的實體,它會執行以下操作(注意:我不知道它們執行的確切順序)。

  • 如果實體未被跟蹤(即其當前狀態為 Detached),則將其狀態設置為 Added——在本例中,它是 BookAuthor 實體。
  • 它用主鍵的值填充正確的外鍵的值。如果連接的主鍵還不可用,它將為跟蹤的主鍵和外鍵數據的 CurrentValue 屬性設置一個惟一的負數。你可以在上圖中看到這一點。
  • 它填充當前未設值的導航屬性——如上圖中所示。

最後一個階段,即階段 3,是調用 SaveChanges 方法時發生的情況,如圖所示。

在“寫數據時數據庫端發生了什麼”一節中,數據庫更改的任何列都被複制回實體類中,以便實體與數據庫匹配。在本例中,數據更新到數據庫時會把主鍵值更新到 Book 的 BookId 和 BookAuthor 的 BookId。
而且,此次數據庫寫入完成后,涉及的所有實體的狀態都會被更新為 Unchanged。

對於上面這樣一個很長的解釋,很多時候你不需要知道這些細節,你只管它“工作了”就行。但是,當某些東西不能正常工作或者想做一些複雜的事情時,比如記錄實體類的更改,那麼了解這個就非常有用。

更新數據到數據庫時發生了什麼

上面的示例是關於向數據庫添加新記錄的,但是沒有進行更新。在這一節中,我將展示當你更新數據庫中已有的記錄時會發生什麼。這裏使用我上一篇文章“EF Core 讀取數據時發生了什麼?”中講到的查詢例子。

這個更新很簡單,只有三行,但是它在代碼中有三個階段:讀取、更新和保存。

var books = context.Books.ToList();
books.First().PublishedOn = new DateTime(2020, 1, 1);
context.SaveChanges();

下圖展示了這三個階段:

如你所見,你使用的查詢類型很重要——普通查詢加載數據並把返回的實體保存一份“跟蹤快照”,返回的實體類被稱為“被跟蹤的”。如果實體沒有沒跟蹤,則無法更新它。

注意:上一節中的 Author 實體類也是被“跟蹤”的。在這個例子中,Author 的跟蹤狀態告訴 EF Core Author 已經在數據庫中,因此不會再次創建。

因此,如果你更改了加載的跟蹤實體類中的任何屬性,那麼當你調用 SaveChanges 時,它會將所有跟蹤的實體類與它們的跟蹤快照進行比較。對於每個類,它遍歷映射到數據庫字段的所有屬性。這個過程稱為更改跟蹤,它將檢測被跟蹤實體中的每一個更改,包括 Title、PubishedOn 等非關係屬性。

在這個簡單的示例中,只有 4 個 Book 實體,但在實際應用程序中,您可能已經加載了許多相互連接的實體類。在這一點上,比較階段可能需要一段時間。因此,你應該嘗試只加載需要更改的實體類。

注意:EF Core 有一個名為 Update 的命令,它用於更新每個屬性/列的特定情況。EF Core 會自動跟蹤更改,默認只更新已更改的屬性/列。

每次更新都將創建一個 SQL UPDATE 命令,所有這些更新都將在一個 SQL 事務中執行。使用 SQL 事務意味着所有更新都作為一個整體,如果其中任何一部分失敗,那麼事務中的任何數據庫更改都會失效。

從數據庫刪除數據時發生了什麼

CRUD 的最後一部分是 DELETE,這在某些情況很簡單,你只需要調用 context.Remove()。在另一些情況它很複雜,例如,當你刪除另一個實體類依賴的實體類時會發生什麼?

刪除映射到數據庫的實體類的方法是 Remove。舉個例子,我加載一個特定的 Book,然後刪除它。

var book = context.Books
    .Single(p => p.Title == "Quantum Networking");
context.Remove(book);
context.SaveChanges();

它的階段如下:

  1. 加載要刪除的 Book 實體類。這會獲取它的所有屬性數據,但對於刪除,您實際上只需要實體類的主鍵。
  2. 調用 Remove 方法其實是將 Book 的狀態標記為 Deleted。這些信息會有序地存儲在跟蹤快照中。
  3. 最後,SaveChanges 創建一個 SQL DELETE 命令,該命令與任何其他數據庫更改一起發送到數據庫,並且在一個 SQL 事務中。

這看起來很簡單,但這裏發生了一些重要的事情,從代碼看並不明顯。原來書名為“Quantum Networking”的書有其他一些實體類關聯到到它——在某個特定的測試用例中,書名為“Quantum Networking”的書關聯到以下實體類:

  • 兩個 Review
  • 一個 PriceOffer
  • 一個 BookAuthor

現在,Review、PriceOffer 和 BookAuthor 實體類只與這本書相關——我們使用術語叫依賴於 Book 實體類。因此,如果這本書被刪除了,那麼這些 Review、PriceOffer 和所關聯的 BookAuthor 數據行也應該被刪除。如果不刪除,那麼數據庫的關聯關係就是不正確的,SQL 數據庫將拋出異常。那麼,為什麼做這個刪除工作?

這裏所發生的都是因為設置了級聯刪除,級聯刪除規則設置了 Books 表和三個依賴表之間的數據庫關係。
下面是 EF Core 為創建 Review 表而生成的 SQL 命令的一個示例:

CREATE TABLE [Review] (
    [ReviewId] int NOT NULL IDENTITY,
    [VoterName] nvarchar(max) NULL,
    [NumStars] int NOT NULL,
    [Comment] nvarchar(max) NULL,
    [BookId] int NOT NULL,
    CONSTRAINT [PK_Review] PRIMARY KEY ([ReviewId]),
    CONSTRAINT [FK_Review_Books_BookId] FOREIGN KEY ([BookId])
         REFERENCES [Books] ([BookId]) ON DELETE CASCADE
);

CONSTRAINT 語句部分定義了約束規則,該約束表示 Review 通過 BookId 列鏈接到 Books 表中的一行。在該約束的最後,你將看到關於 DELETE 級聯的規則。它告訴數據庫,如果它鏈接的書被刪除了,那麼這個 Review 也應該被刪除。這意味着書的刪除是允許的,因為所有相關的行也被刪除了。

這是非常有用的,但有時候想要更改刪除規則怎麼辦?比如我決定不允許刪除客戶訂單中存在的書。為了做到這一點,我在 DbContext 中添加了一些 EF Core 配置來改變刪除規則,如下:

public class EfCoreContext : DbContext
{
    private readonly Guid _userId;

    public EfCoreContext(DbContextOptions<EfCoreContext> options)
        : base(options)

    public DbSet<Book> Books { get; set; }
    //… 其它 DbSet<T>

    protected override void OnModelCreating(ModelBuilder modelBuilder
    {
        //… 其它代碼

        modelBuilder.Entity<LineItem>()
            .HasOne(p => p.ChosenBook)
            .WithMany()
            .OnDelete(DeleteBehavior.Restrict);
    }
}

一旦該配置應用到數據庫,就不會生成 SQL 語句的 DELETE CASCADE。這意味着,如果你試圖刪除客戶訂單中的一本書,那麼數據庫將返回一個錯誤,EF Core 將把這個錯誤變成一個異常。

這使你對正在發生的事情有一個更深的了解,但是還有相當多的內容我沒有介紹(但我在我的書中介紹了)。這裡有一些關於刪除我還沒有提到的事情:

  • 實體類之間可以有 required 關係(依賴關係)和 optional 關係,EF Core 為每種類型使用不同的規則。
  • EF Core 可以通過設置 DeleteBehavior 來設置級聯刪除規則,當實體類存在循環關聯關係時,可以用它避免一些錯誤——一些數據庫在發現循環刪除時會拋出錯誤。
  • 你可以在調用 Remove 方法時提供一個新的只有主鍵有值的類來刪除實體類。這在處理只返回主鍵的場景非常有用。

總結

本文我介紹了 CRUD 中的新增、更新和刪除部分,前一篇文章介紹了讀取部分。

正如您所看到的,使用 EF Core 在數據庫中創建記錄很容易,但內部很複雜。你通常不需要知道 EF Core 或數據庫中發生了什麼,但了解一些細節可以讓你更好地利用 EF Core 的優勢。

更新也很簡單——只需在你讀入的實體類中更改一個或多個屬性,當你調用 SaveChanges 時,EF Core 會找到已更改的數據,並構建 SQL 命令更新數據庫。這適用於非關係屬性(如圖 Book 的 Title 屬性)和導航屬性(你可以在他們的關係)。

最後,我們看了一個刪除案例。同樣很容易使用,但很多處理也是在背後執行的。​ 另外,敬請關注我的下一篇文章,我將討論所謂的“軟刪除”。如果你設置了一個標誌,EF Core 就不會再看到這個實體類了,它仍然在數據庫中,但它是隱藏的。

希望本文對你有用,也希望你關注本系列的更多文章。

祝你編程愉快!

[1]. https://bit.ly/2MXK3ZY
[2]. https://bit.ly/2Yza7QQ
[3]. https://bit.ly/2Y0UORO

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

【其他文章推薦】

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

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

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

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

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

聚甘新

Redis系列(五):數據結構List雙向鏈表源碼解析和API實現

1.介紹

Redis中List是通過ListNode構造的雙向鏈表。

特點:

1.雙端:獲取某個結點的前驅和後繼結點都是O(1)

2.無環:表頭的prev指針和表尾的next指針都指向NULL,對鏈表的訪問都是以NULL為終點

3.帶表頭指針和表尾指針:獲取表頭和表尾的複雜度都是O(1)

4.帶鏈表長度計數器:len屬性記錄,獲取鏈表長度O(1)

5.多態:鏈表結點使用void*指針來保存結點的值,並且可以通過鏈表結構的三個函數為結點值設置類型特定函數,所以鏈表可以保存各種不同類型的值

雙向鏈表詳解:https://www.cnblogs.com/vic-tory/p/13140779.html

中文網:http://redis.cn/commands.html#list

2.源碼解析

// listNode 雙端鏈表節點
typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *next;

    // 節點的值
    void *value;

} listNode;

 

// list 雙端鏈表
typedef struct list { // 在c語言中,用結構體的方式來模擬對象是一種常見的手法

    // 表頭節點
    listNode *head;

    // 表尾節點
    listNode *tail;

    // 節點值複製函數
    void *(*dup)(void *ptr);

    // 節點值釋放函數
    void(*free)(void *ptr);

    // 節點值對比函數
    int(*match)(void *ptr, void *key);

    // 鏈表所包含的節點數量
    unsigned long len;

} list;

 

/* 作為宏實現的函數 */
//獲取長度
#define listLength(l) ((l)->len)
//獲取頭節點
#define listFirst(l) ((l)->head)
//獲取尾結點
#define listLast(l) ((l)->tail)
//獲取前一個結點
#define listPrevNode(n) ((n)->prev)
//獲取后一個結點
#define listNextNode(n) ((n)->next)
//獲取結點的值 是一個void類型指針
#define listNodeValue(n) ((n)->value)

/* 下面3個函數主要用來設置list結構中3個函數指針,參數m為method的意思 */
#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))

/* 下面3個函數主要用來獲取list結構的單個函數指針 */
#define listGetDupMethod(l) ((l)->dup)
#define listGetFree(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)

3.API實現

listCreate函數:創建一個不包含任何結點的新鏈表

/*
 * listCreate 創建一個新的鏈表
 *
 * 創建成功返回鏈表,失敗返回 NULL 。
 *
 * T = O(1)
 */
list *listCreate(void)
{
    struct list *list;

    // 分配內存
    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL;//內存分配失敗則返回NULL

    // 初始化屬性
    list->head = list->tail = NULL;//空鏈表
    list->len = 0;
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;

    return list;
}

listAddNodeHead函數:將一個包含給定值的新結點添加到給定鏈表的表頭

/*
 * listAddNodeHead 將一個包含有給定值指針 value 的新節點添加到鏈表的表頭
 *
 * 如果為新節點分配內存出錯,那麼不執行任何動作,僅返回 NULL
 *
 * 如果執行成功,返回傳入的鏈表指針
 *
 * T = O(1)
 */
list *listAddNodeHead(list *list, void *value)
{
    listNode *node;

    // 為節點分配內存
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;

    // 保存值指針
    node->value = value;

    // 添加節點到空鏈表
    if (list->len == 0) {
        list->head = list->tail = node;
        //該結點的前驅和後繼都為NULL
        node->prev = node->next = NULL;
    }
    else { // 添加節點到非空鏈表
        node->prev = NULL;
        node->next = list->head;
        list->head->prev = node;
        list->head = node;
    }

    // 更新鏈表節點數
    list->len++;

    return list;
}

listAddNodeTail函數:將一個包含給定值的新結點插入到給定鏈表的表尾

/*
 * listAddNodeTail 將一個包含有給定值指針 value 的新節點添加到鏈表的表尾
 *
 * 如果為新節點分配內存出錯,那麼不執行任何動作,僅返回 NULL
 *
 * 如果執行成功,返回傳入的鏈表指針
 *
 * T = O(1)
 */
list *listAddNodeTail(list *list, void *value)
{
    listNode *node;

    // 為新節點分配內存
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;

    // 保存值指針
    node->value = value;

    // 目標鏈表為空
    if (list->len == 0) {
        list->head = list->tail = node;
        node->prev = node->next = NULL;
    }//目標鏈非空
    else {
        node->prev = list->tail;
        node->next = NULL;
        list->tail->next = node;
        list->tail = node;
    }

    // 更新鏈表節點數
    list->len++;

    return list;
}

listInsertNode函數:將一個給定值的新結點插入到給定結點之前或者之後

/*
 * listInsertNode 創建一個包含值 value 的新節點,並將它插入到 old_node 的之前或之後
 *
 * 如果 after 為 0 ,將新節點插入到 old_node 之前。
 * 如果 after 為 1 ,將新節點插入到 old_node 之後。
 *
 * T = O(1)
 */
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
    listNode *node;

    // 創建新節點
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;

    // 保存值
    node->value = value;

    // 將新節點添加到給定節點之後
    if (after) {
        node->prev = old_node;
        node->next = old_node->next;
        // 給定節點是原表尾節點
        if (list->tail == old_node) {
            list->tail = node;
        }
    }
    // 將新節點添加到給定節點之前
    else {
        node->next = old_node;
        node->prev = old_node->prev;
        // 給定節點是原表頭節點
        if (list->head == old_node) {
            list->head = node;
        }
    }

    // 更新新節點的前置指針
    if (node->prev != NULL) {
        node->prev->next = node;
    }
    // 更新新節點的後置指針
    if (node->next != NULL) {
        node->next->prev = node;
    }

    // 更新鏈表節點數
    list->len++;

    return list;
}

listDelNode函數:從指定的list中刪除給定的結點

/*
 * listDelNode 從鏈表 list 中刪除給定節點 node
 *
 * 對節點私有值(private value of the node)的釋放工作由調用者進行。該函數一定會成功.
 *
 * T = O(1)
 */
void listDelNode(list *list, listNode *node)
{
    // 調整前置節點的指針
    if (node->prev)
        node->prev->next = node->next;
    else
        list->head = node->next;

    // 調整後置節點的指針
    if (node->next)
        node->next->prev = node->prev;
    else
        list->tail = node->prev;

    // 釋放值
    if (list->free) list->free(node->value);

    // 釋放節點
    zfree(node);

    // 鏈表數減一
    list->len--;
}

listRelease函數:釋放給定鏈表以及鏈表中所有結點

 

/*
 * listRelease 釋放整個鏈表,以及鏈表中所有節點, 這個函數不可能會失敗.
 *
 * T = O(N)
 */
void listRelease(list *list)
{
    unsigned long len;
    listNode *current, *next;

    // 指向頭指針
    current = list->head;
    // 遍歷整個鏈表
    len = list->len;
    while (len--) {
        next = current->next;

        // 如果有設置值釋放函數,那麼調用它
        if (list->free) list->free(current->value);

        // 釋放節點結構
        zfree(current);

        current = next;
    }

    // 釋放鏈表結構
    zfree(list);
}

 

該函數不僅釋放了表結點的內存還釋放了表結構的內存

 listGetIterator函數:為給定鏈表創建一個迭代器

在講這個函數之前,我們應該先看看鏈表迭代器的結構:

// listIter 雙端鏈表迭代器
typedef struct listIter {

    // 當前迭代到的節點
    listNode *next;

    // 迭代的方向
    int direction;

} listIter;

迭起器只有兩個重要的屬性:當前迭代到的結點,迭代的方向

下面再看看鏈表的迭代器創建函數

/*
 * listGetIterator 為給定鏈表創建一個迭代器,
 * 之後每次對這個迭代器調用 listNext 都返回被迭代到的鏈表節點,調用該函數不會失敗
 *
 * direction 參數決定了迭代器的迭代方向:
 *  AL_START_HEAD :從表頭向表尾迭代
 *  AL_START_TAIL :從表尾想表頭迭代
 *
 * T = O(1)
 */
listIter *listGetIterator(list *list, int direction)
{
    // 為迭代器分配內存
    listIter *iter;
    if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;

    // 根據迭代方向,設置迭代器的起始節點
    if (direction == AL_START_HEAD)
        iter->next = list->head;
    else
        iter->next = list->tail;

    // 記錄迭代方向
    iter->direction = direction;

    return iter;
}

listReleaseIterator函數:釋放指定的迭代器

/*
 * listReleaseIterator 釋放迭代器
 *
 * T = O(1)
 */
void listReleaseIterator(listIter *iter) {
    zfree(iter);
}

listRewind函數和listRewindTail函數:迭代器重新指向表頭或者表尾的函數

 

/*
 * 將迭代器的方向設置為 AL_START_HEAD,
 * 並將迭代指針重新指向表頭節點。
 *
 * T = O(1)
 */
void listRewind(list *list, listIter *li) {
    li->next = list->head;
    li->direction = AL_START_HEAD;
}

/*
 * 將迭代器的方向設置為 AL_START_TAIL,
 * 並將迭代指針重新指向表尾節點。
 *
 * T = O(1)
 */
void listRewindTail(list *list, listIter *li) {
    li->next = list->tail;
    li->direction = AL_START_TAIL;
}

listNext函數:返回當前迭代器指向的結點

 

/*
 * 返回迭代器當前所指向的節點。
 *
 * 刪除當前節點是允許的,但不能修改鏈表裡的其他節點。
 *
 * 函數要麼返回一個節點,要麼返回 NULL,常見的用法是:
 *
 * iter = listGetIterator(list,<direction>);
 * while ((node = listNext(iter)) != NULL) {
 *     doSomethingWith(listNodeValue(node));
 * }
 *
 * T = O(1)
 */
listNode *listNext(listIter *iter)
{
    listNode *current = iter->next;

    if (current != NULL) {
        // 根據方向選擇下一個節點
        if (iter->direction == AL_START_HEAD)
            // 保存下一個節點,防止當前節點被刪除而造成指針丟失
            iter->next = current->next;
        else
            // 保存下一個節點,防止當前節點被刪除而造成指針丟失
            iter->next = current->prev;
    }

    return current;
}

 

 

 

該函數保持了當前結點的下一個結點,避免了當前結點被刪除而迭代器無法繼續迭代的尷尬情況

 listDup函數:複製整個鏈表,返回副本

 

/*
 * 複製整個鏈表。
 *
 * 複製成功返回輸入鏈表的副本,
 * 如果因為內存不足而造成複製失敗,返回 NULL 。
 *
 * 如果鏈表有設置值複製函數 dup ,那麼對值的複製將使用複製函數進行,
 * 否則,新節點將和舊節點共享同一個指針。
 *
 * 無論複製是成功還是失敗,輸入節點都不會修改。
 *
 * T = O(N)
 */
list *listDup(list *orig)
{
    list *copy;//鏈表副本
    listIter *iter;//鏈表迭代器
    listNode *node;//鏈表結點

    // 創建新的空鏈表
    if ((copy = listCreate()) == NULL)
        return NULL;//創建空的鏈表失敗則返回NULL

    // 設置副本鏈表的節點值處理函數
    copy->dup = orig->dup;
    copy->free = orig->free;
    copy->match = orig->match;

    //獲取輸入鏈表的迭代器
    iter = listGetIterator(orig, AL_START_HEAD);
    
    //遍歷整個輸入鏈表進行複製
    while ((node = listNext(iter)) != NULL) {
        
        //副本結點值
        void *value;

        // 存在複製函數
        if (copy->dup) {
            
            //調用複製函數複製
            value = copy->dup(node->value);
         
            //複製結果為空,說明複製失敗
            if (value == NULL) {
                
                //複製失敗則釋放副本鏈表和迭代器,避免內存泄漏
                listRelease(copy);
                listReleaseIterator(iter);
            
                return NULL;
            }
        }
        //不存在複製函數 則直接指針指向
        else
            value = node->value;

        // 將節點添加到副本鏈表 
        if (listAddNodeTail(copy, value) == NULL) {
                
            //如果不能成功添加,則釋放副本鏈表和迭代器,避免內存泄漏
            listRelease(copy);
            listReleaseIterator(iter);
        
            return NULL;
        }
    }

    // 釋放迭代器
    listReleaseIterator(iter);

    // 返回副本
    return copy;
}

 

如果複製失敗則要注意釋放副本鏈表和迭代器,避免內存泄漏

 listSearchKey函數:查找list中值和key匹配的結點

 

/*
 * 查找鏈表 list 中值和 key 匹配的節點。
 *
 * 對比操作由鏈表的 match 函數負責進行,
 * 如果沒有設置 match 函數,
 * 那麼直接通過對比值的指針來決定是否匹配。
 *
 * 如果匹配成功,那麼第一個匹配的節點會被返回。
 * 如果沒有匹配任何節點,那麼返回 NULL 。
 *
 * T = O(N)
 */
listNode *listSearchKey(list *list, void *key)
{
    listIter *iter;//鏈表迭代器
    listNode *node;//鏈表結點

    //獲得鏈表迭代器
    iter = listGetIterator(list, AL_START_HEAD);

    //遍歷整個鏈表查詢
    while ((node = listNext(iter)) != NULL) {

        //存在比較函數
        if (list->match) {

            //利用比較函數進行比較
            if (list->match(node->value, key)) {

                //返回目標結點之前釋放迭代器空間,避免內存泄漏
                listReleaseIterator(iter);

                return node;
            }
        }
        //不存在比較函數
        else {
            //直接比較
            if (key == node->value) {

                //返回目標結點之前釋放迭代器空間,避免內存泄漏
                listReleaseIterator(iter);
                // 找到
                return node;
            }
        }
    }

    //返回目標結點之前釋放迭代器空間,避免內存泄漏
    listReleaseIterator(iter);

    // 未找到
    return NULL;
}

listIndex函數:返回鏈表在給定索引上的值

 

/*
 * 返回鏈表在給定索引上的值。
 *
 * 索引以 0 為起始,也可以是負數, -1 表示鏈表最後一個節點,諸如此類。
 *
 * 如果索引超出範圍(out of range),返回 NULL 。
 *
 * T = O(N)
 */
listNode *listIndex(list *list, long index) {
    
    listNode *n;//鏈表結點

    
    /* n不用設置成NULL的原因:
    如果索引超出範圍,
    那肯定是找到表頭或者表尾沒有找到,
    表頭的前驅和表尾的後繼都是NULL,
    所以這裏n不用設置為NULL,直接設置也可以*/
    
    // 如果索引為負數,從表尾開始查找
    if (index < 0) {
        
        //變成正數,方便索引
        index = (-index) - 1;
    
        //從尾部開始找
        n = list->tail;
        
        //尋找 因為從尾部開始找,所以是前驅
        while (index-- && n) n = n->prev;
        
    }
    
    // 如果索引為正數,從表頭開始查找
    else {
        
        //從頭部開始找
        n = list->head;
    
        //尋找 因為從頭部開始找,所以是後繼
        while (index-- && n) n = n->next;
    }

    return n;
}

listRotate函數:取出鏈表的表尾結點放到表頭,成為新的表頭結點

/*
 * 取出鏈表的表尾節點,並將它移動到表頭,成為新的表頭節點。
 *
 * T = O(1)
 */
void listRotate(list *list) {
    
    //表尾結點
    listNode *tail = list->tail;

    //如果鏈表中只有一個元素,那麼表頭就是表尾,可以直接返回
    if (listLength(list) <= 1) return;

    // 重新設置表尾節點
    list->tail = tail->prev;
    list->tail->next = NULL;

    // 插入到表頭
    list->head->prev = tail;
    tail->prev = NULL;
    tail->next = list->head;
    list->head = tail;
}

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

聚甘新

html/css 滾動到元素位置,显示加載動畫

每次滾動到元素時,都显示加載動畫,如何添加?

 

元素添加初始參數

以上圖中的動畫為例,添加倆個左右容器,將內容放置在容器內部。

添加初始數據,默認透明度0、左右分別移動100px。

 1   //左側容器
 2   .item-leftContainer {
 3     opacity: 0;
 4     transform: translateX(-100px);
 5   }
 6   //右側容器
 7   .item-rightContainer {
 8     opacity: 0;
 9     transform: translateX(100px);
10   }

添加動畫數據

在less中添加動畫數據。這裏只設置了to,也可以省略第1步中的初始參數設置而在動畫里設置from。

執行后,透明度由0到1,倆個容器向中間移動100px回到原處。

 1   //動畫
 2   @keyframes showLeft {
 3     to {
 4       opacity: 1;
 5       transform: translateX(0px);
 6     }
 7   }
 8   @keyframes showRight {
 9     to {
10       opacity: 1;
11       transform: translateX(0px);
12     }
13   }
14   @keyframes hideLeft {
15     to {
16       opacity: 0;
17       transform: translateX(-100px);
18     }
19   }
20   @keyframes hideRight {
21     to {
22       opacity: 0;
23       transform: translateX(100px);
24     }
25   }

觸發動畫

頁面加載/刷新觸發 – 在componentDidMount中執行

頁面滾動時觸發 – 在componentDidMount、componentWillUnmount添加監聽/註銷頁面滾動的事件

校驗當前滾動高度與元素的位置差異:

window.pageYOffset(滾動距離) + windowHeight(窗口高度) > leftElement.offsetTop (元素的相對位置)+ parentOffsetTop(父元素的相對位置) + 200

  1. 真正的滾動視覺位置 – window.pageYOffset(滾動距離) + windowHeight(窗口高度)
  2. 元素距離頂部的高度 – 這裏使用了leftElement.offsetTop + parentOffsetTop,原因是父容器使用了absolute絕對定位。如果是正常布局的話,使用元素當前的位置leftElement.offsetTop即可
  3. 額外添加200高度,是為了優化視覺體驗。當超出200高度時才觸發動畫

當頁面滾動到下方,觸發显示動畫;當頁面重新滾動到上方,觸發隱藏動畫。

 1     componentDidMount() {
 2         this.checkScrollHeightAndLoadAnimation();
 3         window.addEventListener('scroll', this.bindHandleScroll);
 4     }
 5     componentWillUnmount() {
 6         window.removeEventListener('scroll', this.bindHandleScroll);
 7     }
 8     bindHandleScroll = (event) => {
 9         this.checkScrollHeightAndLoadAnimation();
10     }
11     checkScrollHeightAndLoadAnimation() {
12         const windowHeight = window.innerHeight;
13         let parentEelement = document.getElementById("softwareUsingWays-container") as HTMLElement;
14         const parentOffsetTop = parentEelement.offsetTop;
15         let leftElement = (parentEelement.getElementsByClassName("item-leftContainer") as HTMLCollectionOf<HTMLElement>)[0];
16         if (window.pageYOffset + windowHeight > leftElement.offsetTop + parentOffsetTop + 200) {
17             leftElement.style.animation = "showLeft .6s forwards" //添加動畫  
18         } else {
19             leftElement.style.animation = "hideLeft 0s forwards" //隱藏動畫 
20         }
21         let rightElement = (parentEelement.getElementsByClassName(".item-rightContainer") as HTMLCollectionOf<HTMLElement>)[0];
22         if (window.pageYOffset + windowHeight > rightElement.offsetTop + parentOffsetTop + 200) {
23             rightElement.style.animation = "showRight .6s forwards" //添加動畫  
24         } else {
25             rightElement.style.animation = "hideRight 0s forwards" //隱藏動畫 
26         }
27     }

 

關鍵字:React 滾動、加載/出現動畫

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

【其他文章推薦】

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

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

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

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

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

聚甘新

Jmeter系列(21)- 詳解 HTTP Request

如果你想從頭學習Jmeter,可以看看這個系列的文章哦

https://www.cnblogs.com/poloyy/category/1746599.html

 

HTTP Request 介紹

用來發送 HTTP、HTTPS 協議請求

 

HTTP Request 界面

字段名 作用
名稱 不多介紹啦,建議自定義一個識別度高的名稱
註釋 對於測試沒有任何影響,僅記錄作用
協議

http或https,大小寫不敏感

默認:http

服務器名稱或IP
  • 服務器 host 或者 ip,不包括協議
  • 比如:www.baidu.com、192.168.196.128
端口號 目標服務器的端口號,默認:80
方法 發送 http 請求的方法
路徑
  • 目標請求的 URL 路徑
  • 不包括協議、host、ip、端口
內容編碼 請求的編碼方式,默認:iso8859
自動重定向
  • 發出的請求的響應碼是3**,會自動跳轉到新目標頁面
  • 只記錄最終頁面的返回結果
跟隨重定向
  • 和自動重定向唯一不同的是:
  • 會記錄重定向過程中的的所有請求的響應結果
使用 KeepAlive
  • jmeter 和目標服務器之間使用 Keep-Alive 方式進行 HTTP 通信
  • 真正做性能測試強烈建議不勾選
對POST使用multipart/form-data post 請求需要上傳文件時勾選
與瀏覽器兼容的頭
  • 當勾選 multipart/form-data 時,勾選此項
  • http請求頭中的 Content-Type 和Content-Transfer-Encoding 被忽略
  • 而只發送 Content-Disposition 部分

 

Parameters 講解

字段 描述
Name 參數名
Value 參數值
URL Encode?
  • 是否要 URL 編碼?
  • 重點:如果參數值包含了中文、特殊字符(非数字字母以外),最好勾上,當然全都勾上最穩妥
Content-Type
  • 參數值的資源類型
  • 默認:text/plain
Include Equals?
  • 當你的參數值為空的時候,可以選擇不包含=,默認勾選
  • 如果參數值不為空,則不可以取消勾選

 

什麼是 URL 編碼

  • URL 編碼解碼又叫百分號編碼,是統一資源定位(URL)的編碼方式
  • URL 地址(常說網址)規定了数字,字母可以直接使用,另外一批作為特殊用戶字符也可以直接用( / , : @  等),剩下的其它所有字符必須通過 %xx 編碼處理
  • 編碼方法很簡單,在該字符ascii碼的的16進制字符前面加%,如空格字符,ascii碼是32,對應16進制是20,那麼 urlencode 編碼結果是 %20 

 

URL 編碼的栗子

直接在網上搜在線 URL 編解碼

 

include equals 的栗子

參數值為空,且勾選 Include equals

 

參數值為空,但不勾選  Include equals

 

其實說的就是等於號而已,一般也不會傳空值,即使傳了也會帶上=

 

Body Data 講解

  • 沒啥好說的,傳 json 字符串就行了,注意下格式,後面再講具體栗子
  • 不過倒有個重點:如果 Parameters 有參數列表的話,是無法切換到 Body Data 的哦

 

Files Upload 講解

字段 描述
File Path 文件的本地路徑
Parameter Name 參數名
MIME Type 資源媒體類型

 

常見資源媒體類型

類型 文件後綴 格式
超文本標記語言文本 .html text/html
普通文本 .txt text/plain
XML 文件 .xml text/xml
PNG 圖片 .png image/png
GIF .gif image/gif
JPEG 圖片 .jpeg、jpg  image/jpeg

 

類型 文件後綴 格式
表單中進行文件上傳   multipart/form-data
表單默認提交數據的格式   application/x-www-form-urlencoded
XML 數據格式   application/xml
JSON 數據格式   application/json
PDF 文件 .pdf application/pdf
RTF 文本 .rtf application/rtf
GZIP 文件  .gz application/x-gzip
TAR 文件 .tar application/x-tar
AVI 文件 .avi  video/x-msvideo
MPEG 文件 .mpg、.mpeg video/mpeg

 

不同的content-type在jmeter中如何輸入參數

前提

因為是需要真實接口進行測試的,這裏提供兩種方案

  • 自己用 Flask 框架開發了本地的接口進行測試, 如果有需要的同學進群領取哦:870155189
  • 或者進入 http://open.yesapi.cn/?r=user/registration&from=wx_837493986,直接註冊個賬號,弄個免費會員,有在線免費的接口提供測試哦

 

application/x-www-form-urlencoded 的栗子

備註:也是表單提交最常見的栗子

 

Parameters 方式傳參

 

總結

  • 最終表單的參數列表會拼接到 URL 中,所以如果包含了中文、特殊字符就要勾選編碼?
  • 這裏不可以通過 Body Data 傳遞參數哦,會無法識別到參數,已實踐過(即使加了 HTTP請求頭也不行),乖乖用 Parameters 的方式傳參

 

content-type:application/json 的栗子

Body Data 方式傳參

 

添加 HTTP請求頭

 

請求體

 

請求頭

 

結論

重點就是添加 HTTP請求頭,指明 Content-type 是 json 格式

 

content-type:multipart/form-data

重點:用於 post 請求,需要文件上傳的場景;記住不是 get 請求

 

請求參數列表

如果選了 get 方法的話,文件參數是不會生效哦

 

文件參數

 

請求體

 

重點

  • 如果添加了 HTTP請求頭,請務必不要添加 content-type : multipart/form-data 
  • 如果加了的話:那麼所有的請求參數都會被當成文件以二進制形式傳輸,我們 parameters 里的文本格式參數就不會被識別,接口會提示參數為空

 

HTTP Request Advance

說實話我還沒用過這部分的內容,不過還是得了解下每個配置項是什麼意思哦

 

Client implemention 和 Timeouts

字段 描述
implementation 發送http請求的方式,可選項為 java、HttpClient4(默認)
Connect 連接超時時間,單位毫秒
Respones 響應等待超時時間,單位毫秒

 

Embedded Resources from HTML Files

  • 從HTML文件獲取所有內含的資源
  • jmeter 在發出的  HTTP請求獲得響應的 HTML文件內容后,對 HTML進行解析並獲取HTML中包含的所有資源(圖片、flash等)
字段 描述
Retrieve All Embedded Resources 發送http請求的方式,可選項為 java、HttpClient4(默認)
Parallel downloadds. Number

是否使用自設資源處。啟用后可以設置資源池大小,默認為6

URLs must match URL 匹配過濾,填寫此項則只會下載與此內容項匹配的 url 的資源

 

Source address

只用於 HTTP協議且 implemention = HttpClient4 時

字段 描述
IP/Hostname IP /主機名以使用特定的IP地址或(本地)主機名
Device 選擇設備以選擇該接口的第一個可用地址,該設備可以是IPv4或IPv6
Device IPv4 選擇IPv4設備來選擇名稱設備的IPv4地址(如eth0, lo, em0)
Device IPv6 選擇IPv6設備來選擇名稱設備的IPv6地址(如eth0, lo, em0)

 

Proxy Server

代理服務器

字段 描述
Server Name or IP 代理服務器的名稱或者IP地址
Port Number 代理服務器的端口號
Username 代理服務器的用戶名
Password 代理服務器的密碼

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

聚甘新

java併發之synchronized

Java為我們提供了隱式(synchronized聲明方式)和顯式(java.util.concurrentAPI編程方式)兩種工具來避免線程爭用。

本章節探索Java關鍵字synchronized。主要包含以下幾個內容。

  • synchronized關鍵字的使用;
  • synchronized背後的Monitor(管程);
  • synchronized保證可見性和防重排序;
  • 使用synchronized注意嵌套鎖定。

使用方式

synchronized 關鍵字有以下四種使用方式。

  1. 實例方法
  2. 靜態方法
  3. 實例方法中的代碼塊
  4. 靜態方法中的代碼塊
// 實例方法同步和實例方法代碼塊同步
public class SynchronizedTest {
    private int count;
    public void setCountPart(int num) {
        synchronized (this) {
            this.count += num;
        }
    }
    public synchronized void setCount(int num) {
        this.count += num;
    }
}
// 靜態方法同步和靜態方法代碼塊同步
public class SynchronizedTest {
    private static int count;
    public static void setCountPart(int num) {
        synchronized (SynchronizedTest.class) {
            count += num;
        }
    }
    public static synchronized void setCount(int num) {
        count += num;
    }
}

使用關鍵字synchronized實現同步是在JVM內部實現處理,對於應用開發人員來說它是隱式進行的。

每個Java對象都有一個與之關聯的monitor。

當線程調用實例同步方法時,會自動獲取實例對象的monitor。

當線程調用靜態同步方法時,會自動獲取該類Class實例對象的monitor。

Class實例:JVM為每個加載的class創建了對應的Class實例來保存class及interface的所有信息;

Monitor(管程)

Monitor 直譯為監視器,中文圈裡稱為管程。它的作用是讓線程互斥,保護共享數據,另外也可以向其它線程發送滿足條件的信號

如下圖,線程通過入口隊列(Entry Queue)到達訪問共享數據,若有線程佔用轉移等待隊列(Wait Queue),線程訪問共享數據完后觸發通知或轉移到信號隊列(Signal Queue)。

關於管程模型

網上查詢很多文章,大多數羅列 “ Hasen 模型、Hoare 模型和 MESA模型 ”這些名詞,看過之後我還是一知半解。本着對知識的求真,查找溯源,找到了以下資料。

為什麼會有這三種模型?

假設有兩個線程A和B,線程B先進入monitor執行,線程A處於等待。當線程A執行完準備退出的時候,是先退出monitor還是先喚醒線程A?這時就出現了Mesa語義, Hoare語義和Brinch Hansen語義 三種不同版本的處理方式。

Mesa Semantics

Mesa模型中 線程只會出現在WaitQueue,EntryQueue,Monitor。

當線程B發出信號告知線程A時,線程A從WaitQueue 轉移到EntryQueue並等待線程B退出Monitor之後再進入Monitor。也就是先通知再退出。

Brinch Hanson Semantics

Brinch Hanson模型和Mesa模型類似區別在於僅允許線程B退出Monitor后才能發送信號給線程A。也就是先退出再通知。

Hoare Semantics

Hoare模型中 線程會分別出現在WaitQueue,EntryQueue,SignalQueue,Monitor中。

當線程B發出信號告知線程A並且退出Monitor轉移到SignalQueue,線程A進入Monitor。當線程A離開Monitor后,線程B再次回到Monitor。

https://www.andrew.cmu.edu/course/15-440-kesden/applications/ln/lecture6.html

https://cseweb.ucsd.edu/classes/sp17/cse120-a/applications/ln/lecture8.html

Java裏面monitor是如何處理?

我們通過反編譯class文件看下Synchronized工作原理。

public class SynchronizedTest {
    private int count;
    public void setCountPart(int num) {
        synchronized (this) {
            this.count += num;
        }
    }
}

編譯和反編譯命令

javac SynchronizedTest.java
javap -v SynchronizedTest

我們看到兩個關鍵指令 monitorentermonitorexit

monitorenter

Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread ……

每個對象都有一個關聯monitor。

線程執行 monitorenter 時嘗試獲取關聯對象的monitor。

獲取時如果對象的monitor被另一個線程佔有,則等待對方釋放monitor后再次嘗試獲取。

如果獲取成功則monitor計數器設置為1並將當前線程設為monitor擁有者,如果線程再次進入計數器自增,以表示進入次數。

monitorexit

The current thread should be the owner of the monitor associated with the instance referenced by objectref……

線程執行monitorexit 時,monitor計數器自減,當計數器變為0時釋放對象monitor。

原文:https://docs.oracle.com/javase/specs/jvms/se6/html/Instructions2.doc9.html

可見性和重排序

在介紹Java併發之內存模型的時候,我們提到過線程訪問共享對象時會先拷貝副本到CPU緩存,修改后返回CPU緩存,然後等待時機刷新到主存。這樣一來另外線程讀到的數據副本就不是最新,導致了數據的不一致,一般也將這種問題稱為線程可見性問題

不過在使用synchronized關鍵字的時候,情況有所不同。線程在進入synchronized後會同步該線程可見的所有變量,退出synchronized后,會將所有修改的變量直接同步到主存,可視為跳過了CPU緩存,這樣一來就避免了可見性問題。

另外Java編譯器和Java虛擬機為了達到優化性能的目的會對代碼中的指令進行重排序。但是重排序會導致多線程執行出現意想不到的錯誤。使用synchronized關鍵字可以消除對同步塊共享變量的重排序。

局限與性能

synchronized給我們提供了同步處理的便利,但是它在某些場景下也存在局限性,比如以下場景。

  • 讀多寫少場景。讀動作其實是安全,我們應該嚴格控制寫操作。替代方案使用讀寫鎖readwritelock。如果只有一個線程進行寫操作,可使用volatile關鍵字替代。
  • 允許多個線程同時進入場景。synchronized限制了每次只有一個線程可進入。替代方案使用信號量semaphore。
  • 需要保證搶佔資源公平性。synchronized並不保證線程進入的公平性。替代方案公平鎖FairLock。

關於性能問題。進入和退出同步塊操作性能開銷很小,但是過大範圍設置同步或者在頻繁的循環中使用同步可能會導致性能問題。

可重入,在monitorenter指令解讀中,可以看出synchronized是可重入,重入一般發生在同步方法嵌套調用中。不過要防止嵌套monitor死鎖問題。

比如下面代碼會直接造成死鎖。

    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    public void method1()   {
        synchronized (lock1) {
            synchronized (lock2) {
            }
        }
    }
    public void method2()   {
        synchronized (lock2) {
            synchronized (lock1) {
            }
        }
    }

現實情況中,開發一般都不會出現以上代碼。但在使用 wait() notify() 很可能會出現阻塞鎖定。下面是一個模擬鎖的實現。

  1. 線程A調用lock(),進入鎖定代碼執行。
  2. 線程B調用lock(),得到monitorObj的monitor后等待線程B喚醒。
  3. 線程A執行完鎖定代碼后,調用unlock(),在嘗試獲取monitorObj的monitor時,發現有線程佔用,也一直掛起。
  4. 這樣線程A B 就互相干瞪眼!
public class Lock{
protected MonitorObj monitorObj = new MonitorObj();
    protected boolean isLocked = false;
    public void lock() throws InterruptedException{
        synchronized(this){
            while(isLocked){
                synchronized(this.monitorObj){
                    this.monitorObj.wait();
                }
            }
            isLocked = true;
        }
    }
    public void unlock(){
        synchronized(this){
            this.isLocked = false;
            synchronized(this.monitorObj){
                this.monitorObj.notify();
            }
        }
    }
}

總結

本文記錄Java併發編程中synchronized相關的知識點。

歡迎大家留言交流,一起學習分享!!!

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

【其他文章推薦】

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

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

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

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

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

聚甘新

Nissan 無人駕駛電動車預計2020量產

日本汽車廠商Nissan 宣布,該公司歐洲的首次無人駕駛汽車測試將在倫敦進行,2017 年下半年有望在歐洲市場推出配置半自動駕駛的SUV,這一車款已經在日本市場上市。

Nissan 對於無人駕駛技術的研發非常重視,同時在應用方面表現得非常謹慎。此次在倫敦進行測試的無人駕駛車採用的技術短期內不會應用在量產車上,測試的將是一輛LEAF 電動車,配置了雷達、鐳射和鏡頭,預期在2020 年會應用到量產車上。Nissan 會邀請監管機構和政府工作人員來體驗無人駕駛車,測試的過程中車內會安排司機監控,以避免意外狀況的發生,測試不會向大眾開放。

Nissan 並不是第一家在英國進行無人駕駛公共道路測試的公司,Volvo 將在英國的公共道路上進行100 台無人駕駛汽車的測試。

汽車廠商認為無人駕駛技術能夠有效地提升乘車的安全性,減少車禍的死亡率,但這一技術的推廣仍面臨許多挑戰,公眾對於無人駕駛技術存在諸多質疑。

據倫敦經濟學院最新報告顯示,大約有55% 的英國司機表示在無人駕駛汽車中非常不自在,83% 的司機更擔心無人駕駛汽車發生故障,85% 的司機認為無人駕駛系統無法跟人互動。

(合作媒體:。圖片出處:)

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

中國將要求新能源車業者追蹤回收動力電池

電動車、插電式油電混和車等被稱為「新能源車」的車款均需配置車用動力電池,使電池與新能源車的產業發展密切相關。隨著中國新能源車消費量提高,電池汰役回收的需求很快就會浮上檯面;中國工信部因而規定,未來新能源車業者須追蹤、回收所使用的動力電池。

拓墣產業研究所研究,中國的新能源車輛產業涵蓋一般家用車到大眾交通運輸,在政府的積極補貼下,自2015年起就在全球汽車市場上佔有一席之地。2016年的新能源車全球佔比更超過了50%,是全球最大市場;中國的車商比亞迪(BYD)的全球市佔率也有14%之多。

龐大的新能源車市場,意味著龐大的車用電池用量。一般而言,純電動車的車用電池會在蓄電力降至七成左右時汰換,使用期限大約是五至七年不等;因應及將到來的車用電池汰換潮,中國工信部下發《新能源汽車生產企業及產品准入管理規定》,規定自2017年7月1日起施行回收辦法。

根據規定,擁有開發新能源汽車資格的廠商方可生產新能源汽車,且須負責新能源車生產之管理、各式認證申請、確保合格、售後服務等。售後服務須包括產品質保,包括電池等零組件的回收機制。

規定第十八條指出:「新能源汽車生產企業應當在產品全生命週期內,為每一輛新能源汽車產品建立檔案,跟蹤記錄汽車使用、維護、維修情況,實施新能源汽車動力電池溯源信息管理,跟蹤記錄動力電池回收利用情況。」意味車商須建檔追蹤動力電池的實際狀況,以在必要時提醒回收。

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

【其他文章推薦】

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

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

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

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

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

智慧汽車前景看好,車用LED廠展開投資

近來,陸續有汽車與科技大廠於加州展開無人駕駛路上測試,智慧汽車儼然成為科技市場的新藍海。這股趨勢也吸引光學鏡頭和LED廠商展開投資。

看好無人駕駛技術發展潛力,除Tesla、Ford、GM與Goole等廠商展開道路測試外,也有鏡頭廠和LED廠開始投入。自駕車上路仰賴感測器進行路況偵測,因此感測器和車燈比以往更受重視,以滿足嚴格的安全性標準。

在車用LED市場方面,日本Panasonic(松下)有意收購歐洲車燈大廠ZKW,結合自家感測器專長,攻自駕車市場;而據《聯合財經網》報導,台廠群光以子公司群電買下中國車燈廠以進攻其供應鏈,且與LED車燈廠頻繁接觸,同樣有意搶食車用LED這塊大餅。

另外,照明大廠歐司朗(OSRAM)在2017年調降LED車用成本,為的是加快市場滲透率。其採用金屬導線架以取代原先LED陶瓷散熱基板,除成本降低之外,也利於設計;且其車尾燈、方向燈等產品均已改用EMC導線架。

《聯合財經網》指出,有供應商表示,除了LED大燈仍採用陶瓷基板外,其餘車體內外的晝型燈可能改用EMC導線架。隨車用LED應用變多、EMC導線架的使用增加,中國LED晝型的燈滲透率也逼近50%。

 (首圖來源: CC2.0)

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準