認識各種內存地址_網頁設計公司

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

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

什麼是物理地址?

物理地址就是內存單元的絕對地址,物理地址0x0000就表示內存條的第一個存儲單元,0x0010(16進制)就表示內存條的第17個存儲單元,一個存儲單元是1byte(8bit)。

你問為什麼是1byte?

一個內存條是由若干個黑色的內存顆粒構成的。每一個內存顆粒叫做一個chip。

每個chip中又疊了若干bank

在每個bank內部,就是電容的行列矩陣結構了,每一個元素有8個小電容,存儲8個bit,也就是一個字節。

什麼是線性地址和虛擬地址?

在80286系列以前,CPU只支持實模式操作模式。16位寄存器想要對20位地址線進行尋址,使用分段機制,段基址(16位) x 16(左移4位) + 段內偏移地址(4位)就是物理地址了。
這樣的好處是所見即所得,程序員指定的地址就是物理地址,物理地址對程序員是可見的。同時也帶來了一些問題:
1) 無法支持多任務。
2) 程序的安全性無法得到保證。
(根本原因就是一個程序直接修改了其他程序的內存,導致崩潰)

80286系列則是被設計來解決這些問題,段式訪存得到的改進,原來段基址+段內偏移得到的地址不再是實際的物理地址,而是被稱作為線性地址,要經過一個轉換層轉換才變成一個物理地址。這種CPU操作模式就被稱為保護模式了。保護的是:分清楚各個程序使用的存儲區域,不允許隨便跨界訪問。

80286系列之後就進入32位CPU時代了,32位寄存器可以直接訪問32位地址總線。但是在保護模式下,地址仍然採用“段地址:偏移地址”的方式來表示。
段值仍然由原來的16位的cs、ds等寄存器表示,但是此時它們僅僅是一個索引,這些個索引指向一個數據結構的表項,表項中詳細定義了一個段的起始地址、界限、屬性等內容,這個數據結構,叫做GDT(其實還可能是LDT),GDT中的每一個表項,叫做描述符。
這裏我們就詳細看一下保護模式的尋址方式吧。

1) 尋址時,先找到gdtr寄存器,從中得到GDT的基址。
2) 有了GDT的基址,又有段寄存器中保存的索引,可以得到段寄存器“所指”的那個表項,即所指的那個描述符。
3) 得到了描述符,就可以從描述符中得到該描述符所描述的那個段的起始地址。
4) 有了段的起始地址,將偏移地址拿過來與之相加,便能得到最後的線性地址。
5) 有了線性地址,經過變換,即可得到相應的物理地址。

保護模式雖然解決了內存不被跨界訪問,但是其也帶來了新的問題,那就是內存碎片。
首先我們了解一下內存碎片為何產生:
首先假設我們有10B內存:

ID 首地址 尾地址 長度 狀態
0 0 9 10 空閑

當程序申請一個長度為3的內存空間后:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 9 7 空閑

當程序再申請一個長度為2,以及長度為4的內存空間后:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 4 2
2 5 8 4
3 9 9 1 空閑

此時,只剩1個可用空間。如果這時程序再來申請長度大於1的空間,就申請不了,也就是內存不夠。
現在,釋放掉ID=1的空間:

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

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

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 4 2 空閑
2 5 8 4
3 9 9 1 空閑

我們發現,現在可用內存空間為3,但是,這3個空閑空間,並不是連續的。
所以,如果程序現在申請長度為3的內存空間,同樣會申請不了,會出現內存不夠。我們把這種情況,稱之為內存碎片

那內存碎片怎麼解決呢?於是就有了分頁機制,接下來我們詳細講一下分頁:
首先,把物理內存,按照某種尺寸,進行平均分割。比如我現在以2個內存單位,來分割內存,也就是每兩個連續的內存空間,組成一個內存頁:

地址 頁ID 狀態
0 0 空閑
1
2 1 空閑
3
4 2 空閑
5
6 3 空閑
7
8 4 空閑
9

接着,系統同樣需要維護一個內存信息表:

ID 使用的內存頁ID

現在,程序申請長度為3的內存空間,不過由於現在申請的最小單位為頁面,而一個頁面的長度為2,因此現在需要申請2個頁面,也就是4個內存空間。你看,這就浪費了1個內存空間。

地址 頁ID 狀態
0 0
1
2 1
3
4 2 空閑
5
6 3 空閑
7
8 4 空閑
9
ID 使用的內存頁ID
0 0,1

接着,程序再申請長度為1,長度為2的空間:

地址 頁ID 狀態
0 0
1
2 1
3
4 2
5
6 3
7
8 4 空閑
9
ID 使用的內存頁ID
0 0,1
1 2
2 3

釋放掉ID=1,內存頁ID為2的那條內存空間信息:

地址 頁ID 狀態
0 0
1
2 1
3
4 2 空閑
5
6 3
7
8 4 空閑
9
ID 使用的內存頁ID
0 0,1
2 3

現在,就出現了之前的情況:目前一共有4個內存空間,但是不連續。
不過,因為現在是分頁管理機制,因此,現在仍然可以繼續申請長度為4的內存空間。
沒有碎片,能夠盡量地全部用完空間。但仔細想想,這種優勢背後,也是需要付出大量代價的。
分頁的方式下,程序需要記錄內存頁ID,每次使用時,需要從內存頁ID翻譯成實際內存地址,多了一次轉換。
而且這種模式,會浪費一些內存,比如上面申請3個內存空間,實際分配了2個頁面共4個內存空間,浪費了1個內存空間。

還有一個要注意的地方,這個時候”段基址+段內偏移地址”經過段部件處理后得到的線性地址就不再是物理地址了,而是虛擬地址了。
從下圖我們能夠清楚的看出來我們最後的線性地址表示的是頁表的地址,而不是物理地址了。

什麼是邏輯地址和有效地址?

無論CPU在什麼模式下,段內偏移地址又稱為有效地址或者邏輯地址(只是叫法不一樣罷了),例如實模式下mov ax, [0x7c00]0x7c00就是邏輯地址(或有效地址),但這條指令最終操作的物理地址是DS*16+0x7c00
Linux最初就是在32位的80386系列上設計的,並且沒有使用分段機制,所以在Linux上邏輯地址和線性地址就是一回事了。

總結

一圖以概括:

我的博客:https://geanqin.github.io/

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

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

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

分享一個新出爐的JVM里不痛不癢的BUG(Attach機制相關)_網頁設計

網頁設計最專業,超強功能平台可客製化

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網

概述

老早之前寫過一篇文章,關於attach機制的,可以看下這篇老文章了解一下JVM源碼分析之Attach機制實現完全解讀,比如大家常用的jstack,jmap等工具的主要原理都和attach機制有關,在JVM里處理這些命令的線程主要是Attach Listener這個線程,這個線程在JVM里是唯一的,我之前也一直以為是唯一的,但是我們同事最近在做一個線程分析產品的時候,發現我們抓到了多個Attach Listener線程,這讓我也很疑惑,我第一感覺是不可能,肯定是數據抓錯了,直到親眼看到了兩個同名的Attach Listener線程我才不得不相信原來還真有這種情況。

問題分析

不過從Attach Listener的實現來看,它設計的初衷不應該是一個多線程的設計,於是我昨晚上又翻了一遍代碼,發現還真可能存在這種情況。舉個栗子,當我們很多人同時執行jstack的時候,就可能會發生,當然有個前提是之前都沒有做過任何和attach相關的操作。

Attach Listener線程默認情況下不會在JVM啟動的時候就創建,當然也有一個JVM參數可以指定在JVM啟動的時候就啟動這個線程,這個就不會存在我們今天討論的這個問題了,這個JVM參數是-XX:+StartAttachListener

當我們在運行時觸發attach機制的時候,首先會通過Signal Dispatcher線程來創建Attach Listener線程,代碼如下:

在上面的圈起來的init方法里會創建Attach Listener線程,但是在init方法執行之前會通過_initialized屬性來判斷是否需要創建線程,而_initialized設置為true是在attach_listener_thread_entry里,這個是Attach Listener Thread的entry,也就是當這個線程執行的時候執行的方法。

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

但是在設置_initialized=true之前,如果有多個請求信號發出了(比如同時又很多jstack命令觸發),可能會創建多個Attach Listener,因為Signal DispatcherAttach Listener線程是異步執行的。

問題復現

為了讓效果更明顯,我們可以在hotspot里修改下代碼重新編譯下再跑demo

在上面函數里加上圈起來的這段代碼,表示在設置_initialized屬性之前停留15s,當進程起來之後,不斷執行jstack <pid>,最終將會看到有非常多的Attach Listener線程

其實問題的根本就是有一個空檔期(設置_initialized為true之前)可能存在多次創建線程的可能。

總結

總的來說,創建Attach Listener線程是通過Signal Dispatcher線程來創建的,但是決定Signal Dispatcher是否可以重複創建Attach Listener線程的標記是在某個Attach Listener線程里設置的,如果沒有及時設置該標記,就可能存在創建多個Attach Listener線程的情況。

 

一起來學習吧:

PerfMa KO 系列課之 JVM 參數【Memory篇】

實戰:一次疑似內存泄漏的問題排查

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

※推薦評價好的iphone維修中心

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

Spring Data JPA入門及深入_網頁設計公司

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

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

一:Spring Data JPA簡介

  Spring Data JPA 是 Spring 基於 ORM 框架、JPA 規範的基礎上封裝的一套JPA應用框架,可使開發者用極簡的代碼即可實現對數據庫的訪問和操作。它提供了包括增刪改查等在內的常用功能,且易於擴展!學習並使用 Spring Data JPA 可以極大提高開發效率!

  Spring Data JPA 讓我們解脫了DAO層的操作,基本上所有CRUD都可以依賴於它來實現,在實際的工作工程中,推薦使用Spring Data JPA + ORM(如:hibernate)完成操作,這樣在切換不同的ORM框架時提供了極大的方便,同時也使數據庫層操作更加簡單,方便解耦

1:Spring Data JPA與JPA和hibernate三者關係

  我在接下面的文章中都對它們三者進行擴展及應用,以及三者的封裝關係及調用關係,我下面也會以一張圖說明,如果此時有對JPA還一竅不通的可以參考我之前的一篇關於JPA文章的介紹

  關係:其實看三者框架中,JPA只是一種規範,內部都是由接口和抽象類構建的;hibernate它是我們最初使用的一套由ORM思想構建的成熟框架,但是這個框架內部又實現了一套JPA的規範(實現了JPA規範定義的接口),所有也可以稱hibernate為JPA的一種實現方式我們使用JPA的API編程,意味着站在更高的角度上看待問題(面向接口編程);Spring Data JPA它是Spring家族提供的,對JPA規範有一套更高級的封裝,是在JPA規範下專門用來進行數據持久化的解決方案。

   其實規範是死的,但是實現廠商是有很多的,這裏我對hibernate的實現商介紹,如其它的實現廠商大家可以自行去理解,因為規範在這,實現類可以更好別的,面向接口編程。

二:SpringDataJPA快速入門(完成簡單CRUD)

1:環境搭建及簡單查詢

-- 刪除庫
-- drop database demo_jpa;
-- 創建庫
create database if not exists demo_jpa charset gbk collate gbk_chinese_ci;
-- 使用庫
use demo_jpa;
-- 創建表
create table if not exists student(
    sid int primary key auto_increment,     -- 主鍵id
    sname varchar(10) not null,             -- 姓名
    sage tinyint unsigned default 22,        -- 年齡
    smoney decimal(6,1),                    -- 零花錢
    saddress varchar(20)                    -- 住址
)charset gbk collate gbk_chinese_ci;
insert into student values
(1,"螞蟻小哥",23,8888.8,"安徽大別山"),
(2,"王二麻",22,7777.8,"安徽大別山"),
(3,"李小二",23,6666.8,"安徽大別山"),
(4,"霍元甲",23,5555.8,null),
(5,"恭弘=叶 恭弘問",22,4444.8,"安徽大別山"),
(6,"李連傑",23,3333.8,"安徽大別山"),
(7,"馬克思",20,2222.8,"安徽大別山");

MySQL簡單建表語句   重要【後面都參照這個建表語句進行】

<dependencies>
        <!--單元測試坐標  4.12為最穩定-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <!--Spring核心坐標  注:導入此坐標也同時依賴導入了一些其它jar包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring對事務管理坐標-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring整合ORM框架的必須坐標 如工廠/事務等交由Spring管理-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring單元測試坐標-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <!--Spring Data JPA 核心坐標-->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.10.4.RELEASE</version>
        </dependency>
        <!--導入AOP切入點表達式解析包-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>

        <!--Hibernate核心坐標-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.4.10.Final</version>
        </dependency>
        <!--hibernate對持久層的操作坐標-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.4.10.Final</version>
        </dependency>

        <!--這下面的2個el坐標是使用Spring data jpa 必須導入的,不導入則報el異常-->
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>javax.el</artifactId>
            <version>2.2.4</version>
        </dependency>

        <!--C3P0連接池坐標-->
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>

        <!--MySQL驅動坐標-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.32</version>
        </dependency>

        <!--JAXB API是java EE 的API,因此在java SE 9.0 中不再包含這個 Jar 包。
            java 9 中引入了模塊的概念,默認情況下,Java SE中將不再包含java EE 的Jar包
            而在 java 6/7 / 8 時關於這個API 都是捆綁在一起的
            拋出:java.lang.ClassNotFoundException: javax.xml.bind.JAXBException異常加下面4個坐標
            -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>

pom.xml坐標導入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/data/jpa
        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <!--配置註解掃描-->
    <context:component-scan base-package="cn.xw"></context:component-scan>

    <!--配置C3P0連接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo_jpa"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123"></property>
    </bean>

    <!--創建EntityManagerFactory交給Spring管理,讓Spring生成EntityManager實體管理器操作JDBC-->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <!--配置一個連接池,後期獲取連接的Connection連接對象-->
        <property name="dataSource" ref="dataSource"></property>
        <!--配置要掃描的包,因為ORM操作是基於實體類的-->
        <property name="packagesToScan" value="cn.xw.domain"></property>
        <!--配置JPA的實現廠家 實現了JPA的一系列規範-->
        <property name="persistenceProvider">
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"></bean>
        </property>
        <!--JPA供應商適配器 因為我上面使用的是hibernate,所有適配器也選擇hibernate-->
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!--指定當前操作的數據庫類型 必須大寫,底層是一個枚舉類-->
                <property name="database" value="MYSQL"></property>
                <!--是否自動創建數據庫表-->
                <property name="generateDdl" value="false"></property>
                <!--是否在運行的時候 在控制台打印操作的SQL語句-->
                <property name="showSql" value="true"></property>
                <!--指定數據庫方言:支持的語法,如Oracle和Mysql語法略有不同 org.hibernate.dialect下面的類就是支持的語法-->
                <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect"></property>
                <!--設置是否準備事務休眠會話的底層JDBC連接,即是否將特定於事務的隔離級別和/或事務的只讀標誌應用於底層JDBC連接。-->
                <property name="prepareConnection" value="false"></property>
            </bean>
        </property>
        <!--JPA方言:高級特性 我下面配置了hibernate對JPA的高級特性 如一級/二級緩存等功能-->
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"></bean>
        </property>
        <!--注入JPA的配置信息
            加載JPA的基本配置信息和JPA的實現方式(hibernate)的配置信息
            hibernate.hbm2ddl.auto:自動創建數據庫表
                create:每次讀取配置文件都會創建數據庫表
                update:有則不做操作,沒有則創建數據庫表 -->
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>

    <!--配置事務管理器  不同的事務管理器有不同的類 如我們當初使用這個事務管理器DataSourceTransactionManager-->
    <bean id="jpaTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <!--把配置好的EntityManagerFactory對象交由Spring內部的事務管理器-->
        <property name="entityManagerFactory" ref="entityManagerFactory"></property>
        <!--因為entityManagerFactory內部設置數據庫連接了  所有後面不用設置-->
        <!--<property name="dataSource" ref="dataSource"></property>-->
    </bean>

    <!--配置tx事務-->
    <tx:advice id="txAdvice" transaction-manager="jpaTransactionManager">
        <tx:attributes>
            <tx:method name="save*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="insert*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="update*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="delete*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
            <!--如果命名規範直接使用下面2行控制事務-->
            <!--<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>-->
            <!--<tx:method name="*" propagation="REQUIRED" read-only="false"/>-->
        </tx:attributes>
    </tx:advice>

    <!--配置AOP切面-->
    <aop:config>
        <!--在日常業務中配置事務處理的都是Service層,因為這裡是案例講解,所有我直接在測試類進行
            所有我把事務配置這,也方便後期拷貝配置到真項目中-->
        <aop:pointcut id="pt1" expression="execution(* cn.xw.service.impl.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>

    <!--整合SpringDataJPA-->
    <!--base-package:指定持久層接口
        entity-manager-factory-ref:引用其實體管理器工廠
        transaction-manager-ref:引用事務    -->
    <jpa:repositories base-package="cn.xw.dao" entity-manager-factory-ref="entityManagerFactory"
                      transaction-manager-ref="jpaTransactionManager"></jpa:repositories>
</beans>

applicationContext.xml 配置文件(重要)

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sid")
    private Integer id;
    @Column(name = "sname")
    private String name;
    @Column(name = "sage")
    private Integer age;
    @Column(name = "smoney")
    private Double money;
    @Column(name = "saddress")
    private String address;
    //下面get/set/構造/toString都省略
    //注意:我上面的都使用包裝類,切記要使用包裝類,
    // 原因可能數據庫某個字段查詢出來的值為空 null    
}

Student實體類及映射關係

//@Repository("studentDao") 這裏不用加入IOC容器 Spring默認幫我們注入
public interface StudentDao extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> {
    /*
    JpaRepository<T,ID>:T:當前表的類型  ID:當前表主鍵字段類型 
        功能:用來完成基本的CRUD操作 ,因為內部定義了很多增刪改查操作
    JpaSpecificationExecutor<T>:T:當前表的類型
        功能:用來完成複雜的查詢等一些操作
    */
}

StudentDao接口

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;

    @Test
    public void test() {
        //查詢id為4學生
        Student student = sd.findOne(4);
        System.out.println("開始打印:" + student);
        //開始打印:Student{id=4, name='霍元甲', age=23, money=5555.8, address='null'}
    }
}

測試類

2:SpringDataJPA簡單單表接口方法查詢圖 

3:針對上面圖的一些方法示例

  在針對上面方法進行操作的時候,我們的Dao接口必須繼承JpaRepository<T,ID>和JpaSpecificationExecutor<T>(後面複雜查詢使用),大家也可以去研究一下CrudRepository類,這個類在這裏就不說了,JpaRespository類間接實現了它

①:Repository:最頂層的接口,是一個空的接口,目的是為了統一所有Repository的類型,且能讓組件掃描的時候自動識別。
②:CrudRepository :是Repository的子接口,提供CRUD的功能
③:PagingAndSortingRepository:是CrudRepository的子接口,添加分頁和排序的功能
④:JpaRepository:是PagingAndSortingRepository的子接口,增加了一些實用的功能,比如:批量操作等。
⑤:JpaSpecificationExecutor:用來做負責查詢的接口
⑥:Specification:是Spring Data JPA提供的一個查詢規範,要做複雜的查詢,只需圍繞這個規範來設置查詢條件
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;

    /****************************查詢操作*****************************/
    @Test   //查詢並排序
    public void findtestA() {
        //查詢全部 先對age排序后在進行id排序
        Sort sort = new Sort(Sort.Direction.DESC, "age", "id");
        List<Student> students = sd.findAll(sort);
        //打印省略
    }

    @Test   //查詢並分頁
    public void findtestB() {
        //查詢分頁  page是第幾頁 size 每頁個數  當前是第2頁查詢3個   相當limit 6,3
        Pageable pageable = new PageRequest(2, 3);
        Page<Student> page = sd.findAll(pageable);
        System.out.println("當前頁:"+page.getNumber());
        System.out.println("每頁显示條目數:"+page.getSize());
        System.out.println("總頁數:"+page.getTotalPages());
        System.out.println("結果集總數量:"+page.getTotalElements());
        System.out.println("是否是首頁:"+page.isFirst());
        System.out.println("是否是尾頁:"+page.isLast());
        System.out.println("是否有上一頁:"+page.hasPrevious());
        System.out.println("是否有下一頁:"+page.hasNext());
        System.out.println("結果集:"+page.getContent());
        /*
            當前頁:2
            每頁显示條目數:3
            總頁數:3
            結果集總數量:7
            是否是首頁:false
            是否是尾頁:true
            是否有上一頁:true
            是否有下一頁:false
            結果集:[Student{id=7, name='馬克思', age=20, money=2222.8, address='安徽大別山'}]
         */
        //總結:以後做分頁操作變容易了呀
    }

    @Test   //查詢指定學生
    public void findtesC() {
        //創建一個集合   List就是一個Iterable可迭代容器對象,因為實現了Iterable接口
        List<Integer> list = new ArrayList<Integer>() {{
            add(1);add(5);add(3);
        }};
        List<Student> students = sd.findAll(list);
        //打印省略
    }

    /****************************更新操作*****************************/
    @Test   //更新多個學生 更新學生全部地址為 安徽合肥
    @Transactional
    @Rollback(value = false)
    public void savetestA() {
        //創建2個集合  第一個集合查詢全部數據,然後把集合里對象地址改為新的放入新集合上,後面迭代更新
        List<Student> list = sd.findAll();
        System.out.println(list.size());
        List<Student> newlist = sd.findAll();
        for (int i = 0; i < list.size(); i++) {
            Student student = list.get(i);
            student.setAddress("安徽合肥");
            System.out.println(student);
            newlist.add(student);
        }
        List<Student> save = sd.save(newlist);
        //打印略
    }

    /****************************刪除操作*****************************/
    @Test   //刪除指定學生
    public void deletetest() {
        List<Integer> list = new ArrayList<Integer>() {{
            add(1);add(5);add(3);
        }};
        List<Student> all = sd.findAll(list);
        sd.deleteInBatch(all);
    }
    /**
     *  刪除方法介紹:
     *  void deleteAll():底層會一條一條刪除
     *  void delete(Iterable<? extends T> entities):底層會一條一條刪除
     *  void deleteAllInBatch():底層會生成一個帶or語句刪除
     *  void deleteInBatch(Iterable<T> entities):底層會生成一個帶or語句刪除
     *  如:Hibernate: delete from student where sid=? or sid=? or sid=?
     */
}

針對圖上面的一些方法操作

提取注意點:【添加/更新/查詢單個】

①:查詢單個getOne/findOne 【延遲加載/立即加載】

@Test   //查詢單個  立即加載
    public void findOneTest(){
        Student student = sd.findOne(3);
        System.out.println("-------分割線-------");
        System.out.println("打印對象:"+student);
        //控制台打印:
        /*
            Hibernate: select student0_.sid as sid1_0_0_, student0_.saddress as saddress2_0_0_,
            student0_.sage as sage3_0_0_, student0_.smoney as smoney4_0_0_, student0_.sname as
            sname5_0_0_ from student student0_ where student0_.sid=?    【SQL語句】
            -------分割線-------
            打印對象:Student{id=3, name='李小二', age=23, money=6666.8, address='安徽大別山'}
        */
        //說明:在一執行到方法時就會去數據庫查詢,然後把數據封裝到實體類對象上
        //底層調用: EntityManager裏面的find()方法
    }

    @Test   //查詢單個  懶加載【延遲加載】
    @Transactional  //此包下import org.springframework.transaction.annotation.Transactional;
    public void getOneTest(){
        Student student=sd.getOne(3);
        System.out.println("-------分割線-------");
        System.out.println("打印對象:"+student);
        //控制台打印
        /*
            -------分割線-------
            Hibernate: select student0_.sid as sid1_0_0_, student0_.saddress as saddress2_0_0_,
            student0_.sage as sage3_0_0_, student0_.smoney as smoney4_0_0_, student0_.sname as
            sname5_0_0_ from student student0_ where student0_.sid=?
            打印對象:Student{id=3, name='李小二', age=23, money=6666.8, address='安徽大別山'}
         */
        //說明:在執行到getOne方法時並不會去數據庫查詢數據,而只是把此方法交給了代理對象,在後面
        //      某個代碼要對查詢的對象student操作時才會去數據庫做真正查詢
        //底層調用:EntityManager裏面的getReference方法
    }

getOne【延遲加載】/findOne【立即加載】

這裏說明一下:在使用延遲加載的時候必須在方法上面加事務註解

②:添加/更新操作

    @Test   //添加
    @Transactional  //開啟事務
    @Rollback(value = false)    //不自動回滾
    public void savetestA(){
        //注意這裏 我調用了一個不帶 ID的構咋函數
        Student student=new Student("蒼井空",25,555.6,"日本東京");
        Student save = sd.save(student);
        System.out.println("添加的數據是:"+save);
    }

    @Test   //更新
    public void updatetestA(){
        //更新操作的時候 首先要知道更新的ID 后通過把數據全部查詢出來,然後再通過set方法修改
        Student student = sd.findOne(5);    //查詢
        student.setName("小次郎");
        student.setAddress("日本");
        //更新
        Student save = sd.save(student);
        System.out.println("更新后的數據是:"+save);
    }
    /**
     * 總結:更新/添加都是save
     * 區別:傳入帶ID的屬性就是更新  不傳入ID屬性就是添加
     * 注意:添加必須添加事務和回滾機制
     */

更新/添加

   綜上總結:在了解了前面的一些簡單的增刪改查,它都是內部提供好的方法(前提實現指定接口),可以滿足我們開發的大部分需求,可是多表查詢、多條件查詢等等是系統沒有定義的,接下來我和大家介紹一下複雜的查詢及動態查詢等等

 三:SpringDataJPA的複雜自定義CRUD操作

 1:使用JPQL語法進行操作(寫接口方法

  其實我們在使用SpringDataJPA提供的方法就可以解決大部分需求了,但是對於某些業務來說是要有更複雜的查詢及操作,這裏我就要使用自定義接口和註解@Query來完成JPQL的一系列操作,關於JPQL操作我在JPA的一章已經說過大概了

//@Repository("studentDao") //這裏不用加入IOC容器 Spring默認幫我們注入
public interface StudentDao extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> {

    //查詢全部 根據年齡和地址查詢
    @Query("from Student where age=?1 and address=?2")
    List<Student> findByAgeAndAddress(Integer age,String address);

    //查詢全部 根據姓名模糊查詢
    @Query("from Student where name like ?1")
    List<Student> findByLikeName(String name);

    //查詢全部 根據地址為空的返回
    @Query("from Student where address is null ")
    List<Student> findByAddressIsNull();

    //查詢全部學生零花錢大於平均零花錢的所有學生
    @Query("from Student where money > (select avg(money) from Student)")
    List<Student> findByMoneyBigAvgMoney();

    //查詢零花錢在某個範圍內並且降序
    @Query("from Student where money between ?1 and ?2 order by money desc")
    List<Student> findByMoneyBetweenToOrderBy(Integer begin ,Integer end);

    //查詢全部個數
    @Query("select count(id) from Student")
    Long totalCount();

    /*********************刪改操作************************/
    //根據姓名刪除
    @Query("delete from Student where name=?1")
    @Modifying
    void deleteByName(String name);

    //更新數據
    @Query("update Student set name=?2 , address=?3 where id=?1")
    @Modifying
    void updateNameAndAddressById(Integer id,String name,String address);
}

使用JPQL自定義方法完成刪改查 【普通常用】

@Test
@Transactional
@Rollback(value = false)
public void test() {
    //裏面調用JPQL自定義的刪改操作
}
說明: 在使用@Query註解裏面定義的JPQL語法時 查詢不需要事務支持,但是刪改必須有事務及回滾操作,
   而且在定義JPQL語句下面要註明@Modify代表是刪改 JPQL 裏面不存在添加insert操作,所有大家要添加操作的話 調用原來存在的方法
//@Repository("studentDao") //這裏不用加入IOC容器 Spring默認幫我們注入
public interface StudentDao extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> {
    //使用排序   地址模糊查詢后 再按照id降序排列
    @Query("from Student where address like ?1 ")
    List<Student> findByLikeAddressOrderById(String address, Sort sort);

    //使用命名參數  查詢年齡和地址模糊查詢
    @Query("select s from Student as s where address like :address and age=:age")
    List<Student> findByLikeAddressAndAge(@Param(value="age") Integer s_age,@Param("address") String s_address);
}
/***下面測試方法****/

    @Test   //查詢學生帶排序
    public void test() {
        //說在前面:Sort如果不指定DESC或者ASC 如 new Sort("id");說明默認對id ASC升序排列
        //設置id為DESC降序排列
        Sort sort = new Sort(Sort.Direction.DESC, "id");
        List<Student> students = sd.findByLikeAddressOrderById("%大%", sort);
        //打印略
    }
    @Test   //使用命名參數
    public void testA() {
        List<Student> students = sd.findByLikeAddressAndAge(23, "%大別山%");
        //打印略
    }

使用JPAL查詢 【其它查方式】

2:使用原生SQL語句進行操作(寫接口方法

  這種寫SQL語句的在SpringDataJPA中並不常用,因為SpringDataJPA對封裝性那麼高,而且是ORM思想,有各種方法及JPQL語句支持,所有我們本地SQL查詢也只要了解會用即可,SQL語句和我們平常寫的都一樣

//@Repository("studentDao") //這裏不用加入IOC容器 Spring默認幫我們注入
public interface StudentDao extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> {

    //查詢指定的年齡和地址
    @Query(value = "select * from student where sage=?2 and saddress=?1",nativeQuery = true)
    List<Student> findByAgeAndAddress(String address,Integer age);
}

3:方法命名規則查詢

  在Spring Data JPA裏面有一種很不錯的操作,那就是在接口中定義方法而不用實現也不用註解就可以實現其查詢操作,但是寫方法名查詢是有一套規範的,這裏我在官方文檔裏面整理出來了分享給大家【注意:方法名命名規則不能對其增加(insert)和修改(update)】

 ①:方法名查詢的具體規則

//示例場景 //當前域為Student 就是對Student上註解了@Entity,表示對於ORM思想它是一張表 //Student域屬性有name、age、address、dog //dog裏面又是一個封裝類 封裝了Dog的一些屬性如 name、color等

①:按照Spring data的定義規則,查詢的方法必須為【find/findBy/read/readBy/get/getBy】種方式開頭,後面則寫條件屬性關鍵字,條件屬性的
    首字母必須大寫,因為框架在進行方法名解析的時候,會把方法名前面多餘的前綴截取掉,然後對剩下的進行解析
例:
    截取前:getByNameAndAge  截取后:NameAndAge
    截取前:findByAddressLikeAndAge 截取后:AddressLikeAndAge
    截取前:findByDogName 截取后:DogName
問題:findByNameOrderByDesc 這種最後攜帶一個 OrderByDesc 排序的怎麼辦呢? 其實最後面攜帶這種對數據進行最後的排序等等操作,框架會對其另外拆分

②:對已經分割好的方法名如 ‘DogName’ ,根據POJO規範會把首字母變為小寫 ‘dogName’,然後用 ‘dogName’這個屬性去Student域里
    面查詢是否存在此屬性,如果是裏面的某個屬性則進行查詢。

③:如果查詢不到此屬性,則會對‘dogName’從右向左來截取,截取到從右向左的第一個大寫字母(所有截取到了Name),然後框架會把剩下
    的字符‘dog’去Student域裏面查詢,找到此屬性則查詢,沒有會重複前面的截取操作,(重要步驟)

④:現在框架提取到了到了‘dog’,然後去Student域查詢到了,則表示方法最終是根據Student.dog.name方式進行查詢,假設name屬性後面
    還是一個封裝類,框架會一直截取,根據③方式截取。

注意:在寫上面這類方法名是,而且查詢的裏面會有一層一層的封裝類,所有強烈建議使用findByDog_Name();

②:注意一些細節(必看)

1:查詢的條件為null時
    實體介紹:Student裡面包含 address為String類型 age為Integer類型 
    查詢方法定義:List<Student> findByAddressAndAge(String address, Integer age);
    調用時傳入值:List<Student> students = sd.findByAddressAndAge(null,23);
    後台生成的SQL:where (student0_.saddress is null) and student0_.sage=?
 
    結論:當我們傳入了一個null值時,表示會去查詢數據庫中address中字段為null的數據其實在編寫此代碼傳入null值以為會跳過此判斷,
         但是恰恰相反,框架會去查詢為null的數據。(實現此方式的只能去動態查詢,這種簡單的查詢不能滿足要求)

2:排序功能
    List<Student> findByAddressOrderByAge(String address); //名稱正序(正序時,推薦此方式,簡單)
    List<Student> findByAddressOrderByAgeAsc(String address); //名稱正序(效果同上)
    List<Student> findByAddressOrderByAgeDesc(String address); //名稱倒序

3:限定查詢結果集大小
    ①:Student findFirstByOrderByAgeAsc();
    ②:Student findTopByOrderByAgeAsc();
    說明:對錶的全部數據根據age進行Asc(升序)排序后再選擇第一條數據返回
    相應SQL:select .... from student student0_ order by student0_.sage asc limit ?  (這裡是limit 1)
    注意但是我如果寫出:List<Student> findTop3ByOrderByAgeAsc(); 則就是每次返回3條 limit 3
    
    ③:List<Student> findFirst2ByAddress(String address,Sort sort);
    ④:List<Student> findTop2ByAddress(String address,Sort sort);
    說明:首先進行數據查詢並通過Sort進行排序后 再篩選數據列表中最前面的2行數據返回 limit 2
    
    ⑤:Page<Student> queryFirst2ByAddress(String address, Pageable pageable)
    ⑥:List<Student> queryFirst2ByAddress(String address, Pageable pageable)
    說明:首先進行數據查詢 查詢全部指定的address後進行分頁,分頁完後進行數據控制
    控制說明:關於帶分頁的控制是,假設分頁過後查詢的數據id為3,4,5 查詢出這三條數據後進行數據控制,
              本案例是控製為2條,那返回的id數據就是3,4兩條記錄,id為5的就捨棄,那麼如果數據控制是5條,
              那麼就會打印3,4,5另外再去添加6,7補充數據長度
    
    總結:這裏的這裏的First和Top意思相同都是最前面,query也和find一樣;
    
    關於一個小點:
        Page<Student> students = sd.queryFirst1ByAddress("安徽大別山",new PageRequest(1,2));
        這裏我是返回分頁后的第一條數據,可是返回了分頁數據的前一個,分頁后id是3,4控制數據后First1
        返回了id為2 

4:計數 返回總數
    Long countByAddress(String address);

5:刪除
    void deleteByAddress(String address);
    //或 void removeByAddress(String address);
    說明:必須添加事務和回滾,這樣根據條件找到幾條就刪幾條
    @Test
    @Transactional
    @Rollback(value=false)
    public void test() {
        sd.deleteByAddress("安徽大別山");
    }
    //對應的SQL語句
    //Hibernate: select .... from student student0_ where student0_.saddress=?
    //Hibernate: delete from student where sid=?
    //Hibernate: delete from student where sid=?
    //Hibernate: delete from student where sid=?
    //Hibernate: delete from student where sid=?
    //Hibernate: delete from student where sid=?
    //Hibernate: delete from student where sid=?

一些注意事項及其它操作

 ③:列舉幾個常用的方法定義查詢

//根據地址查詢後進行姓名模糊匹配
List<Student> findByAddressAndNameLike(String address ,String likeName);

//查詢指定範圍類的個數
List<Student> countByAgeBetween(Integer ... arg);

//根據對應的Address查詢后對查詢出的數據根據age排序后返回第一條數據
Student findFirstByAddressOrderByAgeDesc(String address);

 四:SpringDataJpa的源碼快速分析

   通過前幾節的增刪改查大家會發現,我們不用寫Dao接口實現類而是繼承JpaRepository和JpaSpecificationExcutor這兩個接口就能夠保證正常的增刪改查功能,這是為什麼呢?

//@Repository("studentDao") //這裏不用加入IOC容器 Spring默認幫我們注入
public interface StudentDao extends JpaRepository<Student, Integer>, JpaSpecificationExecutor<Student> {}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;   //這裏注入進去的數據又是誰呢?
    
    @Test
    public void testA(){
        //查詢單個
        Student one = sd.findOne(2);
        System.out.println(one);
    }
}

  其實不難想象,但凡是憑空就可以注入對象的,這內部肯定用到了JDK動態代理,那JDK代理類又是誰呢?經過debug發現是JdkDynamicAopProxy

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
  ....//這個類實現了InvocationHandler 那必然有invoke方法,這裏面是操作該生成哪個代理對象返回 }

  所有說在那個注入的時候,Spring AOP就是通過JdkDyynamicAopProxy類幫我們實現的,那這個代理類最終創建的代理返回的對象又是誰呢?

   那我們再去查看一下SimpleJpaRepository類,肯定是實現了我們自己接口繼承的2個類,而且也有許多的操作方法的實現

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
  .....
}

總結:就是通過Jdk動態代理生成一個對象返回注入后,我們就可以調出各種操作方法

 五:SpringDataJpa動態查詢

  在日常編程中往往是查詢某個實體的時候,給定的條件是不固定的,那我們在不固定查詢條件的情況下就在Dao編寫查詢方法嗎?這顯然是不可取的,只要在確定及肯定會用到這個查詢條件后才會去Dao編寫此查詢方法;那麼在查詢不固定的情況下我們就會用到Specification動態查詢,要想使用動態查詢的話必須在當前使用的Dao接口下繼承JpaSpecificationExecutor接口;在使用動態查詢對比JPQL,其動態查詢是類型安全,更加面向對象。

/**
 * ①:JpaSpecificationExecutor:
 *  用來做動態查詢的接口
 * ②:Specification:
 *  是Spring Data JPA提供的一個查詢規範,要做複雜的查詢,只需圍繞這個規範來設置查詢條件。
 * JpaSpecificationExecutor接口下一共就5個接口方法
 * 有查詢單個、查詢全部、查詢全部【分頁】、查詢全部【排序】、統計總數
 */
public interface JpaSpecificationExecutor<T> {
    T findOne(Specification<T> spec);
    List<T> findAll(Specification<T> spec);
    Page<T> findAll(Specification<T> spec, Pageable pageable);
    List<T> findAll(Specification<T> spec, Sort sort);
    long count(Specification<T> spec);
}

1:常用查詢(簡單)

①:Root<X> 接口
    此接口是代表查詢根對象,可以獲取實體類中的屬性
    如:root.get("name"); 獲取了實體類的name屬性

②:CriteriaBuilder 接口
    此接口用來構建查詢,此對象裏面有許多查詢條件方法
    方法名稱 Sql對應關係
    equle filed = value gt(greaterThan ) filed > value lt(lessThan ) filed < value ge(greaterThanOrEqualTo ) filed >= value le( lessThanOrEqualTo) filed <= value notEqule filed != value like filed like value notLike filed not like value 注:其實這裏面還有許多的查詢條件方法如求平均值啦查詢指定範圍等等

①:根據某個字段查詢單個數據

//查詢單個
@Test
public void testB() {
    // Specification:查詢條件設置
    Specification<Student> spe = new Specification<Student>() {
        //實現接口的方法   關於Root、CriteriaBuilder上面有介紹
        public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            //獲取實體屬性name
            Path<Object> name = root.get("name");
            //根據指定的姓名查詢
            Predicate select = cb.equal(name, "螞蟻小哥");
            //返回一定是Predicate類型
            return select;
        }
    };
    //查詢單個
    Student student = sd.findOne(spe);
    System.out.println(student);
}

②:多條件查詢、模糊查詢等一些操作

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

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

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;

    @Test   //多條件查詢
    public void testC() {
        //根據年齡和地址查詢
        Specification<Student> spe=new Specification<Student>() {
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Object> age = root.get("age");
                Path<Object> address = root.get("address");
                Predicate p1 = cb.equal(age, "23");
                Predicate p2 = cb.equal(address, "安徽大別山");
                Predicate select = cb.and(p1, p2);
                return select;
            }
        };
        List<Student> students = sd.findAll();
        //打印略
    }

    @Test   //查詢多個條件
    public void testD(){
        //根據精準地址和模糊姓名查詢
        List<Student> students = sd.findAll(new Specification<Student>() {
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Object> address = root.get("address");
                Path<Object> name = root.get("name");
                Predicate p1 = cb.equal(address, "安徽大別山");
                Predicate p2 = cb.like(name.as(String.class), "李%");
                return cb.and(p1,p2);
            }
        });
        //注!!!:在使用gt、lt、ge、le、like等條件方法查詢的時候需要告知傳入的屬性的class屬性
        //如:cb.like(name.as(String.class), "李%");
        //打印略
    }

    @Test //查詢全部為指定住址的學生帶年齡排序
    public void testE(){
        List<Student> students = sd.findAll(new Specification<Student>() {
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.equal(root.get("address"), "安徽大別山");
            }
        }, new Sort(Sort.Direction.DESC, "age"));
    }
    
    @Test //查詢全部帶分頁
    public void testF(){
        Page<Student> students = sd.findAll(new Specification<Student>() {
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return null;    //return null 代表沒條件查詢全部
            }
        }, new PageRequest(2, 2));
    }
}

分頁、排序、多條件

2:複雜查詢(了解)

  我們在之前使用過了Root和CriteriaBuilder的2個接口,可是裏面還有一個參數,是CriteriaQuery接口,它代表一個頂層查詢條件,用來自定義查詢,操作頂層查詢可以使我們更靈活,但是在靈活的基礎上又多了一絲絲複雜

    @Test   //多條件查詢
    public void testC() {
        //根據年齡和地址查詢
        Specification<Student> spe=new Specification<Student>() {
            public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Object> age = root.get("age");
                Path<Object> address = root.get("address");
                Predicate p1 = cb.equal(age, "23");
                Predicate p2 = cb.equal(address, "安徽大別山");
                //query內部提供了一些連接用的如where 、having、groupBy....
                query.where(cb.and(p1,p2));
                return query.getRestriction();
            }
        };
        List<Student> students = sd.findAll();
        System.out.println(students);
        //打印略
    }

六:多表操作(一對一了解

補充:多表操作的 一對一、多對一(一對多)、多對多

1:確定多表之間的關係:
    一對一和一對多:
        一的一方稱為主表,而多的一方稱為從表,外鍵就要建立在從表上,它們的取值的來源主要來自主鍵
    多對多:
        這個時候需要建立一个中間表,中間表中最少由2個字段組成,這2個字段作為外鍵指向2張表的主鍵又組成了聯合主鍵

2:配置多表聯繫註解介紹
@OneToOne(一對一)
@ManyToOne(多對一)
@OneToMany(一對多)
@ManyToMany(多對多)
    內部參數:
        cascade:配置級聯操作
            CascadeType.ALL:所有
            CascadeType.MERGE:更新
            CascadeType.PERSIST:保存
            CascadeType.REMOVE:刪除
        fetch:配置延遲加載和立即加載
            FetchType.EAGER 立即加載
            FetchType.LAZY  延遲加載
        mappedBy:放棄外鍵維護

3:配置外鍵關係的註解
@JoinColumn(定義主鍵字段和外鍵字段的對應關係)
    內部參數:
        name:外鍵字段的名稱
        referencedColumnName:指定引用主表的主鍵字段名稱
        unique:是否唯一。默認值不唯一
        nullable:是否允許為空。默認值允許。
        insertable:是否允許插入。默認值允許。
        updatable:是否允許更新。默認值允許。
        columnDefinition:列的定義信息。
@JoinTable(針對中間表的設置)
    內部參數:
        name:配置中間表的名稱
        joinColumns:中間表的外鍵字段關聯當前實體類所對應表的主鍵字段
        inverseJoinColumn:中間表的外鍵字段關聯對方表的主鍵字段

  在多表操作中、一對一併不是我們掌握的重點,因為在開發中最多的還是使用一對多(多對一)和多對多,廢話不多說,我先來和大家說說一對一的具體操作步驟吧!

  我們在開始使用多表操作的時候,要對原來的部分代碼進行改造,建議大家搭建一個全新的項目,具體的代碼我已經在最上面的入門案例中給出了,只要在新項目中導入相應的坐標和配置文件即可,然後修改配置文件裏面的具體數據庫和數據庫密碼即可,還有就是現在的建表語句和上面的全部操作不太一樣,為了更好的演示一對一的操作我這邊準備了一個數據庫建表語句,下面的表含義是,每個學生表(student)都有一條家庭(family)的登記的記錄表,為一對一關係(理想化表,雙胞胎除外)

-- 一對一關係
-- 刪除庫
drop database demo_jpa_one_one;
-- 創建庫
create database if not exists demo_jpa_one_one charset gbk collate gbk_chinese_ci;
-- 使用庫
use demo_jpa_one_one;

# 家庭表創建
create table if not exists family(
    fid int(11) primary key auto_increment, -- 家庭主鍵
    fmember int(11) not null,               -- 成員個數
    fguardian varchar(10) not null,         -- 監護人
    ftel char(11) not null,                 -- 監護人號碼          
    fdad varchar(10),                       -- 爸爸姓名
    fmom varchar(10),                       -- 媽媽姓名
    faddress varchar(20)                    -- 家庭詳細地址
)charset gbk; 
# 創建學生表
create table if not exists student (
    sid int(11) primary key auto_increment,             -- 編號
    sname varchar(5) not null,                          -- 姓名
    ssex enum('','') default '',                  -- 性別
    sage tinyint(11) unsigned not null default 20,      -- 年齡
    smoney decimal(4,1) default 0,                      -- 零花錢 最高999.9
    saddress varchar(10),                               -- 住址
    senrol date default '0000-00-00',                   -- 入學時間
    f_id int(11),                                       -- 連接家庭id
    foreign  key(f_id) references family(fid)           -- 連接家庭id主鍵
)charset gbk;
# 添加家庭信息
insert into family(fmember,fguardian,ftel,fdad,fmom,faddress)values
(0,"余蒙飄","","戚曦維","余蒙飄","安徽省六安市裕安區"),
(0,"孫戀烈","","梁輪亭","孫戀烈","安徽省合肥市瑤海區"),
(0,"張頻時","","張頻時","","安徽省安慶市宜秀區"),
(0,"王京正","","王京正","梁晝庭","安徽省六安市金安區"),
(0,"劉資納","","王諄斌","劉資納","安徽省滁州市全椒縣"),
(0,"白飛解","","廖旺賜","白飛解","安徽省安慶市大觀區"),
(0,"梁昀輝","","鄔國耿","梁昀輝","安徽省蚌埠市蚌山區"),
(0,"古錄鋼","","","古錄鋼","安徽省滁州市定遠縣"),
(0,"姬橋毅","","姬橋毅","寧竹熊","安徽省合肥市蜀山區"),
(0,"劉始瑛","","劉始瑛","韋歡億","安徽省淮南市大通區");
# 添加學生數據
insert into student(sid,sname,saddress,f_id)values
(1 ,"王生安","安徽六安",1 ),(2 ,"李鑫灝","安徽合肥",2 ),
(3 ,"薛佛世","安徽蚌埠",3 ),(4 ,"蔡壯保","安徽安慶",4 ),
(5 ,"錢勤堃","安徽合肥",5 ),(6 ,"潘恩依","安徽合肥",6 ),
(7 ,"陳國柏","安徽六安",7 ),(8 ,"魏皚虎","安徽六安",8 ),
(9 ,"周卓浩","安徽六安",9 ),(10,"湯辟邦","安徽六安",10);
-- 更新數據
update student set ssex=ceil(rand()*2),sage=ceil(rand()*5+20),smoney=(rand()*999),
senrol=concat(ceil(rand()*3+2017),'-' , ceil(rand()*12) , '-',ceil(rand()*20));
update family set ftel=concat(if(ceil(rand()*3)=1,if(ceil(rand()*3)=2,if(ceil(rand()*3)=1,
    if(ceil(rand()*3)=2,"155","166"),if(ceil(rand()*3)=3,"163","170")),"164"),
if(ceil(rand()*3)=3,"188","176")),floor(rand()*90000000+9999999)),fmember=ceil(rand()*3+2);
-- 查詢數據
show create table student\G
show create table family\G
select * from student;
select * from family;

一對一 數據庫建表語句

  其實我們並不需要創建數據庫表(數據庫必須創建),因為我們使用的是ORM思想的框架,說白了就是程序自動幫我們創建表(設置create配置),我們只需要關心實體類就可以了,這個實體類我也為大家準備好了

/**
 * 創建了一個Student的實體-表關係類
 */
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sid")
    private Integer id;
    @Column(name = "sname")
    private String name;
    @Column(name = "ssex")
    private String sex;
    @Column(name = "sage")
    private Integer age;
    @Column(name = "smoney")
    private Double money;
    @Column(name = "saddress")
    private String address;
    @Column(name = "senrol")
    private String enrol;
    //get/set/toString/無參有參構造器 大家自己創建
}

Student的Entity實體對象   未指定外鍵關係

/**
 * 創建了一個Family的實體-表關係類
 */
@Entity
@Table(name = "family")
public class Family {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "fid")
    private Integer id;
    @Column(name = "fmember")
    private Integer member;
    @Column(name = "fguardian")
    private String guardian;
    @Column(name = "ftel")
    private String tel;
    @Column(name = "fdad")
    private String dad;
    @Column(name = "fmom")
    private String mom;
    @Column(name = "faddress")
    private String address;
    //get/set/toString/無參有參構造器 大家自己創建
}

Family的Entity實體對象    未指定外鍵關係

  在準備好了映射關係之後,我們要創建StudentDao、FamilyDao這2個接口,並且分別繼承JpaRepository和JpaSpecificationExecutor這2個接口,完成了這些操作后就可以開始編寫測試類了

<!--大家把配置文件的JPA配置方式改為create每次都創建表-->
<property name="jpaProperties">
     <props>
          <prop key="hibernate.hbm2ddl.auto">create</prop>
     </props>
</property>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;
    @Autowired
    @Qualifier(value = "familyDao")
    private FamilyDao fd;

    @Test   //完成保存操作
    @Transactional
    @Rollback(value = false)
    public void testC() {
       Student stu=new Student("王二","男",26,666.3,"安徽六安","2020-6-5");
       Family fam=new Family(3,"王二牛","17688888888","王二牛","母老虎","安徽六安裕安區白嫖村");
       //開始保存
       sd.save(stu);
       fd.save(fam);
    }
}

如果不出意外,大家可能會遇到一個異常,這正是我想和大家說的,具體說明在文章最後的【關於多表操作異常】問題總結和大家說明一下,我們解決問題后回來再次運行后,控制台會打印2條建表語句和插入數據的語句,前面說過,這2張表是一個一對一的關聯關係,我們接下來就對這2張表建立關係。

建立關係 Student---Family (一對一) 為Student建立關聯到Family的關係(在Student中增加一個字段)
    @OneToOne(targetEntity = Family.class)
    @JoinColumn(name = "f_id", referencedColumnName = "fid")
    private Family family;
為Family建立關聯到Student的關係(在Family中增加一個字段)
    @OneToOne(targetEntity = Student.class)
    @JoinColumn(name = "f_id", referencedColumnName = "sid")
    private Student student;
這樣就完成了相互引用了,每個實體類都擁護一個對方實體,完成了一對一的關聯

完成了關聯后我們對測試方法進行改造

    @Test   //多條件查詢
    @Transactional
    @Rollback(value = false)
    public void testC() {
        Student stu = new Student("王二", "男", 26, 666.3, "安徽六安", "2020-6-5");
        Family fam = new Family(3, "王二牛", "17688888888", "王二牛", "母老虎", "安徽六安裕安區白嫖村");
        //通過set方法分別設置對應的字段,使其關聯
 stu.setFamily(fam); fam.setStudent(stu);
        sd.save(stu);
        fd.save(fam);
    }

運行後會自動幫我們創建2張表並且設置外鍵

mysql> select * from student;
+-----+----------+------+----------+--------+-------+------+------+
| sid | saddress | sage | senrol   | smoney | sname | ssex | f_id |
+-----+----------+------+----------+--------+-------+------+------+
|   1 | 安徽六安  |   26  | 2020-6-5 |  666.3 | 王二  | 男   |  1   |
+-----+----------+------+----------+--------+-------+------+------+
mysql> select * from family;
+-----+----------------------+--------+-----------+---------+--------+-------------+------+
| fid | faddress             | fdad   | fguardian | fmember | fmom   | ftel        | f_id |
+-----+----------------------+--------+-----------+---------+--------+-------------+------+
|   1 | 安徽六安裕安區白嫖村   | 王二牛  | 王二牛     |       3 | 母老虎 | 17688888888  |   1  |
+-----+----------------------+--------+-----------+---------+--------+-------------+------+

  發現自動幫我們創建的表會有2個外鍵,可是這2個外鍵是多餘的,我們這個時候需要放棄外鍵維護,按照之前的一對一建表語句說明,外鍵是建立在學生表上的,通過學生表上面的外鍵來尋找到家庭表的信息,這個時候我們在家庭表上面放棄外鍵維護

//@OneToOne(targetEntity = Student.class)
    //@JoinColumn(name = "f_id", referencedColumnName = "sid")
    //引用對方的字段
    @OneToOne(mappedBy = "family")
    private Student student;

1:一對一的對象導航查詢

<property name="jpaProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <!--設置數據庫方言-->
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
            </props>
        </property>
 @Test   //多條件查詢
    @Transactional
    @Rollback(value = false)
    public void testC() {
        System.out.println(sd.findOne(1));
        //我們要編寫toString方法后才可以打印全部
    }

七:多表操作(一對多/多對一重點

  其實一對多和多對一本質上是一樣的,只是順序發生了變化,我就詳細的把一對多給講一下,在這裏我已經有過一個數據庫建表語句,但是因為是ORM項目,我們建表語句是由程序幫我們自動創建的,之所以我編寫了SQL語句是為了方便後期查詢練習

#### 一對多  這裏我使用的關係是一個老師對應多個學生,一個學生對應一個老師
#### 老師表為主表、學生表為從表
#### 一對多
-- 刪除庫
drop database demo_jpa_one_many;
-- 創建庫
create database if not exists demo_jpa_one_many charset gbk collate gbk_chinese_ci;
-- 使用庫
use demo_jpa_one_many;
-- 創建主表 (輔導員表)
create table if not exists teacher(
    tid int primary key,               -- 編號
    tname varchar(5) not null,         -- 姓名
    tsex enum('','') default '', -- 性別
    tage tinyint unsigned,             -- 年齡
    tsalary decimal(6,1) default 0,    -- 工資 最高99999.9
    taddress varchar(10)               -- 住址
)charset gbk collate gbk_chinese_ci;

-- 創建從表 (學生表)
create table if not exists student (
    sid int(11) primary key auto_increment,             -- 編號
    sname varchar(5) not null,                          -- 姓名
    ssex enum('','') default '',                  -- 性別
    sage tinyint(11) unsigned not null default 20,      -- 年齡
    smoney decimal(4,1) default 0,                      -- 零花錢 最高999.9
    saddress varchar(10),                               -- 住址
    senrol date default '0000-00-00',                   -- 入學時間
    tid int ,                                           -- 連接老師id
    foreign  key(tid) references teacher(tid)  -- 連接老師主鍵
)charset gbk;

-- 添加老師數據
insert into teacher (tid,tname,taddress)values
(1,'張老師','江蘇南京'),
(2,'李老師','江蘇無錫'),
(3,'王老師','江蘇常熟');

-- 添加學生數據
insert into student(sid,sname,saddress)values
(1 ,"王生安","安徽六安"),(2 ,"李鑫灝","安徽合肥"),
(3 ,"薛佛世","安徽蚌埠"),(4 ,"蔡壯保","安徽安慶"),
(5 ,"錢勤堃","安徽合肥"),(6 ,"潘恩依","安徽合肥"),
(7 ,"陳國柏","安徽六安"),(8 ,"魏皚虎",null);

-- 數據更新
update teacher set tsex=ceil(rand()*2),tage=ceil(rand()*10+25),tsalary=ceil(rand()*3000+8000);
update student set ssex=ceil(rand()*2),sage=ceil(rand()*5+20),smoney=(rand()*999),
senrol=concat(ceil(rand()*3+2017),'-' , ceil(rand()*12) , '-',ceil(rand()*20)),tid=ceil(rand()*3);

一對多 關係建表建庫語句

  此時的一對多關係是【一個老師有多個學生】,反之,如果是多對一隻需要調換關係即可,2者差不多,我就針對這個一對多講解;在編寫程序之前我們必須要搞明白什麼是主表和從表的關係,這影響到我們後面編寫一對多的關係:

主表:
    由主鍵和其它字段組成,後期由其它表參照當前表主鍵,此時的表為主表,
    在其它表中如果引用主表的主鍵字段,那麼主表的主鍵被引用后將不能隨意更改,
    如果強制更改則必須設置級聯操作(cascade)
從表:
    由主鍵、外鍵和其它字段組成,後期由本表的外鍵字段去引用其它帶有主鍵的表,一旦其它的
    表主鍵被引用后,被引用的表則被稱為主表,反之引用其它表主鍵的表被稱之從表

一句話概括:有外鍵字段的表是從表(排除其它複雜表,因為有的表即引用其它別也被其它表引用)

  在我們了解了上面的關係后,我們就可以搭建操作一對多的環境了,首先我們就是要準備建造一個數據庫(不是數據表)名稱為demo_jpa_one_many,因為這個框架只要編寫好映射關係後會自動幫我們創建表的;現在建立一個項目,只需要在空項目導入pom.xml坐標和配置外鍵(配置文件一定要修改正確數據庫連接),我將帶大家一步一步完成一對多操作,如果這些都會直接跳過;完成了上面的操作后,我們首先就是要去編寫實體類及映射關係:

/**
 * 創建了一個Teacher的實體-表關係類
 * 在案例中 當前老師表為主表
 */
@Entity
@Table(name = "teacher")
public class Teacher {

    //創建主鍵字段和普通字段
    @Id
    @Column(name = "tid")
    private Integer id;
    @Column(name = "tname")
    private String name;
    @Column(name = "tsex")
    private String sex;
    @Column(name = "tage")
    private Integer age;
    @Column(name = "tsalary")
    private Double salary;
    @Column(name = "taddress")
    private String address;

    //省略 get/set/有參無參構造器
}

老師表映射關係 主表

/**
 * 創建了一個Student的實體-表關係類
 * 在案例中 當前學生表為從表
 */
@Entity
@Table(name = "student")
public class Student {

    //創建主鍵字段和普通字段
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sid")
    private Integer id;
    @Column(name = "sname")
    private String name;
    @Column(name = "ssex")
    private String sex;
    @Column(name = "sage")
    private Integer age;
    @Column(name = "smoney")
    private Double money;
    @Column(name = "saddress")
    private String address;
    @Column(name = "senrol")
    private String enrol;
    //省略 get/set/有參無參構造器
}

學生表映射關係 從表

  在完成上面的一些操作后,大家已經完成了實體類及映射的創建,這裏我沒有為這2張表建立外鍵關係,因為在沒接觸過多表操作的話冒然把映射一起寫完有可能會出現異常(肯定有人說,這咋這麼啰嗦呀)那好,對有操作的,只是複習怎麼使用的直接可以當cv攻城師複製即可;現在我們來編寫2個dao類

public interface StudentDao extends JpaRepository<Student,Integer>, JpaSpecificationExecutor<Student> { }
public interface TeacherDao extends JpaRepository<Teacher,Integer> , JpaSpecificationExecutor<Teacher> { }

  寫好以後就正式測試了,測試完成后大家會發現是2個沒有任何關係的2張獨立的表(如果出現異常了去下面查看異常講解

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;
    @Autowired
    @Qualifier(value="teacherDao")
    private TeacherDao td;

    @Test   //多條件查詢
    @Transactional
    @Rollback(value = false)
    public void testC() {
        Student student=new Student("張小三","男",25,222.3,"安徽六安","2018-8-8");
        Teacher teacher=new Teacher(10,"張老師","男",35,9999.6,"北京順義");
        //保存
        td.save(teacher);
        sd.save(student);
    }
}

測試代碼 創建2張表

mysql> select * from student;
+-----+----------+------+----------+--------+--------+------+
| sid | saddress | sage | senrol   | smoney | sname  | ssex |
+-----+----------+------+----------+--------+--------+------+
|   1 | 安徽六安   |   25 | 2018-8-8 |  222.3 | 張小三 | 男   |
+-----+----------+------+----------+--------+--------+------+
mysql> select * from teacher;
+-----+----------+------+--------+---------+------+
| tid | taddress | tage | tname  | tsalary | tsex |
+-----+----------+------+--------+---------+------+
|  10 | 北京順義 |   35 | 張老師   |  9999.6 | 男    |
+-----+----------+------+--------+---------+------+

注:創建2表的聯繫

  在創建2表的關係中,我們必須要分清主表和從表的關係,這樣才可以設計出一個完整的關係創建,具體的主表和從表上面以給出介紹

創建從表的關係連接(student)
在從表中添加一個引用teacher屬性,因為一個student中有一個teacher
//創建外鍵字段 因為從表上有明確外鍵引用其它表 所以必須要有外鍵字段
@ManyToOne(targetEntity = Teacher.class) @JoinColumn(name="t_id",referencedColumnName = "tid") private Teacher teacher;
注:
    targetEntity = Teacher.class:當前引用的主表類型
    name="t_id" 代表當前student表中的外鍵字段
    referencedColumnName = "tid"  代表參照主表的哪個字段
建表樣式:
mysql> select * from student;
+-----+----------+------+----------+--------+--------+------+------+
| sid | saddress | sage | senrol   | smoney | sname  | ssex | t_id |
+-----+----------+------+----------+--------+--------+------+------+
|   1 | 安徽六安 |   25 | 2020-6-6 |  666.3 | 李小二 | 男   |  100 |
+-----+----------+------+----------+--------+--------+------+------+

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 創建主表的關係連接(teacher)
在主表中添加一個引用student的屬性,因為一個teacher中有多個student
//創建外鍵字段 這裡是主表,所以主動放棄外鍵維護
//@OneToMany(targetEntity = Student.class)
//@JoinColumn(name = "s_id",referencedColumnName = "sid")
//引用從的外鍵字段
@OneToMany(mappedBy = "teacher") private Set<Student> students=new HashSet<Student>();

注:
    mappedBy = "teacher" :代表參照對方表

假設主表不放棄外鍵維護就會出現下面情況:
mysql> select * from teacher;
+-----+----------+------+--------+---------+------+------+
| tid | taddress | tage | tname  | tsalary | tsex | s_id |
+-----+----------+------+--------+---------+------+------+
| 100 | 北京順義 |   32 | 王老師 |  6666.6 | 男   |   1  |
+-----+----------+------+--------+---------+------+------+
問題所在:這時候主表引用從表,而從表也引用主表,這顯然不是一個合格設計
解決后:在主表上設置的放棄外鍵維護,並參照從表的關係
mysql> select * from teacher;
+-----+----------+------+--------+---------+------+
| tid | taddress | tage | tname  | tsalary | tsex |
+-----+----------+------+--------+---------+------+
| 100 | 北京順義 |   32 | 王老師 |  6666.6 | 男   |
+-----+----------+------+--------+---------+------+

  創建測試,在測試的時候大家把配置文件的配置改為create,代表每次執行都會創建表

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {
    //注入數據
    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;
    @Autowired
    @Qualifier(value="teacherDao")
    private TeacherDao td;

    @Test   //多條件查詢
    @Transactional
    @Rollback(value = false)
    public void testC() {
        //創建2個對象
        Student student=new Student("李小二","男",25,666.3,"安徽六安","2020-6-6");
        Teacher teacher=new Teacher(100,"王老師","男",32,6666.6,"北京順義");
        //把老師放入學生數據中  把學生數據放入老師表中
        student.setTeacher(teacher);
        teacher.getStudents().add(student);
        //保存 這裏注意一下,因為我的teacher主鍵不是自動生成 ,
        // 所以先保存teacher才可以保存student,因為teacher主鍵不是自動生成,直接先保存student會無法獲取teacher主鍵
        td.save(teacher);
        sd.save(student);
    }
}

測試方法

  然後會生成和我給出的創建sql語句生成的相同的字段,並且外鍵也是可以的,如果出現異常,大家檢查一下get/set/無參構造/有參構造,這裏的無參構造必須存在,否則真的會異常

1:級聯操作

  級聯操作分級聯刪除、級聯添加、級聯更新,如果設置了級聯操作就可以完成級聯操作,具體的在一對一上的補充介紹了

    @OneToMany(mappedBy = "teacher",cascade = CascadeType.ALL)
    private Set<Student> students=new HashSet<Student>();

  在主表上設置了級聯操作(ALL=全部都支持),只要保存teaccher就會級聯着保存student

    @Test   //多條件查詢
    @Transactional
    @Rollback(value = false)
    public void testC() {
        //創建2個對象
        Student student=new Student("李小二","男",25,666.3,"安徽六安","2020-6-6");
        Teacher teacher=new Teacher(100,"王老師","男",32,6666.6,"北京順義");
        //把老師放入學生數據中  把學生數據放入老師表中
        student.setTeacher(teacher);
        teacher.getStudents().add(student);
        //直接添加老師就可以保存雙方數據
        td.save(teacher);
    }

2:查詢數據

  這個時候我們導入之前的sql語句,然後把配置改為update,每次執行,有表則不創建表,

    @Test   //多條件查詢
    @Transactional //必須添加事務
    public void testC() {
        Teacher teacher = td.findOne(2);
        System.out.println(teacher);
    }

 八:多表操作(多對多)

  多對多是一個雙向關係,我首先來展示一下SQL語句

##### 多對多

-- 刪除庫
drop database demo_jpa_many_many;
-- 創建庫
create database if not exists demo_jpa_many_many charset gbk collate gbk_chinese_ci;
-- 使用庫
use demo_jpa_many_many;
-- 創建從表 (學生表)
create table if not exists student (
    sid int(11) primary key auto_increment,             -- 編號
    sname varchar(5) not null,                          -- 姓名
    ssex enum('','') default '',                  -- 性別
    sage tinyint(11) unsigned not null default 20,      -- 年齡
    smoney decimal(4,1) default 0,                      -- 零花錢 最高999.9
    saddress varchar(10),                               -- 住址
    senrol date default '0000-00-00'                    -- 入學時間
)charset gbk;

# 學生社團組織
create table if not exists organization(
    oid int(11) primary key auto_increment,  -- 社團主鍵id
    oname varchar(10) not null unique        -- 社團名稱
)charset gbk;
# 中間表 社團和學生對應多對多關係
create table if not exists student_organization(
    soid int(11) primary key auto_increment,         -- 中間表id
    s_id int(11),                                    -- 學生id
    o_id int(11),                                    -- 社團id
    foreign key(s_id) references student(sid),       -- 連接學生id
    foreign key(o_id) references organization(oid)   -- 連接社團id
)charset gbk;

# 添加學生社團組織學習
insert into organization (oname) values
("書法協會"),("法律協會"),("武術協會"),
("魔術社團"),("網球協會"),("啦啦隊團");
-- 添加學生數據
insert into student(sid,sname,saddress)values
(1 ,"王生安","安徽六安"),(2 ,"李鑫灝","安徽合肥"),
(3 ,"薛佛世","安徽蚌埠"),(4 ,"蔡壯保","安徽安慶"),
(5 ,"錢勤堃","安徽合肥"),(6 ,"潘恩依","安徽合肥"),
(7 ,"陳國柏","安徽六安"),(8 ,"魏皚虎",null);
# 添加學生和社團的中間表
insert into student_organization(s_id,o_id) values
(1,1),(1,3),(1,2),(1,6),(2,5),(2,1),(2,2),
(6,6),(6,2),(6,4),(6,3),(7,4),(7,2);

多對多建表語句

  上面多對多關係是一個學生可以加入多個社團(組織),一個組織可以有多名學生,間就構建成多對多關係,廢話不多說,直接上映射關係

在創建映射關係的時候我們得確定哪一方會放棄外鍵維護權,
在多對多的時候我們通常放棄的被動的一方



@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sid")
    private Integer id;
    @Column(name = "sname")
    private String name;
    @Column(name = "ssex")
    private String sex;
    @Column(name = "sage")
    private Integer age;
    @Column(name = "smoney")
    private Double money;
    @Column(name = "saddress")
    private String address;
    @Column(name = "ssenrol")
    private String senrol;

//    @ManyToMany(targetEntity = Organization.class)
//    @JoinTable(name = "student_organization",
//            joinColumns = {@JoinColumn(name = "s_id", referencedColumnName = "sid")} ,
//            inverseJoinColumns = {@JoinColumn(name = "o_id", referencedColumnName = "oid")}
//    )
    @ManyToMany(mappedBy = "students")
    private Set<Organization> organizations = new HashSet<Organization>();
}


@Entity
@Table(name = "organization")
public class Organization {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "oid")
    private Integer id;
    @Column(name = "oname")
    private String name;

    @ManyToMany(targetEntity = Student.class)
    @JoinTable(name = "student_organization",
            joinColumns ={@JoinColumn(name = "o_id",referencedColumnName = "oid")} ,
            inverseJoinColumns = {@JoinColumn(name = "s_id",referencedColumnName = "sid")}
    )
    private Set<Student> students=new HashSet<Student>();
}

多表映射關係

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Client {

    @Autowired
    @Qualifier(value = "studentDao")
    private StudentDao sd;

    @Autowired
    @Qualifier(value = "organizationDao")
    private OrganizationDao od;

    @Test
    @Transactional
    @Rollback(value = false)
    public void fun() {
        Student student=new Student("張三","男",45,555.6,"安徽六安","2012-8-8");
        Organization o=new Organization("魔術社");
        student.getOrganizations().add(o);
        o.getStudents().add(student);
        od.save(o);
        sd.save(student);
    }

}

測試環境

九:對象導航查詢

  在一對多查詢的時候,多的一方數據會有延遲加載,但是對於一對一查詢的時候數據會有立即加載

十:關於多表操作異常

1:關於hibernate數據庫方言問題(dialect)

6月 03, 2020 4:57:00 下午 org.hibernate.dialect.Dialect <init> INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
省去部分...
Hibernate: create table family (...) type=MyISAM
//上面一局為我們創建的是一張表並設置MyISAM引擎 錯誤就在這 無法運行了
6月 03, 2020 4:57:01 下午 org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl handleException WARN: GenerationTarget encountered exception accepting command : Error executing DDL "create table family (...) type=MyISAM" via JDBC Statement org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "create table family (...) type=MyISAM" via JDBC Statement //從上面錯誤可以看出 程序運行的時候默認的數據庫方言設置了 org.hibernate.dialect.MySQLDialect 而這個默認是MyISAM引擎

  問題所在:因為我導入的hibernate坐標是5.4.10.Final,在導入這類高版本的坐標往往要為數據庫方言設置MySQL5InnoDBDialect的配置,在我前面也測試了,關於坐標版本問題,發現5.0.x.Final左右的版本不用設置數據庫方言,默認即可。

<property name="jpaProperties">
   <props>
     <prop key="hibernate.hbm2ddl.auto">create</prop>
     <!--設置數據庫方言--> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
   </props>
</property>

  具體的版本在創建數據庫表的時候會拋各種異常,這裏我整理了一下數據庫方言,方便大家參考

<property name="jpaProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">create</prop>
                <!--設置數據庫方言-->
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
            </props>
        </property>

<!--數據庫方言-->
DB2                         org.hibernate.dialect.DB2Dialect
DB2 AS/400                  org.hibernate.dialect.DB2400Dialect
DB2 OS390                   org.hibernate.dialect.DB2390Dialect
PostgreSQL                  org.hibernate.dialect.PostgreSQLDialect
MySQL                       org.hibernate.dialect.MySQLDialect
MySQL with InnoDB           org.hibernate.dialect.MySQLInnoDBDialect
MySQL with MyISAM           org.hibernate.dialect.MySQLMyISAMDialect
Oracle (any version)        org.hibernate.dialect.OracleDialect
Oracle 9i/10g               org.hibernate.dialect.Oracle9Dialect
Sybase                      org.hibernate.dialect.SybaseDialect
Sybase Anywhere             org.hibernate.dialect.SybaseAnywhereDialect
Microsoft SQL Server        org.hibernate.dialect.SQLServerDialect
SAP DB                      org.hibernate.dialect.SAPDBDialect
Informix                    org.hibernate.dialect.InformixDialect
HypersonicSQL               org.hibernate.dialect.HSQLDialect
Ingres                      org.hibernate.dialect.IngresDialect
Progress                    org.hibernate.dialect.ProgressDialect
Mckoi SQL                   org.hibernate.dialect.MckoiDialect
Interbase                   org.hibernate.dialect.InterbaseDialect
Pointbase                   org.hibernate.dialect.PointbaseDialect
FrontBase                   org.hibernate.dialect.FrontbaseDialect
Firebird                    org.hibernate.dialect.FirebirdDialect

數據庫方言 hibernate.dialect

2:關於對象導航查詢異常

WARNING: Please consider reporting this to the maintainers of org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release
Hibernate: sql語句
java.lang.StackOverflowError at java.base/java.lang.AbstractStringBuilder.inflate(AbstractStringBuilder.java:202)
    at java.base/java.lang.AbstractStringBuilder.putStringAt(AbstractStringBuilder.java:1639)
    at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:513)
   .......

  關以這種錯誤大多都是編寫的toString語句發生了錯誤,大家在寫toString的時候一定要分清主表和從表,在編寫主表的toString的時候一定要去除外鍵字段,而在編寫從表的時候一定要加上外鍵字段,因為平時我們都是通過從表查詢數據(因為從表有指向主表的外鍵),這樣可以把主表的數據通過外鍵查詢出來,但是主表上如果也有從表的字段的話就會一隻循環,數據沒完沒了,所有拋異常也是對的,

總結一句話,從表有主表的字段,主表有從表的字段,打印從表順帶打印主表,但是主表裡面還有從表字段,然後繼續打印從表字段…….

 3:關於延遲加載事務問題

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: cn.xw.domain.Teacher.students, could not initialize proxy - no Session at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606) at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585) at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149) at org.hibernate.collection.internal.PersistentSet.toString(PersistentSet.java:327) at java.base/java.lang.String.valueOf(String.java:2801) at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)

  關於在使用延遲加載的時候,在當前的方法上必須設置@Transactional,因為在使用延遲加載底層已經使用了事務的相關方法

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

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

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

震驚!當Python遇到Excel后,將開啟你的認知蟲洞_潭子電動車

※超省錢租車方案

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

本文主要內容:   1. Excel,你為什麼如此強大 2. 軟件開發也需要團隊作戰 3. Excel的集成方案演化 4. macOS特有的集成方案:applescript 5. Python與Excel集成,有哪些好處 6. 了解多個技術領域,可以間接提高自己的IQ 7. 提高開發效率的利器:生態滲透 8. 上代碼:Python到底如何與Excel交互 9. 用Python替代VBA  
本文主要講Python與Excel的關係以及集成方案,Office家族的其他成員,如Word、PowerPoint與Excel擁有類似的功能,Python同樣可以與Word、PowerPoint等Office成員結合,這些內容我以後會寫文章講解。   我相信看這篇文章的大多數人都是程序員。在廣大程序員的眼裡,Excel以及Office家族的其他成員完全不能與Python、Java、Sprint Boot這些技術相比。Office被貼上的標籤就是:非專業人員的工具。是由那些體制內的人、會計、業務人員使用的工具,其實這是對Excel的最大誤解。

 1. Excel,你為什麼如此強大

  Excel從一問世開始,定位就是橫跨辦公和開發兩界。在上個世界末推出的Excel5.0(那時還沒有office這個產品套件)就已經支持VBA了,那可是在Win32下(一種基於dos的shell,並不屬於真正的操作系統)。儘管有些原始(以現在的眼光看),但在當時卻顯得極為先進。因為Excel(以及同期的Word)是人類史上第一個內置領域特定語言(DSL)的民用系統(可以被各個領域的人使用的系統)。正是因為有了VBA,Excel才顯得非常強大。   可能有的程序員會問,即使Excel支持VBA,可以進行編程,也只不過是完成一些自動化操作而已。VBA的功能完全不能與Python、Java、C#、C++這些被廣大程序員熟知的編程語言相比,更別說成為編程領域的主流了。沒錯,如果單憑VBA本身的確無法與這些流行的編程語言相比,但要知道,VBA和Excel本身可以與其他開發工具融合,也就是說,可以讓VBA和Excel成為自己系統的一部分,這就顯得非常牛逼了。

 2. 軟件開發也需要團隊作戰

  那麼為什麼要將Excel與其他系統結合呢?其實在我們創業時、在公司完成一個項目時,甚至在參加籃球比賽時,都會高頻率提到一個詞:團隊。 現在也流行着一句話:沒有完美的個人,只有完美的團隊。也就是說,沒有人可以什麼都會,什麼都擅長,就算是天才達芬奇也是如此。但團隊就不一樣了,即使某項工作團隊里沒人能做,也可以擴充團隊成員,吸引能做這項工作的人加入團隊。   其實在開發軟件的過程中也同樣是講究團隊作戰的。不管我們使用的開發工具有多強大,用戶社區由多龐大,都無法規避一個事實:任何技術都不可能適合完成所有的工作。 當然,還有另外一種說法就是儘管某種技術可以完成某項工作,但並不是最好的選擇。   現在回到Excel上來。首先要知道Excel擅長什麼,對,沒錯,Excel擅長表格和圖表的製作。儘管有非常多的開發工具也可以製作表格和圖表,但很顯然,Excel是其中最好的(主要用門檻、易用性、功能和用戶基數的綜合指標來衡量)。所以,如果某一個系統要求將數據轉換為表格和圖表,那麼這項任務交給Excel來完成是再合適不過了。  
3. Excel的集成方案演化   在不同的時期,Excel的集成方案也不盡相同。在Excel的大眼夾時代(那時還是Office97和Office2000的時代),微軟與開源世界還水火不容,我也還處於學生時代。在那個時代,Excel只能與Windows下的開發工具融合,其中主要的開發工具包括微軟的VC、VB、以及Borland(現在已經消失)的Delphi、C++ Builder等。     最初的集成方案有如下3種:

  • 可視方式:通過OLE組件將Excel直接嵌入窗口中,其實就相當於擁有了一個高級版的表格編輯器
  • VBA方式:通過ActiveX技術創建Excel.Applicaiton對象(一般是CreateObject函數),然後就可以任意調用VBA的API了,我比較喜歡這種方式。通過這種方式,可以將系統中的數據按着一定的格式直接傳輸到Excel中,給用戶提交的是包含表格數據的Excel文檔。
  • 反客為主方式:這種方式將Excel作為主體。也就是說,主要的操作界面是Excel,在Excel中調用或訪問其他系統。例如,很多年前我做過一個基於Excel的報表系統。該系統分別使用Delphi和Excel實現。Delphi做的管理系統,將數據發送給Excel。但需要用戶自己調整報表格式。我採用的方案是通過Excel的VBA實現表格的格式設置。其中有部分功能需要訪問SQL Server數據庫,以及完成與Delphi實現的系統類似的功能,這部分功能使用了Delphi封裝的Dll(COM組件),然後通過Excel反過來調用這些DLL。最終用戶使用的方式是用主系統完成大部分工作。如果想調整Excel的報表樣式,可以直接用Excel來完成(會在Excel上提供一些自定義的菜單和按鈕,現在通過加載項【Add-ins】實現)

  不過隨着時間的推移,現在的微軟已經擁抱開源和其他系統。所以Excel也不僅限於Windows。在macOS上也可以完成在Windows上的大多數工作,甚至Visual Studio也推出了macOS版本。而且.net core也可以同時跨Windows、macOS和Linux三個平台,SQL Server也開始支持Linux。以後我們會在更多的場景看到微軟的身影。   既然Excel已經支持了macOS平台,就需要採用跨平台的方式與Excel集成。當然,前面介紹的幾種方案現在仍然可用,但僅限於Windows平台。不過我們無法預測用戶到底使用哪一個平台,所以應該盡量使用跨平台方案。   目前主要的跨平台方案有如下3種:

  • 直接修改xlsx文件
  • 通過VBA間接調用其他編程語言
  • 使用office.js

  第1中方式有很多編程語言都支持,例如,Python、Java、Julia、Go、JavaScript。幾乎你能想到的編程語言,都有支持xlsx格式的庫。這裏只討論Python。如果想了解更多關於集成Excel的技術,可以關注我的公眾號:極客起源。   在Python語言中,支持Excel文件格式的庫非常多,如非常著名的openpyxl、xlsxwriter等。通過這些庫,可以在不依賴Excel環境的情況下,生成xlsx格式的文件。不過這些庫基本是只是生成Excel文件,並不能更好地利用VBA以及更高級的功能。由於Excel文件格式非常複雜,完全支持比較困難,所以這些庫只是支持一部分Excel的功能,但這些功能對於絕大多數需求已經足夠了。   第2種其實是一種取巧的方式,通過VBA做橋,調用其他編程語言,相當於用其他編程語言代替了VBA。這其中典型的就是xlwings。儘管這種方式從表面上看可以直接在Excel中像使用VBA一樣使用這些編程語言,但從本質上看仍然是直接寫xlsx文件。功能其實與第1種方式相同,因為這並不是官方支持的功能。   第3種是office.js,這是微軟官方提供的一個基於JavaScript的程序庫。基於Node.js,可以用JavaScript完全取代VBA實現Excel以及Office其他成員的加載項。office.js可以在Electron、Web應用以及大多數基於JavaScript的場景中使用。關於office.js的內容我以後會寫文章詳細描述,對office.js感興趣的同學也可以關注“極客起源”公眾號,會不定期更新這方面的內容。    
4. macOS特有的集成方案:applescript 除了跨平台解決方案外,在macOS上,還支持使用applescript與Office(Excel、World、PowerPoint等)交互,這些內容我以後再撰寫文章詳細講解。如果要了解excel applescript api,可以參考Excel Reference。從效果來看,applescript操作Office與在Windows下通過COM組件操作Office類似,可以完全控制Office,只是applescript的語法更接近自然語言。   下面的applescript代碼會創建一個新的Excel文檔,並將其保存為first.xlsx文件。  

 

 運行后,會看到一個打開的Excel文檔,並且已經保存為firstx.xlsx文件。

 

 5. Python與Excel集成,有哪些好處

  Excel對於Python來說,可以將Excel看做是一個可編程的大組件。這個組件的主要功能就是可以製作任意複雜的報表和圖表。儘管Python有很多模塊可以製作報表和圖表。但這些模塊的功能和效果完全沒辦法與Excel相比。所以將Python與Excel結合的最大好處是可以快速完成製作報表的任務,而且效果杠杠滴。   將Python與Excel相結合,其實還會引出另外一個思考,就是成為專家還是通才的問題。我聽到有很多程序員說,要將某種語言搞通,如PHP、Python、Java等,然後就可以很輕鬆解決所有的問題。結果真是這樣嗎?   很久以前,我聽過一個關於微軟的故事(相信很多人也聽過),在微軟有一個幾十人的團隊,花了好幾個月還沒完成一個項目,聽說是遇到了某些難題。這時有一個老程序員(據說至少50歲以上)將自己關在辦公室里一個星期,搞定! 我們先不管這個故事是真是假,那麼從理論上來說,是否有這個可能呢?其實如果光看編程速度,再牛叉的程序員,也不可能比普通程序員快幾十倍,更何況數百倍了。但還有另外一種可能,就是這名老程序員使用了完全不同的方法,繞過了大多數影響效率的因素,例如,使用了不同的工具,採用了不同的轉換方式,甚至使用了不同的設計理念等等。這就不是能力問題了,而是認知的問題。我將其稱為“認知蟲洞”。

 所謂“認知蟲洞”,是指通過某種方式很難完成某項工作,但通過另外完全不同,甚至是顛覆三觀的方式,可以用極短的時間達到目的,而且效果極好。就像找到了可以穿越浩瀚星空的蟲洞。這也有點像數學中的“等價替換”。

  可能這個微軟的例子離我們太遠,下面舉一個我自己的關於Excel例子,很多年前,我還在國內某大型軟件公司作高級程序員。團隊需要製作大量的報表,使用的主要開發工具是Delphi、後端是SQL Server數據庫。Delphi本身有自己的報表系統,叫QuickReport。功能是很強大的,但問題是,做起來太費勁。例如,要畫表格線時,如果一不小心將某根線拖到了別的地方,而且被其他東西覆蓋,那你就找把,還必須要找到,否則打印出來的表格上就會莫名其妙多了根線。結果團隊好幾個人弄了好幾天還沒弄完(也包括我)。後來我實在不想這麼弄了(因為買了幾張影碟,着急回家看電影,不想加班),於是想到了利用Excel或Word來完成這個報表系統。通過Delphi傳輸數據。   說干就干,花了不到2個小時,所有的報表全部搞定(只有我一個人哦),幾個人幾天都沒搞定的東西,我自己不到2個小時搞定,這當然不是我編程速度快了幾十倍,而是處在了不同的維度,使用了完全不同的技術來實現,用QuickReport需要一根線一根線的畫,而使用Excel,我不需要畫線,只需要用SQL語句查詢出數據,然後將這些數據發送給Excel即可。單單用了一個Excel,速度就提高了這麼多,如果系統中很多部分都使用了類似的技術,那麼編程效率提高數百倍,甚至上千倍,也不是沒有可能的。  
6. 了解多個技術領域,可以間接提高自己的IQ   可能有的同學會問,既然可以將多種技術結合起來大幅度提高開發效率,那麼為什麼不通過團隊合作的方式來完成了,通常一個人無法學會那麼多技術。其實這就是一個認知的問題,團隊合作只有在項目所採用的技術被確定后,例如,如何集成多種技術,才可以發揮作用。問題是,如果多種知識分散在不同人的大腦中,很可能沒有人意識到應該去這樣融合多種技術,就更談不上團隊合作了,就像你要探索宇宙,首先你要知道存在宇宙這種東西,否則怎麼去探索呢? 我將這種現象稱為“認知孤島”(相對於“認知蟲洞”而言),就是說並不是沒有能力去做,而是壓根就沒有意識到應該這樣做(由於知識的缺乏、同時導致想象力的受限)。   為什麼達芬奇那麼牛逼,除了聰明之外,達芬奇還橫跨多個完全不同的領域,正是因為對生物學和解刨學的了解,蒙娜麗莎的畫像才會那麼自然,栩栩如生,因為達芬奇對骨骼、肌肉的構造非常了解,這是其他任何畫家都無法比肩的。如果你在某一個領域排名前20%,而在另外一個領域排名也是前20%,那麼如果需要兩個領域的知識來解決問題時,你就會排名前4%(20% * 20%),如果是3個領域就是百里挑一。如果是6個領域,那就是萬里挑一,據說達芬奇涉足十多個領域。由於自己和自己溝通的成本為0,所以只有擁有足夠多的知識,並且有融合他們的能力,那麼你就是下一個達芬奇!

 我們可以舉個數學與Python的例子:

 如果我們的程序需要計算某個表達式的定積分(例如y = 2 * x從0到1的定積分)。    假設我們使用的是Python語言,並且不太清楚有什麼庫可以自動計算定積分,那麼採用的方式就是利用數值計算的方式寫程序去完成,計算定積分的數值計算公式比較複雜,可能不是在短時間內能完成的。不過要是了解sympy這個庫,那就是幾行代碼的事:  

import sympy
x = sympy.Symbol('x')
f = 2 * x
# 開始計算定積分
print(sympy.integrate(f,(x,0,1)))

是不是很簡單呢? 只需要了解一個API的用法就搞定了。

假設現在我們還有一個需求,要計算某個函數在某一點的導數(導數在深度學習中經常使用),而手頭又沒有必要的庫(也有可能是不知道),那麼只要了解導數的原理,就很容易通過幾行代碼搞定,這就屬於高等數學的範疇了。  

 導數原理,右側是導數的計算公式,

# 計算導數的函數
def derivative(f,x):
  h = 0.0001   # x軸的增量,需要是一個很小的值,但要在浮點數精度範圍內,通常不能超過小數點后6位
  return (f(x + h) - f(x - h)) / (2 * h)
# 待計算的函數1(y = 2 * x)  
def f1(x):
    return 2 * x
import math
# 待計算的函數2(  y = sin(x) * cos(x) / (sin(x) + cos(x))  )
def f2(x):
    return math.sin(x) * math.cos(x) / (math.sin(x) + math.cos(x))

print(derivative(f1,10))    # 1.9999999999953388
print(derivative(f2,123))   # -0.331842825692652

   PS:對這些代碼和高等數學不熟悉也沒關係,這裏我只是舉個例子,後期我會寫一些關於數學和編程方面的文章,詳細解釋這些好玩的東西,可以關注我的公眾號:極客起源 ,會不斷更新各種技術和數學文章,以及視頻課程。   從這兩個案例可以看出,在某一個領域需要非常費勁才能搞出來的東西,在另一個領域其實就是hello world。如果了解足夠多的領域,那麼完成很多工作,就會表現出天才的特徵(這也是成為天才的途徑之一,另一個途徑是投胎)。  

 PS:Excel的功能不僅僅是製作報表,Excel還擁有強大的數據分析能力。所以如果將Python與Excel集成,就意味着Python將擁有了Excel的全部能力,相當於Python擁有了Excel的整個生態。我將其稱為“生態滲透”。也就是通過集成或其他方式,一種技術可以直接或間接使用另外一種技術的全部或大部分資源。

7. 提高開發效率的利器:生態滲透    在未來,支持生態滲透的開發方式會非常普遍,如果只是用了一些現成的庫或開源軟件,並不能大幅度提高開發效率,但如果可以利用某些強大系統的生態,就不一樣了。在未來,還會有很多支持“生態滲透”的開發工具。例如,我們團隊研發的UnityMarvel,就是一款超平台開發系統。這裏之所以稱為“超平台”,而不是“跨平台”,是因為UnityMarvel不僅僅可以跨操作系統平台,還可以跨數據庫平台,雲平台、API平台、開源硬件、物聯網等,以及支持虛擬SQL、客戶端服務端一體化、柔性熱更新、Office加載項、瀏覽器插件等新特性。因此稱為“超平台”開發系統。並且自己研發了Ori編程語言(語法融合了Python、Java、Go等語言的優秀特性,但功能得到了前所未有的增強)。通過這些特性,可以用前所未有的規模利用其他系統的生態,要遠比Python使用Excel的生態更完美。其他功能先不解釋(等發布后我再寫文章詳談),先說說UnityMarvel是如何跨數據庫的。   所謂跨數據庫(目前指關係型數據, 以後會支持文檔、鍵值等NoSQL數據庫),是指用UnityMarvel開發基於數據庫的應用並不需要事先確定到底用什麼數據庫(如MySQL、SQL Server、Oracle等),UnityMarvel內置了一種虛擬數據庫,可以直接用虛擬數據庫開發,在發布時,會要求選擇使用的數據庫,例如,選擇MySQL或SQL Server。UnityMarvel會通過rosetta引擎將Ori語言的代碼轉換為支持MySQL的代碼。關於數據庫的部分,主要是用過內置的一種虛擬SQL完成的,而且這種SQL語言是與Ori語言是融為一體的。例如,如果要從persons表中查詢出id大於30的所有記錄,可以直接這樣寫:  

var  result = SELECT * FROM persons
                                 WHERE id > 30;

  result的類型是SQLSelect,將result賦給Grid組件,就會直接显示查詢結果。當發布時,會將上面的代碼轉換為使用相應數據庫(如MySQL、SQL Server)的特定編程語言(如JavaScript、Java等)的代碼。這麼做的好處如下: 1. 不需要進行數據庫選型,數據庫是在發布時後期綁定的; 2. 如果想切換數據庫(例如,從MySQL換成Oracle),只需要重新發布,選擇相應的數據庫就可以了,不需要修改一行代碼; 3. 統一數據庫接口,開發人員並不需要了解各種數據庫的細節,開發門檻低; 4. UI與數據庫交互非常容易,不必考慮各種數據庫引擎和庫,只需要直接將SQL語句賦給與其交互的UI組件即可; 5. 自動檢測和去除大多數SQL中的潛在風險,如SQL注入等; 6. 脫離數據庫環境開發。例如,想使用MySQL數據庫開發,但當前機器上並沒有MySQL開發環境,又不想安裝MySQL。這時仍然可以用UnityMarvel內置的虛擬數據庫進行開發,然後發布即可,部署在有MySQL環境的機器上就可以成功運行了;  

 

 

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

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

8. 上代碼:Python到底如何與Excel交互   在這一節玩點真格的,看一看Python到底如何與Excel交互。我們使用目前最常用的openpyxl來完成操作。先看一個未處理的Excel表格。

 

 

 上面這個表格是關於營業計劃的數據,看起來很彆扭,因為不同層次的數據之間沒有縮進,也沒有背景顏色,甚至沒有表格線。如果要將這個表格交給領導,估計領導會拿起塊磚頭砸過來!   領導最希望見到下面的表格:

 這個表格看起來是不是很舒服呢!其實這個表格用Excel做起來也並不費勁。不過仍然需要N步,這裏就不詳細解釋如何用Excel來做這個表格了,現在來看如何利用Python閃電般進行格式轉換。

from openpyxl.styles import Alignment,Font,Border,Side,Color,PatternFill
import openpyxl
# 打開待轉換的文件
workbook = openpyxl.load_workbook('原始表格.xlsx')

ws = workbook.active
table = ws['A1':'D11']
# 設置字體
ft = Font(name="黑體")
for rows in table:
    for cell in rows:
        cell.font = ft
# 調整行高(所有的行的高度統一設置為18)
for i in range(1,ws.max_row + 1):
    ws.row_dimensions[i].height = 18.0
# 插入列
ws.insert_cols(1,1)

# 調整新插入列的寬度
ws.column_dimensions['A'].width = 5

# 插入行
ws.insert_rows(1,1)
# 調整新插入行的高度
ws.row_dimensions[1].height = 10


# 文字左對齊,数字右對齊
leftAlign = Alignment(horizontal='left',vertical='center')
rightAlign = Alignment(horizontal='right',vertical='center')
for row in ws.rows:
    for cell in row:
        # 数字,右對齊
        if type(cell.value) == int:
            cell.alignment = rightAlign
        else:
            cell.alignment = leftAlign

# 調整列寬
ws.column_dimensions['B'].width =20
ws.column_dimensions['C'].width =9
ws.column_dimensions['D'].width =9
ws.column_dimensions['E'].width =9
ws.column_dimensions['F'].width =4  # 讓表格線出來一點



# 為表格添加邊框

topBorder = Border(top=Side(border_style='thick',color='000000'))
                #bottom=Side(border_style='thick',color='000000'),
                #horizontal = Side(border_style='thin',color='000000'))
# 添加表格頂邊的粗線(包括最後沒有數據的列)
for col in range(2, ws.max_column + 2):
    ws.cell(2, col).border = topBorder

bottomBorder = Border(bottom=Side(border_style='thick',color='000000'))
# 添加表格底邊的粗線(包括最後沒有數據的列),這裏需要加1,是因為前面繪製表格頂邊的粗線時,最大列的數量已經多了1個
# 所以只需要加1即可
for col in range(2, ws.max_column + 1):
    ws.cell(ws.max_row, col).border = bottomBorder

# 添加水平細線
horizontalBorder = Border(top=Side(border_style='thin',color='000000'))
for col in range(2, ws.max_column + 1):
    for row in range(4,ws.max_row):
        ws.cell(row, col).border = horizontalBorder

# 單元格縮進
ws['B5'].alignment=Alignment(indent=1)
ws['B6'].alignment=Alignment(indent=1)
ws['B8'].alignment=Alignment(indent=1)
ws['B11'].alignment=Alignment(indent=1)

ws['B9'].alignment=Alignment(indent=2)
ws['B10'].alignment=Alignment(indent=2)

# 設置文字顏色
blueFont = Font(name="Arial",color = '4169E1')
for col in range(3, ws.max_column + 1):
    ws.cell(5, col).font= blueFont
    ws.cell(6, col).font = blueFont
    ws.cell(9, col).font = blueFont
    ws.cell(10, col).font = blueFont
# 設置背景色
fill = PatternFill("solid", fgColor="B0C4DE")
for col in range(2, ws.max_column + 1):
    ws.cell(4, col).fill= fill
    ws.cell(7, col).fill = fill
    ws.cell(12, col).fill = fill

workbook.save('轉換后的表格.xlsx')

print('見證奇迹的時刻')

 

現在運行程序,當輸出“見證奇迹的時刻”后,就會在當前目錄生成一個“轉換后的表格.xlsx”,該文件就是上圖的效果,是不是很神奇呢?   從這段程序中可以看出,轉換該表格需要多少步,代碼並不複雜,大家可以根據openpyxl的文檔研究。  
9. 用Python替代VBA   目前微軟官方還沒有將Python作為VBA的替代品,倒是將JavaScript作為了另外一個選擇(office.js),不過可以利用xlwings做一個折中。xlwings可以單獨使用,也可以通過xlwings office加載項提供的RunPython函數運行Python代碼。   現在有一個hello.py文件,代碼如下:  

import xlwings as xw
def hello_xlwings():
    wb = xw.Book.caller()
    wb.sheets[0].range("A1").value = "Hello xlwings!"

  在xlwings的安裝目錄有一個xlwings.xlam文件,該文件是Excel的加載項文件,也就是Excel VBA的發行包文件。現在隨便開啟一個空的Excel workbook,然後點擊“工具”>“Excel加載項”菜單項,會打開如下圖所示的對話框,找到xlwings.xlam文件,並選中該文件。

 

 然後在“開發工具”選項卡中點擊“Visual Basic”按鈕(如下圖所示),進入VBA編輯頁面。

 

 

 最後引用xlwings庫即可。

  現在可以新建一個VBA模塊,然後編寫下面的代碼:  

 Sub test()
     RunPython ("import  hello; myproject.hello_xlwings()")
 End Sub   

運行腳本,就會看到在“A1”的位置插入了Hello xlwings!

  OK,現在大家已經了解了如何使用Python與Excel交互,其他還有很多種方法,而且也不僅僅只有Python能與Excel交互,其實幾乎所有的編程語言,甚至是C語言,都有想用的Library可以與Excel交互。那就期待我下面的文章吧!    

 

 

   

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

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

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

從源碼研究如何不重啟Springboot項目實現redis配置動態切換_包裝設計

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

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

上一篇Websocket的續篇暫時還沒有動手寫,這篇算是插播吧。今天講講不重啟項目動態切換redis服務。

背景

多個項目或微服務場景下,各個項目都需要配置redis數據源。但是,每當運維搞事時(修改redis服務地址或端口),各個項目都需要進行重啟才能連接上最新的redis配置。服務一多,修改各個項目配置然後重啟項目就非常蛋疼。所以我們想要找到一個可行的解決方案,能夠不重啟項目的情況下,修改配置,動態切換redis服務。

如何實現切換redis連接

剛遇到這個問題的時候,想必如果對spring-boot-starter-data-redis不是很熟悉的人,首先想到的就是去百度一下(安慰下自己:不要重複造輪子嘛)。

可是一陣百度之後,你找到的結果可能都是這樣的:

public ValueOperations updateRedisConfig() {
    JedisConnectionFactory jedisConnectionFactory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
    jedisConnectionFactory.setDatabase(db);
    stringRedisTemplate.setConnectionFactory(jedisConnectionFactory);
    ValueOperations valueOperations = stringRedisTemplate.opsForValue();
    return ValueOperations;

沒錯,絕大多數都是切換redis db的代碼,而沒有切redis服務地址或賬號密碼的。而且天下代碼一大抄,大多數博客都是一樣的內容,這就讓人很噁心。

沒辦法,網上沒有,只能自己造輪子了。不過,從強哥這種懶人思維來說,上面的代碼既然能切庫,那是不是host、username、password也同樣可以,於是我們加入如下代碼:

public ValueOperations updateRedisConfig() {
    JedisConnectionFactory jedisConnectionFactory = (JedisConnectionFactory) stringRedisTemplate.getConnectionFactory();
    jedisConnectionFactory.setDatabase(db);
    jedisConnectionFactory.setHostName(host);
    jedisConnectionFactory.setPort(port);
    jedisConnectionFactory.setPassword(password);
    stringRedisTemplate.setConnectionFactory(jedisConnectionFactory);
    ValueOperations valueOperations = stringRedisTemplate.opsForValue();
    return valueOperations;
}

話不多說,改完重啟一下。額,運行結果並沒有讓我們見證奇迹的時刻。在調用updateRedisConfig方法的之後,使用redisTemplate還是只能切換db,不能進行服務地址或賬號密碼的更新。

這就讓人頭疼了,不過想也沒錯,如果可以的話,網上不應該找不到類似的代碼。那麼,現在該咋辦嘞?

強哥的想法是:redisTemplate每次獲取ValueOperations執行get/set方法的時候,都會去連接redis服務器,那麼我們就從這兩個方法入手看看能不能找得到解決方案。

接下來就是源碼研究的過程啦,有耐心的小夥伴就跟着強哥一起找,只想要結果的就跳到文末吧~

首先來看看入手工具方法set:

 
public boolean set(final String key, Object value) {
  boolean result = false;
  try {
          ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
          operations.set(key, value);
          result = true;
      } catch (Exception e) {
          logger.error("set cache error:", e);
      }
  return result;
}

我們進入到operations.set(key, value);的set方法實現:

public boolean set(String key, Object value) {
        boolean result = false;
    try {
        ValueOperations<Serializable, Object> operations = this.redisTemplate.opsForValue();
        operations.set(key, value);
        result = true;
    } catch (Exception var5) {
      this.logger.error("set error:", var5);
    }
    return result;
}

哦,走的是execute方法,進去看看,具體調用的是AbstractOperations的RedisTemplate的execute方法(中間跳過幾個重載方法跳轉):

public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
    Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
    Assert.notNull(action, "Callback object must not be null");
    RedisConnectionFactory factory = getConnectionFactory();
    RedisConnection conn = null;
    try {
      if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
        conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
      } else {
        conn = RedisConnectionUtils.getConnection(factory);
      }
      boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
      RedisConnection connToUse = preProcessConnection(conn, existingConnection);
      boolean pipelineStatus = connToUse.isPipelined();
      if (pipeline && !pipelineStatus) {
        connToUse.openPipeline();
      }
      RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
      T result = action.doInRedis(connToExpose);
      // close pipeline
      if (pipeline && !pipelineStatus) {
        connToUse.closePipeline();
      }
      // TODO: any other connection processing?
      return postProcessResult(result, connToUse, existingConnection);
    } finally {
      RedisConnectionUtils.releaseConnection(conn, factory);
    }
}

方法內容很長,不過大致可以看出前面是獲取一個RedisConnection對象,後面應該就是命令的執行,為什麼說應該?因為強哥也沒去細看後面的實現,因為我們要關注的就是怎麼拿到這個RedisConnection對象的。

那麼我們走RedisConnectionUtils.getConnection(factory);這句代碼進去看看,為什麼我知道是走這句而不是上面那句,因為強哥沒開事務,如果大家有打斷點,應該默認也是走的這句,跳到具體的實現方法:RedisConnectionUtils.doGetConnection(……):

public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
boolean enableTransactionSupport) {
    Assert.notNull(factory, "No RedisConnectionFactory specified");
    RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
    if (connHolder != null) {
      if (enableTransactionSupport) {
        potentiallyRegisterTransactionSynchronisation(connHolder, factory);
      }
      return connHolder.getConnection();
    }
    if (!allowCreate) {
      throw new IllegalArgumentException("No connection found and allowCreate = false");
    }
    if (log.isDebugEnabled()) {
      log.debug("Opening RedisConnection");
    }
    RedisConnection conn = factory.getConnection();
    if (bind) {
      RedisConnection connectionToBind = conn;
      if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
        connectionToBind = createConnectionProxy(conn, factory);
      }
      connHolder = new RedisConnectionHolder(connectionToBind);
      TransactionSynchronizationManager.bindResource(factory, connHolder);
      if (enableTransactionSupport) {
        potentiallyRegisterTransactionSynchronisation(connHolder, factory);
      }
      return connHolder.getConnection();
    }
    return conn;
  }

代碼還是很長,話不多說,斷點走的這句:RedisConnection conn = factory.getConnection();那就看看其實現方法吧:JedisConnectionFactory.getConnection(),這個是個關鍵方法:

public RedisConnection getConnection() {
 if (cluster != null) {
   return getClusterConnection();
 }
 Jedis jedis = fetchJedisConnector();
 JedisConnection connection = (usePool ? new JedisConnection(jedis, pool, dbIndex, clientName)
     : new JedisConnection(jedis, null, dbIndex, clientName));
 connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
 return postProcessConnection(connection);
}

看到了,代碼很短,但是我們從中可以獲取到的內容卻很多:

第一個判斷是是否有集群,這個強哥項目暫時沒用,所以不管;如果大家有用到,可能要要考慮下裏面的代碼。

Jedis對象是在這裏創建的,熟悉redis的應該都知道:Jedis是Redis官方推薦的Java連接開發工具。直接用它就能執行redis命令。

usePool 這個變量,說明我們連接的redis服務器的時候可能用到了連接池;不知道大家看到usePool會不會有種恍然醒悟的感覺,很可能就是因為我們使用了連接池,所以即使我們之前的代碼中切換了賬號密碼,連接池的連接還是沒有更新導致的處理無效。

我們先看看fetchJedisConnector方法實現:

protected Jedis fetchJedisConnector() {
  try {
    if (usePool && pool != null) {
      return pool.getResource();
    }
 
    Jedis jedis = new Jedis(getShardInfo());
  // force initialization (see Jedis issue #82)
    jedis.connect();
  
    potentiallySetClientName(jedis);
    return jedis;
  } catch (Exception ex) {
throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
  }
}

哦,可以看到,Jedis對象是根據getShardInfo()構建出來的:

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

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

public BinaryJedis(JedisShardInfo shardInfo) {
  this.client = new Client(shardInfo.getHost(), shardInfo.getPort(), shardInfo.getSsl(), shardInfo.getSslSocketFactory(), shardInfo.getSslParameters(), shardInfo.getHostnameVerifier());
  this.client.setConnectionTimeout(shardInfo.getConnectionTimeout());
  this.client.setSoTimeout(shardInfo.getSoTimeout());
  this.client.setPassword(shardInfo.getPassword());
  this.client.setDb((long)shardInfo.getDb());
}

那就是說,只要我們掌握了這個JedisShardInfo的由來,我們就可以實現redis相關配置的切換。而這個getShardInfo()方法就是返回了JedisConnetcionFactory類的JedisShardInfo shardInfo屬性:

public JedisShardInfo getShardInfo() {
  return shardInfo;
}

那麼如果我們知道了這個shardInfo是如何創建的,是不是就可以干預到RedisConnect的創建了呢?我們來找找它被創建的地方:

走的JedisConnectionFactory的afterPropertiesSet()進去看看:

/*
  * (non-Javadoc)
  * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
  */
public void afterPropertiesSet() {
 if (shardInfo == null) {
   shardInfo = new JedisShardInfo(hostName, port);
   if (StringUtils.hasLength(password)) {
     shardInfo.setPassword(password);
   }  
   if (timeout > 0) {
       setTimeoutOn(shardInfo, timeout);
     }
   }

   if (usePool && clusterConfig == null) {
     this.pool = createPool();
   }
 
   if (clusterConfig != null) {
     this.cluster = createCluster();
   }
}

哦吼~,整篇博文最關鍵的代碼終於出現了。我們可以看到,JedisShardInfo的所有信息都是從JedisConnetionFactory的屬性中來的,包括hostName、port、password、timeout等。而且,如果JedisShardInfo為null時,調用afterPropertiesSet方法會幫我們創建出來。然後,該方法還會幫我們創建新的連接池,簡直完美。最最重要的是,這個方法是public的。

所以,嘿嘿,綜上,我們總結改造的幾個點:

1.連接redis用到了連接池,需要先給他銷毀;

2.創建Jedis的時候,將JedisShardInfo先設為null;

3.手動設置JedisConnetionFactory的hostName、port、password等信息;

4.調用JedisConnetionFactory的afterPropertiesSet方法創建JedisShardInfo;

5.給RedisTemplate設置處理后的JedisConnetionFactory,這樣在下次使用set或get方法的時候就會去創建新改配置的連接池啦。

實現如下:

public void updateRedisConfig() {
  RedisTemplate template = (RedisTemplate) applicationContext.getBean("redisTemplate");
  JedisConnectionFactory redisConnectionFactory = (JedisConnectionFactory) template.getConnectionFactory();
//關閉連接池
  redisConnectionFactory.destroy();
  redisConnectionFactory.setShardInfo(null);
  redisConnectionFactory.setHostName(host);
  redisConnectionFactory.setPort(port);
  redisConnectionFactory.setPassword(password);
  redisConnectionFactory.setDatabase(database);
  //重新創建連接池
  redisConnectionFactory.afterPropertiesSet();
  template.setConnectionFactory(redisConnectionFactory);
}

重啟項目之後,調用這個方法,就可以實現redis庫及服務地址、賬號密碼的切換而無需重啟項目了。

如何實現動態切換

強哥這裏就使用同一配置中心Apollo來進行動態配置的。

首先不懂Apollo是什麼的同學,先Apollo官網半日游吧(直接看官網教程,比看其他博客強)。簡單的說就是一個統一配置中心,將原來配置在項目本地的配置(如:Spring中的application.properties)遷移到Apollo上,實現統一的管理。

使用Apollo的原因,其實就是因為其接入簡單,且具有實時更新回調的功能,我們可以監聽Apollo上的配置修改,實現針對修改的配置內容進行相應的回調監聽處理。

因此我們可以將redis的配置信息配置在Apollo上,然後監聽這些配置。當Apollo上的這些配置修改時,我們在ConfigChangeListener中,調用上面的updateRedisConfig方法就可以實現redis配置的動態切換了。

接入Apollo代碼非常簡單:

Config redisConfig = ConfigService.getConfig("redis");
ConfigChangeListener listener = this::updateRedisConfig;
redisConfig.addChangeListener(listener);

這樣,我們就可以實現具體所謂的動態更新配置啦~

當然,其他有相同功能的配置中心其實也可以,只是強哥項目中暫時用的就是Apollo就拿Apollo來講了。

考慮到篇幅已經很長了,就不多解釋Apollo的使用了,用過的自然看得懂上面的方法,有不懂的也可以留言提問哦。

好了,就到這吧,原創不易,怎麼支持你們知道,那麼下次見啦

關注公眾號獲取更多內容,有問題也可在公眾號提問哦:

強哥叨逼叨

叨逼叨編程、互聯網的見解和新鮮事

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

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

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

OAuth2.0系列之基本概念和運作流程(一)_台中搬家

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

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

@

目錄

  • 一、OAuth2.0是什麼?
    • 1.1 OAuth2.0簡介
    • 1.2 OAuth2.0官方文檔
  • 二、OAuth2.0原理
    • 2.1 OAuth2.0流程圖
  • 三、 OAuth2.0的角色
  • 四、OAuth2.0授權模式
    • 4.1 授權碼模式(authorization code)
    • 4.2 簡化模式(implicit grant type)
    • 4.3 密碼模式(resource owner password credentials)
    • 4.4 客戶端模式(client credentials)
  • 五、優質參考資料

一、OAuth2.0是什麼?

1.1 OAuth2.0簡介

OAuth 2.0是目前最流行的授權機制,用來授權第三方應用

OAuth是一種開放協議, 允許用戶讓第三方應用以安全且標準的方式獲取該用戶在某一網站,移動或者桌面應用上存儲的秘密的資源(如用戶個人信息,照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。

OAuth 1.0協議(RFC5849)作為一個指導性文檔發布,是一個小社區的工作成果。
本標準化規範在OAuth 1.0的部署經驗之上構建,也包括其他使用案例以及從更廣泛的IETF社區收集到的可擴展性需求。

OAuth 2.0協議不向後兼容OAuth 1.0。這兩個版本可以在網絡上共存,實現者可以選擇同時支持他們。

1.2 OAuth2.0官方文檔

官網:https://oauth.net/2/ ,官網只有英文版文檔,您也可以參考翻譯過來的文檔,鏈接:OAuth2 RFC6749中文翻譯

OAuth2.0在安全性方面做了比較大的提高,簡單來說OAuth2.0就是一種授權協議,可以用來授權,隨意點個網站,如圖這種網站,用戶不想註冊,就可以用微信、支付寶登錄,就是場景是很常見的,也是OAuth2.0的應用

二、OAuth2.0原理

OAuth2.0是一種授權機制,正常情況,不使用OAuth2.0等授權機制的系統,客戶端是可以直接訪問資源服務器的資源的,為了用戶安全訪問數據,在訪問中間添加了Access Token機制。客戶端需要攜帶Access Token去訪問受到保護的資源。所以OAuth2.0確保了資源不被惡意客戶端訪問,從而提高了系統的安全性。

2.1 OAuth2.0流程圖

引用官方圖片介紹OAuth2.0總體流程:

  • (A)客戶端向從資源所有者請求授權。
  • (B)客戶端收到授權許可,資源所有者給客戶端頒發授權許可(比如授權碼code)
  • (C)客戶端與授權服務器進行身份認證並出示授權許可(比如授權碼code)請求訪問令牌。
  • (D)授權服務器驗證客戶端身份並驗證授權許可,若有效則頒發訪問令牌(accept token)。
  • (E)客戶端從資源服務器請求受保護資源並出示訪問令牌(accept token)進行身份驗證。
  • (F)資源服務器驗證訪問令牌(accept token),若有效則滿足該請求。

三、 OAuth2.0的角色

OAuth2.0定義如下角色:

引用OAuth2 RFC6749中文翻譯:

  • 資源所有者(Resource Owner): 能夠許可受保護資源訪問權限的實體。當資源所有者是個人時,它作為最終用戶被提及。
  • 用戶代理(User Agent): 指的的資源擁有者授權的一些渠道。一般指的是瀏覽器、APP
  • 客戶端(Client) 使用資源所有者的授權代表資源所有者發起對受保護資源的請求的應用程序。術語“客戶端”並非特指任何特定的的實現特點(例如:應用程序是否在服務器、台式機或其他設備上執行)。
  • 授權服務器(Authorization Server): 在成功驗證資源所有者且獲得授權后頒發訪問令牌給客戶端的服務器。
    授權服務器和資源服務器之間的交互超出了本規範的範圍。授權服務器可以和資源服務器是同一台服務器,也可以是分離的個體。一個授權服務器可以頒發被多個資源服務器接受的訪問令牌。
  • 資源服務器(Resource Server): 託管受保護資源的服務器,能夠接收和響應使用訪問令牌對受保護資源的請求。

四、OAuth2.0授權模式

OAuth2.0有4種授權模式:

  • 授權碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)

其中最常用的是授權碼模式,4種授權模式的詳細介紹可以參考阮一峰老師的:OAuth 2.0 的四種方式

4.1 授權碼模式(authorization code)

授權碼(authorization code)方式,指的是第三方應用先申請一個授權碼,然後再用該碼獲取令牌。

官網圖片:

從調接口方面,簡單來說:

  • 第一步:獲取code:
    eg:oauthServer+”/oauth/authorize?client_id=”+clientId+”&response_type=code&redirect_uri=”+redirectUrl+”&scope=all”
    如果沒有登錄,則會跳轉到統一身份認證登錄頁面。如果用戶登錄了,調用接口后,會重定向到redirect_uri,授權碼會作為它的參數

  • 第二步:獲取access_token
    eg:oauthServer+”/oauth/token?code=”+code+”&grant_type=authorization_code&client_secret=”+clientSecret+”&redirect_uri=”+redirectUri+”&client_id=”+clientId

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

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

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1MzQ5NzMsInVzZXJfbmFtZSI6Im5pY2t5IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJmMjM0M2Q0NC1hODViLTQyOGYtOWE1ZS1iNTE4NTAwNTM5ODgiLCJjbGllbnRfaWQiOiJvYSIsInNjb3BlIjpbImFsbCJdfQ.LWkN2gC2dBrGTn5uSPzfdW6yRj7jhlX87EE8scY02hI",
    "token_type": "bearer",
    "expires_in": 59,
    "scope": "all",
    "user_name": "nicky",
    "jti": "f2343d44-a85b-428f-9a5e-b51850053988"
}
  • 第三步:訪問系統資源,此時統一認證服務會根據該認證客戶端權限信息判斷,決定是否返回信息。

訪問:http://localhost:8084/api/userinfo?access_token=${accept_token}

4.2 簡化模式(implicit grant type)

簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了”授權碼”這個步驟,因此稱簡化模式

從調接口方面,簡單來說:

  • 第一步:訪問授權,要傳client_id:客戶端id,redirect_uri:重定向uri,response_type為token,scope是授權範圍,state是其它自定義參數

http://localhost:8888/oauth/authorize?client_id=cms&redirect_uri=http://localhost:8084/callback&response_type=token&scope=read&state=123

  • 第二步:授權通過,會重定向到redirect_uri,access_token碼會作為它的參數

http://localhost:8084/callback#access_token=${accept_token}&token_type=bearer&state=123&expires_in=120

  • 第三步:拿到acceptToken之後,就可以直接訪問資源

http://localhost:8084/api/userinfo?access_token=${accept_token}

4.3 密碼模式(resource owner password credentials)

密碼模式中,用戶向客戶端提供自己的用戶名和密碼,這通常用在用戶對客戶端高度信任的情況

從調接口方面,簡單來說:

  • 第一步:直接傳username,password獲取token

http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&username=admin&password=123456&grant_type=password&scope=all

  • 第二步:拿到acceptToken之後,就可以直接訪問資源

http://localhost:8084/api/userinfo?access_token=${accept_token}

4.4 客戶端模式(client credentials)

客戶端模式(client credentials)適用於沒有前端的命令行應用,即在命令行下請求令牌

從調接口方面,簡單來說:

  • 第一步: 獲取token
    http://localhost:8888/oauth/token?client_id=cms&client_secret=123&grant_type=client_credentials&scope=all

  • 第二步:拿到acceptToken之後,就可以直接訪問資源

http://localhost:8084/api/userinfo?access_token=${accept_token}

學習必要的理論知識后,還是實踐一下才能明白整個流程,詳情參考我OAuth2.0系列博客專欄:OAuth2.0系列博客,SpringBoot的參考我系列博客專欄:SpringBoot系列博客

五、優質參考資料

  • OAuth 2.0 的四種方式
  • OAuth2.0系列博客專欄
  • OAuth2.0 JWT 認證授權
  • RFC6749官方參考文檔

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

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

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

關於C#委託三種調用的分享_台中搬家公司

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

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

一、同步調用

1、同步調用會按照代碼順序來執行
2、同步調用會阻塞線程,如果是要調用一項繁重的工作(如大量IO操作),可能會讓程序停頓很長時間,造成糟糕的用戶體驗,這時候異步調用就很有必要了。

舉個栗子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;

namespace Test
{
    public delegate int AddHandler(int a, int b);
    public class Calc
    {
        public static int Add(int a, int b)
        {
            Console.WriteLine("開始計算:" + a + "+" + b);
            Thread.Sleep(3000); //模擬該方法運行三秒
            Console.WriteLine("計算完成!");
            return a + b;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
              Console.WriteLine("===== 同步調用 SyncInvokeTest =====");
            AddHandler handler = new AddHandler(Calc.Add);
            int result = handler.Invoke(1, 2);
            Console.WriteLine("繼續做別的事情。。。");
            Console.WriteLine(result);
            Console.ReadKey();
        }    
    }
}

* 問:為什麼Invoke的參數和返回值和AddHandler委託是一樣的呢?
* 答:Invoke方法的參數很簡單,一個委託,一個參數表(可選),
 而Invoke方法的主要功能就是幫助你在UI線程上調用委託所指定的方法。
Invoke方法首先檢查發出調用的線程(即當前線程)是不是UI線程,
 如果是,直接執行委託指向的方法,如果不是,它將切換到UI線程,
 然後執行委託指向的方法。不管當前線程是不是UI線程,
 Invoke都阻塞直到委託指向的方法執行完畢,然後切換回發出調用的
 線程(如果需要的話),返回。
 所以Invoke方法的參數和返回值和調用他的委託應該是一致的。

二、異步調用

1、異步調用不阻塞線程,而是把調用塞到線程池中,
2、程序主線程或UI線程可以繼續執行。
3、委託的異步調用通過BeginInvoke和EndInvoke來實現。

舉個栗子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;

namespace Test
{
    public delegate int AddHandler(int a, int b);
    public class Calc
    {
        public static int Add(int a, int b)
        {
            Console.WriteLine("開始計算:" + a + "+" + b);
            Thread.Sleep(3000); //模擬該方法運行三秒
            Console.WriteLine("計算完成!");
            return a + b;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("===== 異步調用 AsyncInvokeTest =====");
            AddHandler handler1 = new AddHandler(Calc.Add);
            //IAsyncResult: 異步操作接口(interface)
            //BeginInvoke: 委託(delegate)的一個異步方法的開始
            IAsyncResult result1 = handler1.BeginInvoke(1, 2, null, null);
 
            Console.WriteLine("繼續做別的事情1。。。");
            //異步操作返回
            Console.WriteLine(handler1.EndInvoke(result1));//會等待加法類計算,如果沒計算好就堵塞線程
            Console.WriteLine("繼續做別的事情2。。。");
            Console.ReadKey();
        }    
    }
}

注意:
* BeginInvoke : 開始一個異步的請求,調用線程池中一個線程來執行,
* 返回IAsyncResult 對象(異步的核心). IAsyncResult 簡單的說,
* 它存儲異步操作的狀態信息的一個接口,也可以用他來結束當前異步。
* 注意: BeginInvoke和EndInvoke必須成對調用.即使不需要返回值,
* 但EndInvoke還是必須調用,否則可能會造成內存泄漏。

結果:

可以看到,主線程並沒有等待,而是直接向下運行了。
但是問題依然存在,當主線程運行到EndInvoke時,如果這時調用沒有結束(這種情況很可能出現),這時為了等待調用結果,線程依舊會被阻塞。

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

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

三、異步回調

用回調函數,當調用結束時會自動調用回調函數,解決了為等待調用結果,而讓線程依舊被阻塞的局面。

 舉個栗子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;

namespace Test
{
    public delegate int AddHandler(int a, int b);
    public class Calc
    {
        public static int Add(int a, int b)
        {
            Console.WriteLine("開始計算:" + a + "+" + b);
            Thread.Sleep(3000); //模擬該方法運行三秒
            Console.WriteLine("計算完成!");
            return a + b;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
         Console.WriteLine("===== 異步回調 AsyncInvokeTest =====");
            AddHandler handler2 = new AddHandler(Calc.Add);
            //異步操作接口(注意BeginInvoke方法的不同!)
            IAsyncResult result2 = handler2.BeginInvoke(1, 2, new AsyncCallback(MyCallBack),
                "AsycState:OK");
            Console.WriteLine("繼續做別的事情。。。");
            Console.ReadKey();
        }    
        static void MyCallBack(IAsyncResult result)
        {
            //result 是“加法類.Add()方法”的返回值
            //AsyncResult 是IAsyncResult接口的一個實現類,空間:System.Runtime.Remoting.Messaging
            //AsyncDelegate 屬性可以強制轉換為用戶定義的委託的實際類。
            AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate;
            Console.WriteLine(handler.EndInvoke(result));
            Console.WriteLine(result.AsyncState);
        }      
    }
}

委託的類型為AddHandler,則為了訪問 AddHandler.EndInvoke,

result 是“加法calc.Add()方法”的返回值

AsyncResult 是IAsyncResult接口的一個實現類,空間:System.Runtime.Remoting.Messaging

必須將異步委託強制轉換為 AddHandler。可以在異步回調函數(類型為 AsyncCallback)中調用 AddHandler.EndInvoke,以獲取最初提交的 AddHandler.BeginInvoke 的結果。

 

ok,三種委託調用的分享就到這裏了,有疑問的歡迎指正!

 

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

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

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

DIVOOM Pixoo LED發光像素單肩包:可自己換圖案、文字,絕對獨一無二!_網頁設計公司

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

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

這次為大家介紹的是 DIVOOM Pixoo LED發光像素單肩包開箱,不知道大家還記不記得去年我們曾經介紹過一款「Pixoo像素發光電腦背包」?當時開箱介紹完之後,聽說就立刻賣光光,到前陣子都還有讀者在詢問哪裡還有貨。限量就是這麼的殘酷,現在經過一年後原團隊再次推出了一款體積較小,使用情境更多,攜帶更方便的隨身單肩包,這次一樣數量有限,先來看阿達簡單的開箱介紹。

DIVOOM Pixoo LED發光像素單肩包開箱

這次由 DIVOOM推出的 Pixoo 系列 LED 單肩包外盒在設計上看起來蠻時尚的:

開箱裡面有一個攜行袋,掏出後就有 Pixoo LED發光像素單肩包與說明書:

Pixoo LED發光像素單肩包外型跟一般的單肩包無異,不過別看它體積不大,前、中、後都有收納空間,加上暗袋的設計能裝的東西不少:

一開始讓我注意到的是磁吸快拆肩帶,真的好用且牢固:

先看最前面,裡面可放行動電源與各種物品:

LED發光像素單肩包耗電量不大,官方實測搭配10000mAh的行動電源可連續跑馬顯示一天以上:

中間區域的空間容量最大,不論是 iPad mini、微單相機、Switch放裡面都沒問題,還有網格袋可收納零錢等物品:

另外還有一組鑰匙扣可別上鑰匙、悠遊卡、門禁卡之類常忘記帶出門的物品:

後層可當防盜層,因為緊貼身體比較不會被扒手覬覦,可放錢包、證件之類的物品:

在LED發光像素單肩包後方還有提把設計,當手包也沒問題:

下方的約束帶可鬆開增加單肩包的內容量,也可放置腳架、三軸之類的物品:

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

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

中間還有暗袋設計,可放雨傘、小水壺之類的物品,真的蠻能裝的:

LED發光像素單肩包這邊買

 

LED發光像素單肩包面板切換

LED發光像素單肩包最重要的功能之一就是在它的正面有一個16×16的LED面板,只要下載 DIVOOM APP 就可以將自己設計的圖片文字傳到包包上(當然要有點藝術天分):

也可以傳輸文字到單肩包上,如果您在搭捷運時身體不適想座博愛座的話,就可以寫上「我身體不舒服」、「我腳痛」、「我是孕婦」之類的文字,正義魔人應該就比較不會來廢話(除非他不識字啦):

除了文字跑馬以外,還有音樂遊戲、俄羅斯方塊…等小遊戲可以遊玩,這時候就用手機當手把,LED發光像素單肩包當作螢幕玩,也是相當有趣的小功能:

DIVOOM Pixoo LED發光像素單肩包除了當作一般的側背包以外,也可以當胸包、腰包、提包,搭配您自己無限的創意(沒創意的話APP裡面有一堆免費圖案可用),絕對讓你在外走跳成為眾人目光焦點,也不會有撞包的問題,白天晚上都相當清楚:

騎車還有提醒後方的功能喔:

LED發光像素單肩包本身也具備防潑水功能,下雨也不怕:

我們也拍攝了開箱影片給大家參考,個人覺得相當多功能的小型單肩包,阿達也舉辦抽獎活動,想要得到的話到 YouTube影片下方留言,2/28號抽出:

 

LED發光像素單肩包這邊買

現在 DIVOOM Pixoo LED發光像素單肩包正在壞朋友募資平台上舉行預購,價格我個人也覺得很合理,外面不少沒有特殊功能的單肩包就賣快要兩千元,這一款除了會發光、文字跑馬、秀動畫圖片以外,用料也實在,個人蠻推薦的,也提供給大家參考喔。

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

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

Google Store 推出新年優惠,Pixel 手機、Chromecast、Nest 智慧喇叭直接現折_如何寫文案

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

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

隨著新年即將來臨,Google Store 近日也宣布推出新年優惠活動,Pixel 手機、Chromecast、Nest 智慧喇叭系列都能祭出折扣,而且這次是直接現折,讓你立刻就能享受優惠價。此外,Pixel 4a 5G 版本的白色款式也已悄悄降臨 Google Store(不過沒有折扣)。

Google Store 推出新年優惠

這次符合 Google Store 新年優惠的 Pixel 手機,只有 Pixel 4a 5G 版本(純粹黑),原價 NT$15,990,現在入手即可省下 NT$1,000,意味著價格變成 NT$14,990。這次也是首次 Pixel 4a 5G 推出現折優惠的活動(之前送折價卷)。就是白因為才剛推出,理所當然還是以原價販售:

Pixel 5 這次就不在名單內,目前依舊以 NT$18,990 的原價販售。而 Nest 智慧喇叭「Nest Mini」與「Nest Audio」兩款都有優惠。

Nest Mini 原價為 NT$1,785,現在現省 NT$400,變成 NT$1,385 的優惠價,兩色都是:

Nest Audio 原價為 NT$3,180,現在現省 NT$350,折扣後價格變成 NT$2,830:

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

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

Chromecast 折扣 NT$150,原價 NT$1,445,現在購買可以 NT$1,295 的優惠價入手:

整體來看,這次 Google Store 的優惠雖然沒有像先前聖誕節、雙11 那麼超值,不過跟平常相比至少有降價,對於有意購買上述產品的人來說,還是可以省下一些荷包,剛好拿來補貼紅包錢。

Google Store 新年優惠預計到 2021 年 2 月 14 日晚上 11:59 點截止。

補充資料

  • 台灣 Google Store:點我前往

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

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

OPPO Find X3 Pro 更多關鍵規格曝光:將搭載高通 S888 處理器、12GB RAM、256GB ROM_網頁設計公司

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

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

幾週前,爆料大神 Evan Blass 才洩漏 OPPO Find X3 Pro 的官方渲染圖,不過至今關於 OPPO 準備推出 Find X 系列的 Find X3 和 Find X3 Pro 除了後者幾乎確認搭載高通 S888 旗艦處理器,其餘所知的規格其實相當有限。不過稍早網路流出有關 Find X3 Pro 的規格和多個測試平台的截圖畫面,也透露更多 Find X3 Pro 的關鍵規格,其中包括螢幕解析度等資訊。

▲圖片來源:Evan Blass(Voice/@evleaks)

OPPO Find X3 Pro 更多關鍵規格曝光:將搭載高通 S888 處理器、12GB RAM、256GB ROM

▲圖片來源:Evan Blass(Voice/@evleaks)

稍早在酷安網出現多張疑似 OPPO Find X3 Pro 的規格截圖畫面,其型號 PEEM00 與過去的傳聞相符:

▲圖片來源:酷安網

根據手機的截圖顯示, Find X3 Pro 的螢幕解析度為 1440×3216 ,據傳螢幕尺寸為 6.67 吋並將支持自適應調整的 120Hz 更新率(支援 10Hz – 120Hz 可變螢幕更新率)。而 Find X3 系列也將首搭載 OPPO 「全鏈路色彩管理系統」,這套基於硬體所開發的色彩管理系統,該色彩管理系統擁有兩大核心技術,全鏈路 10-bit 及色彩管理,能夠大幅度提升色彩採集的豐富度,在顯示環節則能讓畫質更加清晰且更精準的還原顯示圖像色彩。

硬體規格方面, Find X3 Pro 除了確認搭載 Qualcomm Snapdragon 888 處理器,將具備 12GB RAM 和 256GB ROM ,從之前曝光的安兔兔跑分成績 Find X3 Pro 的成績測得 771,491 分的成績。作業系統方面,將運行 Android 11 並搭配 Color OS 11.2 使用者介面。

▲圖片來源:酷安網

▲圖片來源:酷安網

同時在 Geekbench 資料庫也出現相同型號 PEEM00 的裝置,其單核和多核測試分別獲得 4236 分和 13323 分。不過根據以往經驗 Geekbench 資料庫的內容僅可做為參考,無法確認是真實的規格。

▲圖片來源:Geekbench

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

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

回顧去年 11 月,爆料大神  Evan Blass 首次提及關於將搭載高通 Snapdragon 888 處理器的 OPPO Find X3 Pro(代號:Fussi),當時提及 Find X3 Pro 相機將配備 5000 萬像素四鏡頭主相機,其中標準鏡頭和超廣角鏡頭都將採用 SONY 全新的感光元件(其中包括 SONY IMX766 感光元件),此外也具備 1300 萬像素 2x 望遠鏡頭以及 300 萬像素微距鏡頭。

延伸閱讀:
ROG Phone 5 更多外觀、規格細節曝光:另有 Anime Matrix 顯示版本?

OPPO Find X3 Lite 完整實機開箱照洩漏!只是外觀有點眼熟?

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

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