過來人告訴你,去工作前最好還是學學Git

前言

只有光頭才能變強。

文本已收錄至我的GitHub精選文章,歡迎Star:https://github.com/ZhongFuCheng3y/3y

之前遇到過很多同學私信問我:「三歪,我馬上要實習了,我要在實習前學些什麼做準備啊?」

三歪在實習之前也同樣問過自己當時的部門老大。

如果再給我一次機會,我會先去花點時間去學學Git

Git我相信大家對它應該不陌生吧?但凡用過GitHub的同學應該多多少少都會了解一下Git

不知道當時大家學Git的時候是看哪個教程的,我看的是廖雪峰老師的Git系列的。

(別看到廖雪峰就以為是廣告了啊,哈哈哈哈,這篇純原創分享)

分享一下三歪的經歷

剛實習的時候,一直都忙着看各種東西。有一天,我學長說:我看你也學了一些基礎了,我們來看看公司的代碼吧,看看我們生產環境是怎麼做的。

於是我學長丟了一個Git鏈接給三歪

https://github.com/ZhongFuCheng3y/3y.git

那三歪做了什麼?三歪去IDEA下把這個Git給Clone下來:

我用Clone完了以後,我學長又補了一句:這個項目不是用master分支的哦,你切換一下分支

三歪:啥?切換分支?咋整?我忘了。

我學長看了下我,貌似不咋會切換分支,就說:“我來吧”。

於是在命令行終端一頓操作后,對三歪說:“好了”

三歪:“我對Git不是很熟悉,之前一直都是在IDEA上操作的。你們一般用命令行多還是圖形界面的多呀?”

我學長:“這沒什麼,反正工具這東西,學學就行,不是什麼大問題。也沒必要說很仔細去學它,就工具嘛”

三歪:“嗯”

時間飛逝,又過了一段時間…

三歪被分配了一個需求,於是就需要新建分支去做這個需求了。所有的標準應用線上走的是master分支,公司通過一個發布系統來控制發布版本、以及整套上下線的流程。

於是我要先在發布系統裡邊新建Git分支:

完了以後,我就在IDEA界面上選擇那個被我新建完的分支

但發現我死活找不到…於是我就問我學長:我在發布系統裡邊新建了分支,為什麼在IDEA上找不到啊?

學長:“怎麼會呢,我看看”。

找了一會,他問我:“你fetch 過了嗎?”

三歪:“啥?”

於是他拿着我的電腦,打開了終端,又以是命令行的方式敲了一頓,問我:“這是不是你新建的分支?“

三歪點了點頭,於是我學長說:”好了,你再看看“。

後來發現,新建完遠程分支,如果在IDEA上要能感知到,可以在pull界面上刷新一下,那就能找到了。

也不是說命令行一定會就比界面牛逼,其實IDEA的Git功能也做得挺好的。現在我都是混合使用,一些操作用命令行,一些操作用IDEA快捷鍵。

commitpush的時候就喜歡用快捷鍵。command+k和command +shift+k我就感覺比敲命令要快不少。

這些都是個人習慣的問題,也無對錯之分,怎麼方便怎麼來。

其實也不是所有的系統都會走發布系統的(有標準應用,非標準應用)。如果要自己寫一個啟動的腳本,一般我們會做些什麼?無非就是用Git拉最新的代碼,然後用maven打個包,然後啟動。

理解Git

如果你看過上一篇《三歪給女朋友講解什麼是Git》應該能大概了解什麼是Git了。

其實我覺得學Git主要理解工作區 -> 暫存區->倉庫 這幾個概念。

我們使用Git其實絕大部分的操作都是在本地上完成的,比如說add 和commit

只有我們push的時候,才會把本地完成好的內容推到遠程倉庫

通過上一篇文章我們知道在每個人的本地都有完整的歷史版本,所以我們可以在本地就能穿梭到不同的版本,然後將修改之後的代碼再重新提交到遠程倉庫上。

所謂的工作區實際上就是我們真正的的本地目錄

我們在本地添加文件后,需要add到暫存區,文件一旦被add到了暫存區,意味着Git能追蹤到這個文件。

當我們修改到一定程度之後,我們會執行一次提交commit,在提交的時候我們會”備註“自己這次的提交修改了什麼內容。

一次commit在Git就是一個版本,Git是版本控制的軟件,我們可以隨意穿梭到任何的版本中,修改代碼。

暫存區是這麼一個概念呢?

暫存區就像購物車,沒到付款的時候你都不確定購物車裡的東西全部都是要的。每拿一件商品就付一次款,那麻煩可大了。

從宏觀上看,Git其實有本地和遠程的概念,只是本地又分了工作區、暫存區、本地倉庫。再次強調:我們操作幾乎都是在本地完成,每個人的本地都會有所有歷史版本信息

我們一般會新建分支去支持每一次的修改。

其實分支這個概念也挺好理解的:我們需要并行開發,同時我們又不關心對方改的是什麼內容,改的是什麼文件。因此我們需要在自己的專屬環境下去修改內容,只要把最終修改完后的內容合併到一個主分支就OK了。

假設三歪做完了,經過校驗通過後,把自己的代碼merge(合併)到origin/master分支后,然後就發布上線啦。

隨後,雞蛋也做完了,自己的分支校驗完了以後,他此時也想把自己的代碼合併到origin/master。不料,他改的代碼跟三歪改的代碼有衝突了(Git不知道選擇誰的的代碼),那雞蛋只能手動merge了。

綜合來看,我們使用Git大多數的場景就是各自分支開發,然後各自在本地commit(提交),最後匯總到master分支。

所以,我們學Git大多數就學怎麼實現分支的增刪改、切換以及版本的穿梭

學習Git的小tips:

Unix/Linux 命令中,- 后一般跟短命令選項(通常是單字母,也有一些命令是例外的),-- 后一般跟長命令選項。如果只有一個單獨的--,後面不緊跟任何選項,則表示命令選項結束,後續的都作為命令的參數而不是選項。

例如:git checkout -- filename filename作為git checkout 的參數,而不是選項。

日常Git使用場景

、如果這個項目的代碼我們在本地還沒有,我們先去GitLab裡邊找對應的Git地址,然後Clone到本地:

git clone https://github.com/ZhongFuCheng3y/3y.git

、接到了新的需求,我們要新建一個分支,然後基於這個分支去開發:

git checkout -b feature/sanwaiAddLog

在開發的時候,我們肯定會有兩個操作:

  • 在原來的基礎上添加新的文件
  • 在原有的文件上修改

、不管怎麼樣,等我們做到一定程度了,我們都會提交代碼。如果我們添加了新的文件,我們需要先add,然後再commit

git add .
git commit  -m "try to commit files to GitHub, i am java3y"

、假設我們一切順利,在沒人打擾的情況下已經寫好了代碼了,然後我們會把自己的分支push到遠程倉庫

git push

、假設我們寫到一半,其他小夥伴已經把他的代碼merge到主分支了,我們也需要把他最新的 代碼給pull拉取下來(可以 git fetch + git merge 替代)。

git pull

如果沒有衝突,那git就會把他的代碼給merge到我當前的分支上。如果有衝突,Git會提醒我去手動解決一下衝突。

、假設我們寫到一半了,現在工作區的代碼都已經commit了。此時同事說要不幫忙一起排查一個問題,同事一般用的是自己分支,於是就得問他:你用的哪個分支啊?於是得把他的分支給拉下來,看看他的代碼哪兒有問題

git fecth -- 手動拉取遠程倉庫更新的信息
git checkout  分支名   -- 切換到他的分支

現在切換到他的分支,相當於你的環境跟他的環境是一模一樣的,於是就可以愉快地一起看Bug了。

、假設我們寫到一半了,現在工作區的代碼還沒commit。現在有同事說要排查問題或者一個新的Bug被發現了,要緊急切換到其他的分支。現在我又不想commit(我就寫了一半,編譯還報着錯誤,沒理由讓我commit吧)。

這時,我會把工作區的代碼先stash到暫存區給保存起來,然後就可以愉快地切換其他的分支了。

git stash

等我解決完另一個bug或者幫別人看完問題了,我再把剛剛保存在暫存區的代碼給撈出來,繼續幹活

git stash pop

、我一直在修Bug,現在的分支已經被我搞得人摸鬼樣了,我非常難受,甚至不知道自己在這個過程中改了多少東西了。

思路已經完全被打亂了,我想回到一個穩定的commit重新出發,重來吧(通過下面的命令,把工作區的代碼都改成對應commit的代碼了)。

git reset --hard  版本號

那我怎麼找到版本號呢?Git也是有日誌的:

git log --pretty=oneline

常用的Git命令

查看Git工作區、暫存區的變更情況(可以知道哪些沒有commit、哪些沒有被Git追蹤):git status

拉取遠程最新的變更到本地:git fetch

切換分支:git checkout 分支名

將代碼還原到某個版本(包括工作目錄):git reset --hard 版本號

查看Git的提交(commit)記錄:git log

將代碼還原到某個版本后,後悔了,想重新回去,但在提交記錄已經找不到了。git reset --hard reset 之後的 commit都給抹殺掉了。找到最近的執行Git命令:git reflog

還原到某個版本了,現在我為了穩健,不想再原來的分支上修改了,再新建一個分支吧(-b 參數把當前分支切換到了要創建的分支上):git checkout -b 分支名

我們把上一次還是”相對穩健“的分支合併到我新建的分支上:git merge 分支

突然想看看現在有多少個分支:git branch -a

新增幾個文件了,隨手git add一下吧

改得差不多了,隨手git commit -m 一下吧,最好還是寫好備註,不然以後等改多了,你都不知道你改了什麼啦。

改完了,提交到遠程吧:git push

想把遠程分支最新的代碼給拉下來,然後合併到本地上。我們可以用git fetchgit merge 來實現,也可以通過git pull來實現。一般我用的都是git fetch+git merge ,這樣會更加可控一些

有的時候,本地分支在master分支,然後忘了切其他的分支去修改,直接在master改了,然後也push到遠程了。等你發現的時候,你會真的想罵自己。

咋辦?最簡單的辦法其實我們還是可以git reset --hard到對應的版本,然後將其修改或者復原,再強制提交到master分支:git push -u origin/master -f

三歪瞎扯

在這篇文章中,我列出的Git常用的命令其實並不多吧。

像很多博客講的difftagconfig之類的命令我都沒有講,我這邊現實開發時這些命令也沒怎麼用過…

如果覺得我說漏的,可以在評論區補充,一起學習。

其實現在IDEA也很強大,很多時候都可以配合IDEA給我們提供的Git去做很多事。有的場景敲命令會比較方便,有的時候就直接圖形化界面就比較方便。

diff 這個功能而言, 肯定還是圖形界面好用一些吧(至少我是這樣認為的

IDEA配合一些快捷鍵,使用Git也能爽得飛起。Git始終也只是一個工具,如果你有興趣可以了解它的實現(我覺得大部分人可能不知道它是怎麼實現的);

如果沒興趣看它的實現,了解它是怎麼使用的,也足夠應付日常的開發場景了。

總的來說,現在的互聯網公司大多數還是用Git的,Git本身使用上其實不難,只要理解了Git是幹嘛的,它有個本地倉庫的概念,它可以來回穿梭各種版本,然後將本地的信息提交到遠程,跟着教程把常用的命令敲敲也差不多了。

如果實在是不懂,也別慌(我都給你們打了個樣了);主動認慫,虛心求教,同事們都不會嫌棄你的。

如果實習之前不知道要準備什麼去公司,要是對Git不了解,我覺得Git可以有佔一席之位。

更多Git命令和參考資料:

  • https://github.com/xjh22222228/git-manual
  • https://juejin.im/post/5edcf3a36fb9a047fa04fbc3
  • https://www.liaoxuefeng.com/wiki/896043488029600

各類知識點總結

下面的文章都有對應的原創精美PDF,在持續更新中,可以來找我催更~

  • 92頁的Mybatis
  • 129頁的多線程
  • 141頁的Servlet
  • 158頁的JSP
  • 76頁的集合
  • 64頁的JDBC
  • 105頁的數據結構和算法
  • 142頁的Spring
  • 58頁的過濾器和監聽器
  • 30頁的HTTP
  • 42頁的SpringMVC
  • Hibernate
  • AJAX
  • Redis
  • ……

涵蓋Java後端所有知識點的開源項目(已有8K+ star):

  • GitHub
  • Gitee訪問更快

我是三歪,一個想要變強的男人,感謝大家的點贊收藏和轉發,下期見。給三歪點個贊,對三歪真的非常重要!

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

聚甘新

vs.net/vscode中使用Beetlex創建vue應用

      平時在開發Vue應用則需要安裝nodejs,vue cli等相關東西相對來說麻煩一些;如果你喜歡像vs.net/vscode創建普通項目一樣就能開發Vue項目的話那可以嘗試一下BeetleX針對Vue編寫的服務插件;只需要創建一個簡單的Console項目引用相關插件即可以構建一個單頁面的Vue項目。雖然在開發的時候需要用到Beetlex,但後期發布完全可以用在其他平台上,因為組件會針對Vue的內容最終生成一個可發布的js文件。接下來介紹一下這個插件的使用(vs.net/vscode均可)

創建項目

首先需要創建一個c#的Console項目

創建項目后需要引用兩個BeetleX的組件包,可通過Nuget安裝最新版本;分別是:BeetleX.FastHttpApi.HostingBeetleX.FastHttpApi.VueExtend;這兩個組件的作用分別是在項目中啟動HTTP服務和針對.VUE文件生成相應的javascript文件。

項目文件布局

由於是Console項目,所以需要針對相關文件存放規則,具體大概如下:

所有html,css,js和vue等文件必須存放在項目的views目錄下;對於這個目錄下用什麼子目錄存放相關文件就看自己的需求了。

服務和基礎資源配置

為了讓控制台服務作為一個HTTP服務需要做一些簡單的配置

static void Main(string[] args)
{
    var builder = new HostBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            services.UseBeetlexHttp(o =>
            {
                o.Port = 80;
                o.SetDebug();
                o.LogToConsole = true;
                o.LogLevel = BeetleX.EventArgs.LogType.Info;
            },
            s =>
            {
                s.AddExts("woff;ttf;woff2");
                s.Vue().CssRewrite("/css/{1}.css").JsRewrite("/js/{1}.js");
                s.Vue().Debug();
                var resource = s.Vue().CreateWebResorce(typeof(Program).Assembly);
                resource.AddScript("vue.js", "axios.js", "beetlex4axios.js", "jquery.js", "echarts.js", "bootstrap.js", "page.js");
                resource.AddCss("bootstrap.css", "bootstrapadmin.css", "admin.css");
            },
            typeof(Program).Assembly);
        });
    builder.Build().Run();
}

前部分主要描述在那個端口開發HTTP服務,並設置相關日誌显示級別;後半部分主要是描述vue配置一些信息。這個後面會詳細描述,接下來看啟動一下服務看下日誌。

服務日誌會显示資源加載和服務端口的情況。

VUE擴展配置

前面服務啟動的時候就已經配置相關VUE的內容,這裏再詳細解說一下。

s.Vue().Debug();
s.Vue().CssRewrite("/css/{1}.css").JsRewrite("/js/{1}.js");
var resource = s.Vue().CreateWebResorce(typeof(Program).Assembly);
resource.AddScript("vue.js", "axios.js", "beetlex4axios.js", "jquery.js", "echarts.js", "bootstrap.js","page.js");
resource.AddCss("bootstrap.css", "bootstrapadmin.css", "admin.css");

Beetlex的Vue插件會管理項目的兩種資源,分別是css和javascript. 

  • Debug方法

        主要是告訴組件每次調用資源都重新從文件中生成,這樣開發都在變更相關文件的時候無須重新編譯,保存文件后刷頁面即可。此方法在Release編譯模式下並不生效。

  • CssRewrite和JsRewrite方法

         這兩個方法主要是描述通過那些路徑訪問到css和javascript資源,以上定義/css/路徑任意一文件都會得到項目中所有的css內容;/js/路徑任意文件都得到項目的javascript內容。

  • WebResource

        這個類用於描述如何收集對應的css和javascript文件;對於javascript文件來說會先打包這些基礎的文件然後再追加項目中的vue文件。打包的順序是依據定義的順序來進行。

啟動頁

在項目vue文件只是模塊文件,我們需要在根目錄下定義一個HTML文件作為訪問落地頁面,接下來看一下這頁面的定義

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <link href="/css/v1.css?group=BeetleX.AdminUI" rel="stylesheet" />
    <script src="/js/v1.js?group=BeetleX.AdminUI"></script>
    <title>BeetleX AdminUI</title>
</head>
<body>
    <div id="page">
        <main_menu @menu_resize="OnMenuResize($event)" @openwindow="OnOpenWindow($event)"></main_menu>
        <windows_bar :windows="windows" :full="full" :selectwindow="selectWindow.id" @close="OnCloseWindows($event)"></windows_bar>
        <div class="main-content" :style="{left:(full=='max'?'60px':'260px')}">
            <keep-alive>
                <component :is="selectModel" @openwindow="OnOpenWindow($event)" :token="selectWindow.data" :winsize="sizeVersion"></component>
            </keep-alive>
        </div>
        <page_footer></page_footer>
    </div>
    <script>
        var page = new Vue(pageInfo);
        page.OnOpenWindow({ id: 'home', title: '主頁', model: 'models_home' })
    </script>
</body>
</html>

頁面只是負責資源加載和VUE模塊組裝,在這裏定義的css和javascript加載組是Beetlex.AdminUI即對應剛才加載的程序集資源包:

var resource = s.Vue().CreateWebResorce(typeof(Program).Assembly);

這個落地頁的展示效果如下:

模塊定義

項目配置完成后就可以在views目錄定義自己的vue模塊,存放層次目錄沒有具體的要求可根據自己的喜好來定義存放目錄.對於vue模塊的定義和傳統的vue定義會有些差別的,模塊文件名作為對應的模塊名稱。文件內部主要有HTML和JavaScript組成,而不是像傳統那樣一個vue文件和一個js文件。下面是一個models_home.vue模塊的描述:

    <div style="width:99%;">
        <div class="row">
            <div class="col-lg-6">
                <models_panel :title="'銷售走勢'" :child="'models_monthline'" :winsize="winsize"></models_panel>
                <models_panel :title="'僱員銷售比例'" :child="`model_employeesspie`" :winsize="winsize"></models_panel>
            </div>
            <div class="col-lg-6">
                <models_panel :title="'最新訂單'" :child="`models_neworders`"></models_panel>
                <models_panel :title="'客戶訂單比例'" :child="`model_customerspie`" :winsize="winsize"></models_panel>
            </div>
        </div>
    </div>
<script>
 {
        props: ["winsize"],
        data: function () {
            return {
               
            }
        },
    }
</script>

組件支持的VUE模塊描述要相對簡單一些,沒有一些import的東西;只有HTML和一個VUE構造信息的結構體。

接下來是一個簡單的列表模塊models_employees.vue:

    <div>
        <table class="table">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Title</th>
                    <th>Region</th>
                    <th>City</th>
                    <th>Country</th>
                    <th>Address</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="item in GetEmployees.result">
                    <td><a href="javascript:void(0)" @click="OnOpen(item)">{{item.FirstName}} {{item.LastName}}</a> </td>
                    <td>{{item.Title}}</td>
                    <td>{{item.Region}}</td>
                    <td>{{item.City}}</td>
                    <td>{{item.Country}}</td>
                    <td>{{item.Address}}</td>
                </tr>
            </tbody>
        </table>
    </div>
<script>
    {
        data: function () {
            return {
                GetEmployees: new beetlexAction("/Employees", null, []),
            }
        },
        methods: {
            OnOpen: function (item) {
                this.$open('emp' + item.EmployeeID, '僱員:' + item.FirstName + ' ' + item.LastName, 'models_employeedetail', { id: item.EmployeeID });
            }
        },
        mounted: function () {
            this.GetEmployees.get();
        }
    }
</script>

發布

Beetlex原本是一個HTTP服務模塊,正常情況你把相關文件嵌入到項目發布即可在Linux或windows下運行(環境.net core 2.1或更高版本)。如果你不相基於Beetlex運行,那你可以在編譯目錄下獲取相關的javascript和css完全打包好的文件放到其他環境中部署。

代碼

如果你感興趣可以訪問 https://github.com/IKende/AdminUI   演示地址: http://adminui.beetlex.io/

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

【其他文章推薦】

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

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

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

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

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

聚甘新

「大家都累了」 美中仍保守 曼谷氣候談判陷泥沼

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

聚甘新

建立台灣建築節能經驗:政策循序漸進  技術在地試驗

環境資訊中心特約記者 陳文姿報導

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

【其他文章推薦】

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

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

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

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

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

聚甘新

2017第二屆中國中原國際充電站(樁)技術展

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

聚甘新

香港促推電動車用電池回收機制,避免生態汙染

一般的電動車用電池約有7~8 年的使用壽命,但因含有鋰、硫酸等重金屬,若無妥善回收處理,將有環境汙染之虞。香港政府所引入的首批電動車之電池屆臨退役,專家呼籲政府盡速制訂回收辦法,以避免更嚴重的環境破壞。

香港《蘋果日報》報導,香港在豁免電動車首次登記稅後,電動車數量急速從2010年的100輛以下成長到今年8月的6,167輛,其中以Tesla和Volkswagen最受歡迎。以一般電動車用電池約7年的平均壽命來看,香港首批電動車的舊電池已屆臨汰役,但香港目前仍無回收制度;若廢電池隨意丟棄,所造成的重金屬汙染恐怕較傳統柴油車和汽油車更嚴重,完全背離環保的美意。

香港汽車會副會長李耀培表示,車用電池的儲電量若降低到70%,就可能影響電動車行動速度,甚至造成熄火,進而影響行車安全,因此在此階段就會汰役換新。不過,這也意味著汰役的車用電池其實仍有70%的蓄電量,經妥善處理後可進行二次運用。

報導指出,香港過去會將這類廢電池交給有牌照的化學廢物處置設施進行初步處理,之後再出口到海外回收。但李耀培表示,這只是把汙染丟出香港,並沒有解決問題。他呼籲政府應制定長遠的回收機制,或可參考英國、歐洲等國家將汰役的電動車用電池改製為風力、太陽能儲能設備,更能有效利用電池,且也有助環保。

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

聚甘新

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網頁設計為架站首選

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

※回頭車貨運收費標準

聚甘新