Google Code Prettify

顯示具有 spring data 標籤的文章。 顯示所有文章
顯示具有 spring data 標籤的文章。 顯示所有文章

2018年7月23日 星期一

Spring Data Redis

原文在「Introduction to Spring Data Redis」,我只是節錄並加上自己的心得,詳細仍要看原文。我的 Redis 不是安裝在本機,是安裝在另一台電腦 (CentOS 7) 上,Redis 預設是不提供遠端連線,所以要先修改 redis.conf 裡的設定,要修改的有兩個 - bindprotected-mode

bind 127.0.0.1 前加上 # 變註解就行了,也就是不限制那個 IP 都可以連進來。
protected-mode 改為 no 取消保護模式。
  • build.gradle
dependencies {
  compile('org.springframework.boot:spring-boot-starter-data-redis')
  compile('org.springframework.boot:spring-boot-starter-web')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
 
  compile group: 'redis.clients', name: 'jedis', version: '2.9.0'
}
紅色是這次的主題需要用到的。
  • JedisConfig.java
@Configuration
public class JedisConfig {
  @Bean
  public RedisTemplate<String, Object> redisTemplate() {
     RedisTemplate<String, Object> template = new RedisTemplate<>();
     template.setConnectionFactory(jedisConnectionFactory());
     
     return template;
  }
 
  @Bean
  public JedisConnectionFactory jedisConnectionFactory() {
     JedisConnectionFactory jedisConFactory = new JedisConnectionFactory();
     jedisConFactory.setHostName("192.168.70.233");
     jedisConFactory.setPort(6379);
     return jedisConFactory;
  }
}
定義這兩個 Bean,後續會用到的 CrudRepository 即是透過這它們連上 Redis 的。
  • Student.java
@RedisHash("Student")
@Data
public class Student implements Serializable {
   
    public enum Gender { 
        MALE, FEMALE
    }
 
    private String id;
    private String name;
    private Gender gender;
    private int grade;

    public Student() { }
    
    public Student(String id, String name, Gender gender, int grade) {
      this.id = id;
      this.name = name;
      this.gender = gender;
      this.grade = grade;
    }
}
這是後面我們要存到 Redis 上的物件,是屬於 Redis 中的 Hash 型別物件。
  • StudentRepository.java
@Repository
public interface StudentRepository extends CrudRepository<Student, String> {
 
}
熟悉 spring data 的人對 CrueRepository 一定不陌生,看來它不只能用在關聯式資料庫,也能用在 NoSQL 的資料庫。
  • DemoTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoTest {
 
  @Autowired
  private StudentRepository studentRepository;

  @Test
  public void test() {
    Student student = new Student("Eng2015001", "John Doe", Student.Gender.MALE, 1);
    studentRepository.save(student);
  
    Student retrievedStudent = studentRepository.findById("Eng2015001").get();
    System.out.println(retrievedStudent.toString());
  }
}

簡單的單元測試程式,將 id 為 Eng2015001 的 Student 物件放入 Redis,接著我們可以用「Redis Desktop Manager」這個小工具連到 Redis 後看到內容,如下。


2017年1月28日 星期六

spring data: PagingAndSortingRepository

在「spring data: CrudRepository 自動生成」有說明過 spring data 的 CrudRepository,提供了許多實用的預設 method,方便程式員進行資料庫的 CRUD 操作,這篇說明另一個好用的介面 PagingAndSortingRepository,只要宣告一個繼承這個介面的介面,即可以輕鬆擁有查詢翻頁的功能,也就是在網頁中,如果查詢結果很多會分成很多頁面時,可以使用這個介面提供的預設 method,它會傳回指定頁次的結果。
假設網站管理者要查詢網站中的使用者,並進行管理,就可以用到這個介面。
public interface UsersDAO extends PagingAndSortingRepository<Users, String> {
    public List<Users> findByEmail(String email);
    //public Page<Users> findAll(Pageable p);
}
上面的 UserDAO 繼承了 PagingAndSortingRepository 介面,於是我們不需要寫任何程式,就可以使用 findAll 等預設的介面,上面 findAll 只是我寫出來讓大家知道 spring data 中是怎麼宣告,實際上是不需要寫出來,測試程式如下:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = { WebInitializer.class, RootConfig.class, JpaConfig.class, MvcConfig.class })
@ActiveProfiles("TEST")
public class UserDAOTest {
    @Autowired
    private UsersDAO dao;
    
    @Transactional
    @Test
    public void testFindAll() {
        Pageable p = new PageRequest(0, 5);
        Page<Users> all = dao.findAll(p);
        all.forEach(u -> System.out.println(u.toString()));
    }
} 
看一下紅色部份,dao 直接可以呼叫 findAll,參數傳入的是指定資料結果每 5 筆資料一頁,要傳回的是第 1 頁 (0 代表第一頁、1 代表第二頁…)。至於前面的 @WebAppConfiguration,這只是因為這個程式是 spring MVC 程式,所以在初始化時,會帶入一些 web 相關的物件,所以要這麼寫才不會有錯,如果不是 web 程式,就不需要這個 annotation。另外一提的是,不管 CrudRepository 或 PagingAndSortingRepository,spring data 都提供了預設 method 以外的一個規範,只要規循該規範定義 method,spring data 就可以自動產生相關的程式,例如上面的 findByEmail,這個 method 就可以用來搜尋特定 email 的使用者,spring data 還可以根據我們宣告的傳回值判斷傳回的是一個或多個結果,測試程式如下:
    @Transactional
    @Test
    public void testFindByEmail() {
        Users user = dao.findByEmail("hi.steven@gmail.com");
        assertNotNull(user);
        System.out.println(user.toString());
    }
spring data 提供的規範有那些? 可以參考「Professional Java for Web Applications - Featuring WebSockets, Spring Framework, JPA Hibernate and Spring Security」第 641 頁,內容如下:
Is and Equals are implied in the absence of other keywords, but you may explicitly use them. For example, findByIsbn is equivalent to findByIsbnIs and findByIsbnEquals.
Not and IsNot negate any other keyword except for Or and And. Is and Equals are still implied in the absence of other keywords, so findByIsbnIsNot is equivalent to findByIsbnIsNotEqual.
After and IsAfter indicate that the property is a date and/or time that should come after the given value, whereas Before and IsBefore indicate that the property should come
before the given value. Example: findByDateFoundedIsAfter(Date date).
Containing, IsContaining, and Contains indicate that the property’s value may start and end with anything but should contain the given value. This is similar to StartingWith, IsStartingWith, and StartsWith, which indicate that the property should start with the specified value. Likewise, EndingWith, IsEndingWith, and EndsWith indicate that the property should end with the specified value.
Example:
findByTitleContains(String value) is equivalent to the SQL criteria WHERE title = '%value%'.
Like is similar to Contains, StartsWith, and EndsWith, except that value you provide should already contain the appropriate wildcards (instead of Spring Data adding them for you). This gives you the flexibility to specify more advanced patterns. NotLike simply negates Like. Example: findByTitleLike(String value) could be called with value "%Catcher%Rye%" and would match “The Catcher in the Rye” and “Catcher Brings Home Rye Bread.”
Between and IsBetween indicate that the property should be between the two specified values. This means that you must provide two parameters for this property criterion. You can use Between on any type that may be compared mathematically in this manner, such as numeric and date types. Example: findByDateFoundedBetween(Date start, Date end).
Exists indicates that something should exist. Its meaning may vary wildly between storage mediums. It is roughly equivalent to the EXISTS keyword in JPQL and SQL.
True and IsTrue indicate that the named property should be true, whereas False and IsFalse indicate that the named property should be false. These keywords do not require method parameters because the value is implied by the keywords themselves. Example: findByApprovedIsFalse().
GreaterThan and IsGreaterThan indicate that the property is greater than the parameter value. You can include the parameter value in the bounds with GreaterThanEqual or IsGreaterThanEqual. The inverse of these keywords are LessThan, IsLessThan, LessThanEqual, and IsLessThanEqual.
In indicates that the property value must be equal to at least one of the values specified. The parameter matching this criterion should be an Iterable of the same type of the property.
Example: findByAuthorIn(Iterable<String> authors).
Null and IsNull indicate that the value should be null. These keywords also do not require method parameters because the value is implied.
Near, IsNear, Within, and IsWithin are keywords useful for certain NoSQL database types but have no useful meaning in JPA.
Regex, MatchesRegex, and Matches indicate that the property value should match the String regular expression (do not use Pattern) specified in the corresponding method parameter.






【美影 - 關鍵少數】
這部電影是由真人真事改編,時代是1960年,當時美國還是個種族歧視、性別歧視非常嚴重的國家,許多學校不但不准黑人入學,也不准女性入學,在 NASA 卻有一群優秀的黑人女性科學家,在雙重壓迫下,為美國的太空競賽默默的作出重大的貢獻。

劇中的科學家白人全部是男性,黑人全部是女性,這是一個很怪的狀況,但是電影中沒有解釋原因,因為在種族、性別的雙重歧視下,白人女性、黑人男性應該更有機會進入 NASA,為什麼偏偏是最被歧視的黑人女性成為裡面的關鍵少數?
撇開上面的疑惑不談,電影中相當精典的橋段,如上圖,主角在黑板上寫下她的發現。在電腦進入 NASA 協助運算前,NASA 竟然養了一大群數學高手充當人肉計算機,這些人除了頭腦要好,看來體力也要不錯,不然沒能力在梯子上爬上爬下,恐怕也沒有表現的機會。

2017年1月11日 星期三

JPA + Spring: 查詢結果為非實體 table (複合型別)


上圖兩個 table 是一對多的關係,要查詢出飯局的地點在那一個餐廳,及餐廳的相關資料,可以用之前介紹過的一對多方式 …
這篇要說明的是另一種方式,這個方式在三個或三個以上 table join 時,特別有用。假設我們要傳回的欄位如下:
 1 @Entity
 2 @Data
 3 public class AppointmentDetail {
 4     private String email; //電子郵件
 5     private String name; //飯局名稱
 6     private String restaurantName; //餐廳名稱
 7     private String restaurantAddr; //餐廳地址
 8     private String description; //飯局說明
 9     private int people; //參與人數
10     private int willPay; //消費額
11     private Date htime; //舉辦時間
12     
13     @Id
14     private Long sysId; //序號
15 }
如上的類別 AppointmentDetail 包含了兩個 table 的欄位,這個類別一樣要用 @Entity 標示,且也要用 @Id 標示可成為唯一的欄位,特別說明的是第 2 行的 @Data,這和 JPA 或 Spring 無關,這是 Lombok 提供的功能,用來簡化程式設計,在這邊加上 @Data 就可以為類別裡所有的欄位 (field) 加上 getter、setter method,這解決了程式設計人員老是在 POJO 中廢時寫 getter、setter 的困擾。

接著在 DAO 中增加一個 method 如下,寫法沒什麼特別,只是傳回結果擁有兩個 table 的欄位。
1     public AppointmentDetail find(Long sysId) {
2         Query query = manager.createNativeQuery(
3                 "  select a.email, a.name, r.name restaurantName, r.address restaurantAddr, a.description, a.people, a.willPay, a.htime, a.sysId "
4                 + "from appointment a inner join restaurant r on a.restaurant_sysid = r.sysId where a.sysId = :sysId "
5                 , AppointmentDetail.class);
6         query.setParameter("sysId", sysId);
7         return (AppointmentDetail) query.getSingleResult();
8     }
測試程式如下,當然也和之前寫過的使用 DAO 的方式沒什麼不一樣。
1     @Transactional
2     @Test
3     public void testFindDetail() {
4         AppointmentDetail detail = daoApp.find(12L);
5         assertNotNull(detail);
6         System.out.println(detail.toString());
7     }
總結來說,唯一的不同在於 @Entity 類別,在那個類別中,不需要使用 @Table 來指出是屬於資料庫裡的那個 table。
另外,在 spring data 中提供另一種更簡潔的寫法,只要寫一個介面,繼承 Repository 或它的子介面 (CrudRepositoryPagingAndSortingRepository、RevisionRepository),可以直接在 method 上寫 sql,如下:
public interface AppointmentDetailDAO extends Repository<AppointmentDetail, Long> {
    @Query(value="select a.email, a.name, r.name as restaurantName, r.address as restaurantAddr, a.description, a.people, a.willPay, a.htime, a.sysId from Appointment a inner join Restaurant r on a.restaurant_sysId = r.sysId where a.sysId = :sysId", nativeQuery=true)
    public AppointmentDetail findDetailBySysId(@Param("sysId") Long sysId);
}
這裡要注意的是,AppointmentDetail 是複合型別,只能用 native query (nativeQuery 預設值為 false),不能用 JPQL,否則會有無法轉換型別的錯誤訊息「org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.Object[]] to type …」。




【Lombok 的使用】
  1. 從官網上下載 lombok.jar,將它放在 eclipse.ini 所在的目錄裡。
  2. 在 eclipse.ini 中加入如下內容:
  3. -Xms2048m
    -Xmx4096m
    -javaagent:lombok.jar
  4. 重啟 eclipse。

2017年1月8日 星期日

spring data: CrudRepository 自動生成

spring 專案針對 ORM 獨立出一個命名為 spring data 的專案,使用 spring data 可以更簡化程式設計,回頭看一下「JPA + Hibernate + Spring + JUnit (annotation)」,現在要將它用 spring data 改寫 …
  • 修改 JpaConfig
在類別的開頭加上紅色部份,指出要使用的 entityManagerFactory 及 transactionManager,還有就是要載入那些 package 下符合條件的介面,也就是繼承了 spring data 為方便程式開發者提供的一些制式介面,用來簡化程式,這些介面包含有 CrudPository、PagingAndSortingRepository、JpaRepository。
 1 @Configuration
 2 @EnableTransactionManagement
 3 @EnableJpaRepositories (
 4     basePackages = "idv.steven.invitation.database.dao",
 5     entityManagerFactoryRef = "entityManagerFactory",
 6     transactionManagerRef = "jpaTransactionManager"
 7 )
 8 public class JpaConfig {
 9     @Bean
10     public DataSource dataSource() {
11         BasicDataSource ds = new BasicDataSource();
12         ds.setDriverClassName("oracle.jdbc.OracleDriver");
13         ds.setUrl("jdbc:oracle:thin:@192.168.0.102:1521:DemoDB");
14         ds.setUsername("steven");
15         ds.setPassword("P@ssw0rd");
16         ds.setInitialSize(5);
17         
18         return ds;
19     }
20     
21     @Bean
22     public JpaVendorAdapter jpaVendorAdapter() {
23         HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
24         
25         adapter.setDatabase(Database.ORACLE);
26         adapter.setShowSql(true);
27         adapter.setGenerateDdl(false);
28         adapter.setDatabasePlatform("org.hibernate.dialect.Oracle10gDialect");
29         
30         return adapter;
31     }
32     
33     @Bean
34     public PlatformTransactionManager jpaTransactionManager() {
35         JpaTransactionManager tm = new JpaTransactionManager();
36         tm.setEntityManagerFactory(this.entityManagerFactory().getObject());
37         
38         return tm;
39     }
40         
41     @Bean
42     public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
43         LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
44         emf.setDataSource(this.dataSource());
45         emf.setPackagesToScan(new String[] {"idv.steven.invitation.database.dao", "idv.steven.invitation.database.entity" });
46         emf.setJpaVendorAdapter(this.jpaVendorAdapter());
47         
48         return emf;
49     }
50 }
  • 刪除 UsersDAOImpl 類別,並修改 UsersDAO 介面。
1 public interface UsersDAO extends CrudRepository<Users, String> {
2     
3 }
讓我們開發的介面繼承 CrudRepository,spring data 就會提供一些制式的 method,並自動產生實作的程式碼。在 CrudRepository 後面的 Users 及 String,分別指出 Entity 及 primary key 的型別。CrudRepository 提供的制式 method 如下: (參考 Professional Java for Web Applications: Featuring Websockets, Spring Framework, JPA Hibernate, and Spring Security 一書,p.  638.)
count() returns a long representing the total number of unfiltered entities extending T.
delete(T) and delete(ID) delete the single, specified entity, whereas delete(Iterable<? extends T>) deletes multiple entities and deleteAll() deletes every entity of that type.
exists(ID) returns a boolean indicating whether the entity of this type with the given surrogate key exists.
findAll() returns all entities of type T, whereas findAll(Iterable<ID>) returns the entities of type T with the given surrogate keys. Both return Iterable<T>.
findOne(ID) retrieves a single entity of type T given its surrogate key.
save(S) saves the given entity (insert or update) of type S where S extends T, and returns S, the saved entity.
save(Iterable<S>) saves all the entities (again, S extends T) and returns the saved entities as a new Iterable<S>.
  • 測試
雖然 UsersDAO 介面裡沒有任何 method,但是我們可以直接調用 CrudRepository 提供的 method …
1 @Transactional
2 @Test
3 public void findTalking() {
4     Users users = dao.findOne("hi.steven@gmail.com");
5     List<Appointment> talking = users.getAppointments();
6     for(Appointment t:talking) {
7         System.out.println("結果:" + t.toString());
8     }
9 }




【題外話 - 在 build.gradle 中設定排除特定 jar 檔】
要管理 java 的 jar 檔不是一件簡單的事,所以陸續出現了 Maven、Gradle 等解決方案 (當然這兩個軟體不只在解決 jar 檔的相依性),我現在慣用的是 Gradle,很方便的它會幫我找到所有相依的 jar 檔。但是,也可能出現 jar 檔會同時有兩個,只是版本不一樣,因為 open source 本來就不可完全協調的很一致,一定會有使用不同版本 jar 的可能性存在,當系統很龐大時,引入的 open source 甚至 framework 很多時,很難找出那個 jar 檔是因為那個 framework 的相依 jar 檔而被引入,像是我最近遇到的就是 gradle 抓回來的 jar 檔,同時存在著 hibernate-jpa-2.0-api 及 hibernate-jpa-2.1-api,這樣程式當然會出問題,必須在 build.gradle 中將不需要的 jar 檔排除! 方法很簡單,在 build.gradle 檔裡加入如下設定:
configurations {
    runtime.exclude group: 'org.hibernate.javax.persistence', module: 'hibernate-jpa-2.0-api'
}
runtime.exclude 算是指令,指出要排除那個 jar 檔,重點在於後面的 group、module 值我們怎麼知道要填什麼? 以上面的設定來說,要排除的是 hibernate-jpa-2.0-api-1.0.1.Final.jar,那麼就先在 Google 裡搜尋「 hibernate jpa maven」找到 maven 的 repository,由裡面可以找到 gradle 要由 maven repository 取得 jar 檔所需要的設定,如下:
compile group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.0-api', version: '1.0.1.Final'
值有三個 group、name、version,將 group的值填入上面的 group,name 的值填入上面的 module,這樣就大功告成了!