20211008のJavaに関する記事は3件です。

【Java】Spring JDBCで登録機能の実装

今回はJava SpringFWを用いて登録・更新・削除機能を実装してみました。 バリデーションについては、JPAではなくJDBCtemplateを使用しております。 以下はそれぞれに分けて解説をしておりますので、よければ参考にしてください!! • 【更新】Spring JDBCで更新機能の実装(作成中) • 【削除】Spring JDBCで削除機能の実装(作成中) • 【まとめ】Spring JDBCで登録・更新・削除(作成中) • 【エラー解消】Spring JDBCでエラーメッセージが消えない!?(作成中) 0, 目次 1, 完成品 2, 環境 3, 関連ソースコード     ① Form と entity   ② Controller   ③ Service   ④ Impl と Custom と Repository   ⑤ HTML と JavaScript   ⑥ messages.properties と ValidationMessages.properties   ⑦ pom.xml 4, 参考にしたサイト 1, 完成品 ユーザー登録までの流れ 本記事では、A5SQLを用いてDBにユーザーを登録していきます。 HTMLではリストを使って初期値としてユーザーを表示させています。 そのため、事前にテストデータとしてEmployeesテーブルを作成しました。 参考までに:【SQL】テーブルとレコード(登録処理用) ユーザーの登録や更新もこちらのテーブルを使っていきます! 2, 環境 Windows10 sts-4.10.0.RELEASE A5SQL Mk-2 Version2.16.1 〜前提として〜 ・Spring boot(STS)が導入されていること ・SQLを用いたデータベースの簡単な知識(insert,updateなどのSQL文) ・デバックができること 3, 関連ソースコード  ① Form と entity EmployeesForm.java package jp.co.nakasan.test.database.dbtest; import javax.validation.constraints.Pattern; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotBlank; import org.springframework.stereotype.Component; import lombok.Data; @Data @Component public class EmployeesForm { /** * radio */ private String radio; /** * id */ private String id; /** * 名前 */ // nullまたは空文字でないことをチェック @NotBlank(message = "{notblank_check}") // 最小値・最大値を指定(@size,@maxだと常に判定される) @Length(min = 0, max = 100, message = "{length_check}") private String name; /** * 年齢 */ @NotBlank(message = "{notblank_check}") @Length(min = 0, max = 3, message = "{length_check_age}") private String age; /** * 誕生日 */ @NotBlank(message = "{notblank_check}") // 誕生日の正規表現を指定 @Pattern(regexp = "([1-2]{1}[0-9]{3}/[0-1]{1}[0-9]{1}/[0-3]{1}[0-9]{1}|[1-2]{1}[0-9]{3}-[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1})", message = "{pattern_check_birthday}") private String birthday; /** * メールアドレス */ @Length(min = 0, max = 100, message = "{length_check}") // メールの正規表現を指定 @Email private String eMail; /** * 住所 */ @Length(min = 0, max = 100, message = "{length_check}") private String address; /** * グループ */ @Length(min = 0, max = 100, message = "{length_check}") private String groupId; } 今回は、BindingResultクラスを使用するため、@NotBlankのようにバリデーションをかけています。 message = "{notblank_check}" はエラーメッセージの日本語化に対応させるため、名前をつけています。 (あとでpropertiesファイルに紐付けます。) また、@DateTimeFormatというアノテーションも使えるらしいのですが、フィールドの型をlocalDate(日付時刻型)にする必要があるとのこと。。今回は大人しく自作の正規表現でパターン化しました? Employees.java package jp.co.nakasan.test.database.entity; import java.io.Serializable; import java.sql.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import com.sun.istack.NotNull; import lombok.Data; @SuppressWarnings("serial") @Data // ゲッターとセッターが不要になる @Entity // Entityクラスの宣言 @Table(name = "employees", schema = "データベース名") // テーブルと紐づけ public class Employees implements Serializable{ /** * 社員id */ @Id // プライマリキー @Column(name = "id") // データベース上のカラムとのマッピングを指定 private Integer id; /** * 名前 */ @Column(name = "name") @NotNull private String name; /** * 年齢 */ @Column(name = "age") @NotNull private Integer age; /** * 生年月日 */ @Column(name = "birthday") @NotNull private Date birthday; /** * e-mail */ @Column(name = "email") private String email; /** * 住所 */ @Column(name = "address") private String address; /** * グループ */ @Column(name = "groupid") private String groupId; } Entityクラスではデータベースの使用するテーブルとを紐づけています。 物理名ではbirthDayになっているが、マッピングするSpringPhysicalNamingStrategyクラスではクラス内のすべてを小文字変換しているので、大文字小文字を区別する必要があるようです。groupIdも同様です。 ② Controller EmployeesController.java package jp.co.nakasan.test.practicebase.dbtest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping(path = "/valiSpring") public class EmployeesController { @Autowired private EmployeesForm form; @Autowired private EmployeesService service; /** * 初期画面 * @param model // 画面表示 * @return */ @RequestMapping(path = "", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("List", service.getSelectedJudges()); model.addAttribute("employeesForm", form); return "practice/valiSpring"; } /** * データの表示 * @param EmployeesForm * @param model // 画面表示 * @param BindingResult // バリデーション * @param proc  // Jsのクリックイベント"select"または"button" * @return */ @RequestMapping(path = "{proc}", method = RequestMethod.POST) // (form, bindingresult, model)の順でないとエラー public String pushSend(@PathVariable String proc, @Validated EmployeesForm inputForm, BindingResult result, Model model) { // セレクトボックス押下時 if (proc.equals("select")) { // ユーザー情報取得 this.form = service.getSelectedInfoData(inputForm); model.addAttribute("employeesForm", this.form); // 送信ボタン押下時 "null" } else if (inputForm.getRadio() == null) { // 入力値の表示(BindingResult対策) this.form = service.keepForm(inputForm); // 選択チェックエラー model.addAttribute("messageList", service.getSelectedNull()); model.addAttribute("employeesForm", this.form); // 送信ボタン押下時 "insert" } else if ((inputForm.getRadio().equals("insert")) && (!(result.hasErrors()))) { // ユーザー新規登録 & 入力チェックエラー model.addAttribute("messageList", service.getSelectedInsert(inputForm)); // 送信ボタン押下時 "update" } else if ((inputForm.getRadio().equals("update")) && (!(result.hasErrors()))) { --省略-- // 送信ボタン押下時 "delete" } else if (inputForm.getRadio().equals("dalete")) { --省略-- } model.addAttribute("List", service.getSelectedJudges()); return "practice/valiSpring"; } } path = "{proc}"で「select」と「button」のどちらか一方のパラメータを受け取れるようにしています。 こうすることで、ユーザーが画面内のどっちのボタンを押したのかを判定できるようにしています。(jsファイルで定義) 更新、削除処理については別記事で紹介してますので、本記事では割愛させて頂きますっ。?‍♂️ ③ Service EmployeesService.java package jp.co.nakasan.test.practicebase.dbtest; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Objects; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import jp.co.nakasan.test.database.entity.Employees; import jp.co.nakasan.test.database.repository.EmployeesRepository; @Service // データベースのトランザクション制御 @Transactional public class EmployeesService { @Autowired EmployeesRepository repository; /** * 全ユーザー情報取得処理 * @param * @return employees // 全ユーザーのデータ(id,名前,年齢,誕生日) */ public List<Employees> getSelectedJudges() { // 全ユーザー情報取得用変数リスト List<Employees> employees = null; // 全ユーザー情報取得(id,名前のみ) employees = repository.findEmployees(); return employees; } /** * 入力値を初期化させない処理 * @param EmployeesForm * @return form // 入力値をそのまま返す */ // バリデーションエラーメッセージの隔離メソッド public EmployeesForm keepForm(EmployeesForm inputForm) { // formの初期化(別formの生成) EmployeesForm form = new EmployeesForm(); // 入力初期値の保存 form.setRadio(inputForm.getRadio()); form.setId("0"); form.setName(inputForm.getName()); form.setAge(inputForm.getAge()); form.setBirthday(inputForm.getBirthday()); form.setEMail(inputForm.getEMail()); form.setAddress(inputForm.getAddress()); form.setGroupId(inputForm.getGroupId()); return form; } /** * ユーザー情報取得処理(1件) * @param EmployeesForm * @return */ public EmployeesForm getSelectedInfoData(EmployeesForm inputForm) { Employees employeesList = null; // formの初期化 EmployeesForm form = new EmployeesForm(); // 選択されたユーザーID String selectedId = inputForm.getId(); // ユーザーを選択していない場合 if (selectedId.equals("0")) { form.setId("0"); form.setName(""); form.setAge(""); form.setBirthday(""); form.setEMail(""); form.setAddress(""); form.setGroupId(""); } else { // ユーザーidで情報取得 employeesList = repository.findInfoData(selectedId); // フォーマットの指定 SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); // Date型をString型に変換 String Birthday = sdf.format(employeesList.getBirthday()); form.setRadio(inputForm.getRadio()); form.setId(employeesList.getId().toString()); // nullを許可する // toString()だとnullが入ってしまう form.setName(Objects.toString(employeesList.getName(), "")); form.setAge(Objects.toString(employeesList.getAge(), "")); form.setBirthday(Objects.toString(Birthday, "")); form.setEMail(Objects.toString(employeesList.getEmail(), "")); form.setAddress(Objects.toString(employeesList.getAddress(), "")); form.setGroupId(Objects.toString(employeesList.getGroupId(), "")); } return form; } /** * ラジオボタン判定 * @param * @return list // エラーメッセージリスト */ public List<String> getSelectedNull() { List<String> list = new ArrayList<>(); list.add("新規登録、更新、削除のいずれかを選択してください。"); return list; } /** * ユーザー新規登録処理 * @param EmployeesForm * @return list // エラー&登録結果の判定リスト */ public List<String> getSelectedInsert(EmployeesForm inputForm) { // insert用変数 Employees employees = new Employees(); // エラーメッセージ用リスト List<String> list = new ArrayList<>(); // フォーマットを指定 SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); // Date型の初期化 Date formatDate = null; // yyyy/MM/ddにフォーマット変換 String formDate = inputForm.getBirthday().replace('-', '/'); try { // String型からDate型に変換 formatDate = sdf.parse(formDate); // java.util.Dateは日付型のクラス // 日付型のクラスに変換 java.util.Date d1 = formatDate; // java.sql.date は java.util.dateクラスのサブクラス // サブクラスに変換(時分秒ミリ秒まで保存可) java.sql.Date d3 = new java.sql.Date(d1.getTime()); employees.setName(inputForm.getName()); employees.setAge(Integer.parseInt(inputForm.getAge())); employees.setBirthday(d3); // 空文字じゃなかったら実行(空文字だったらnullとして登録) if (!(inputForm.getEMail() == "")) { employees.setEmail(inputForm.getEMail()); } if (!(inputForm.getAddress() == "")) { employees.setAddress(inputForm.getAddress()); } if (!(inputForm.getGroupId() == "")) { employees.setGroupId(inputForm.getGroupId()); } // ユーザー登録処理 int rowNumber = repository.insertRecord(employees); // デフォルトで"false"を設定 boolean result = false; if (rowNumber > 0) { // insert成功 result = true; } // ユーザー登録結果の判定 if (result == true) { list.add("新規登録完了"); } } catch(Exception e) { list.add("データベースに問題がありました。入力しなおしてください。(登録処理)"); } return list; } --更新処理-- --削除処理-- } Serviceクラスでの主な処理は以下の通りです。 「全ユーザー情報取得処理」・・・セレクトボックスで各ユーザー選択時に使用 「入力値を初期化させない処理」・・・イベント時に入力値をそのままキープさせる 「ユーザー情報取得処理(1件)」・・・選択したユーザー(1件)のデータ全てを取得 「ラジオボタン判定処理」・・・エラーメッセージの作成 「ユーザー新規登録処理」・・・ユーザー登録処理 判定メッセージについては各メソッドごとにリストを生成し、出力しています。 ④ Impl と Custom と Repository EmployeesRepository.java package jp.co.nakasan.test.database.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import jp.co.nakasan.test.database.entity.Employees; import jp.co.nakasan.test.database.repository.custom.EmployeesRepositoryCustom; /** * カスタムインターフェイスとJpaRepositoryを継承(extends)したもの。 * @author  なかさん。 * */ @Repository public interface EmployeesRepository extends JpaRepository<Employees, Integer>, EmployeesRepositoryCustom { } 基本JPAリポジトリという便利なものでDBからデータなどの情報を取得できるわけですが、今回使用しているJdbcTemplateはJPAでは取得できないような状況にも対応できる(自分で好きにカスタムして取得できる)とのことで、つまりはJPAより自由度が高いということですね。時と場合により使い勝手が良さそうです。? JdbcTemplateはのちにでてくるImplクラスに記述します。 そして、JPA以外はカスタムインターフェースの継承が必要です。 厳密には必要なわけではありませんが、JDBCのメソッドを都度列挙しなくて済むようになるので必然かと。 Repositoryはそれらの継承のためのファイルです。 継承イメージとしては Repositoryクラス > JpaRepositry Repositoryクラス > Customクラス(インターフェース) > Implクラス < JdbcTemplate みたいな感じです。 EmployeesRepositoryCustom.java package jp.co.nakasan.test.database.repository.custom; import java.util.List; import jp.co.nakasan.test.database.entity.Employees; /** * implの継承 * @author なかさん。 * */ public interface EmployeesRepositoryCustom { /** * @param   */ public List<Employees> findEmployees(); /** * @param  id */ public Employees findInfoData(String id); /** * @param  employees */ public int insertRecord(Employees employees); } カスタムクラスではimplクラスを継承します。 implで作成したメソッドをそのままコピペするだけで終了です。^^; 逆にカスタムに持ってこないと、せっかくimpl内で書いたメソッドがそのままの状態で放置プレイされてしまいます汗 EmployeesRepositoryImpl.java package jp.co.nakasan.test.database.impl; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; import jp.co.nakasan.test.database.entity.Employees; import jp.co.nakasan.test.database.repository.custom.EmployeesRepositoryCustom; /** * JDBCtemplateでレコード取得 * @author  なかさん。 * */ @Component public class EmployeesRepositoryImpl implements EmployeesRepositoryCustom { @Autowired JdbcTemplate jdbc; /** * レコードの取得(全ユーザーのid,名前のみ) * @param * @return */ public List<Employees> findEmployees() { // 返却値 初期化 List<Employees> resultRec = null; RowMapper<Employees> rowMapper = new BeanPropertyRowMapper<Employees>(Employees.class); // -------------------- // クエリー生成 // -------------------- StringBuffer query = new StringBuffer(); query.append(" SELECT"); query.append(" e.id, "); query.append(" e.name   "); query.append(" FROM"); query.append(" employees e "); // データベースからデータをリストで取得 List<Employees> list = jdbc.query(query.toString(), rowMapper); resultRec = list; return resultRec; } /** * レコードの取得(ユーザー1件のレコード全て) * @param id * @return */ public Employees findInfoData(String id) { // 返却値 初期化 Employees resultRec = null; RowMapper<Employees> rowMapper = new BeanPropertyRowMapper<Employees>(Employees.class); // -------------------- // クエリー生成 // -------------------- StringBuffer query = new StringBuffer(); query.append(" SELECT"); query.append(" e.id, "); query.append(" e.name, "); query.append(" e.age, "); query.append(" e.birthDay, "); query.append(" e.email, "); query.append(" e.address, "); query.append(" e.`groupId` "); query.append(" FROM"); query.append(" employees e "); query.append(" WHERE"); query.append(" e.id = ? "); // フォームのidとテーブルのidを紐づけるためリスト化 ArrayList<String> paramList = new ArrayList<String>(); paramList.add(id); // データベースからデータをリストで取得 List<Employees> list = jdbc.query(query.toString(), rowMapper, paramList.toArray()); resultRec = list.get(0); return resultRec; } /** * ユーザー新規登録 * @param employees entityクラス * @return */ public int insertRecord(Employees employees) { // 返却値 int query; // -------------------- // クエリー生成 // -------------------- query = jdbc.update("INSERT INTO" + " employees " + "(name," + " age," + " birthDay," + " email," + " address," + " groupId)" + " VALUES(?, ?, ?, ?, ?, ?)", employees.getName(), employees.getAge(), employees.getBirthday(), employees.getEmail(), employees.getAddress(), employees.getGroupId()); return query; } --更新処理-- --削除処理-- } Implクラスでの主な処理は以下の通りです。 ・ レコードの取得(全ユーザーのid,名前のみ) ・ レコードの取得(ユーザー1件のレコード全て) ・ ユーザー新規登録 新規登録処理が成功すれば、返却値queryに1が        失敗すれば、返却値queryに0が代入されます。 この値はServiceクラスでboolean型に変換され、処理結果メッセージの判定として使います。 ⑤ HTML と JavaScript valiSpring.html <body> <div class="contents"> <form id="employeesForm" th:object="${employeesForm}" method="POST"> <main class="ml-3 mt-3" th:fragment="init" role="main"> IDは自動採番です。新規登録時,IDを選択しても反映されません。 <br> 再度「選択してください」を押すと入力値がクリアされます。 <!--判定メッセージを出す--> <div class="0" style="color: green;"> <tr th:each="name : ${messageList}"> <br> <td th:text="${name}"> </tr> </div> <!--エラーメッセージを出す--> <div class="0" style="color: brown;" th:if="${#fields.hasErrors('*')}"> <tr th:each="err : ${#fields.errors('*')}"> <br> <td th:text="${err}"> </tr> </div> <!--登録用のラジオボタンを表示--> <p> <div class="1"> <input type="radio" name="radio" th:value="insert" th:field="*{radio}">新規登録 <input type="radio" name="radio" th:value="update" th:field="*{radio}">更新 <input type="radio" name="radio" th:value="delete" th:field="*{radio}">削除 </div> </p> <!--IDのセレクトボタン--> <div class="2"> <select name="employeesId" th:field="*{id}" th:value="*{id}"> <option value="0">--- 選択してください ---</option> <option th:each="list : ${List}" th:value="${list.id}" th:selected="${list.id == id}" th:inline="text">[[${list.id}]] : [[${list.name}]]</option> </select> </div> <!--削除時以外の必須項目--> <div class="3 mt-3"> <label style="width: 90"> 名前 </label> <input style="width: 120" name="name" th:field="*{name}" th:value="*{name}"/> <label style="width: 170"> 年齢 </label> <input style="width: 120" name="age" th:field="*{age}" th:value="*{age}"/> <label style="width: 300"> 生年月日 </label> <input style="width: 120" name="birthday" th:field="*{birthday}" th:value="*{birthday}"/> </div> <!--項目--> <div class="4 mt-3"> <label style="width: 90"> e-mail </label> <input style="width: 200" name="eMail" th:field="*{eMail}" th:value="*{eMail}"/> <label style="width: 90"> 住所 </label> <input style="width: 550" name="address" th:field="*{address}" th:value="*{address}"/> </div> <!--項目--> <div class="5 mt-3"> <label style="width: 90"> グループ </label> <input style="width: 70" name="groupId" th:field="*{groupId}" th:value="*{groupId}"/> <button class="ml-4" name="send01" th:name="send">送信</button> </div> </main> </form> </div> </body> ここでもしBindingResultのバリデーションメッセージが表示されないとき、form名を疑ってください。 私が初めて実装したとき、form名はValiSpringFormで、エラーメッセージはformに「default messages={0}は必須項目です。」と入っているのにうまく表示できませんでしたが、結論としてValiSpringForm → valiSpringFormと変更するとうまくいきました!(頭文字Vを小文字に変更) 原因はBindingResultに入っているメッセージの中にform名が書いてあり、それがHTML側のform名と一致していなかったため表示されなかったみたいです。 おそらくクラス内でバリデーションチェックを行う際、頭文字が小文字へ強制的に変換されてしまうのでしょう。 なので、私の時はHTML,JavaScript,Controllerのform名とバリデーション結果のform名を一致させるとうまく表示できるようになりました!ぜひ参考までに。?‍♂️ Employees.js var practice = (function() { var global = { }; // 名前空間オブジェクトを private オブジェクトで定義 /** * ページロード時に実行される処理。 */ window.onload = function() { init(); }; function init() { // セレクトボタン押下時 $('select[name="id"]').change(function() { // 現在アクセスしているパス情報を取得 var rootPath = main.getDir(location, 0); // 遷移先への処理 var form = $("#employeesForm"); // "select"パラメータを渡す form.attr("action", rootPath + "/valiSpring/" + "select"); form.attr("target", "_self"); form.submit(); }); // 送信ボタン押下時 $('button[name="send"]').click(function() { // 現在アクセスしているパス情報を取得 var rootPath = main.getDir(location, 0); // 遷移先への処理 var form = $("#employeesForm"); // "buttom"パラメータを渡す form.attr("action", rootPath + "/valiSpring/" + "button"); form.attr("target", "_self"); form.submit(); }); } return global; // 名前空間オブジェクトを return する })(); Controller側でpath="{proc}"というようにパラメータを受け取る準備ができています。 あとは、selectやbuttonのように任意のパラメータをコントローラに渡し、イベントの処理分けをするといった形です。 ⑥ messages.properties と ValidationMessages.properties messages.properties --省略-- # フィールド名の日本語化 employeesForm.name = 名前 employeesForm.age = 年齢 employeesForm.birthday = 生年月日 employeesForm.eMail = メールアドレス employeesForm.address = 住所 employeesForm.groupId = グループ名 ここではバリデーションの日本語化をしています。 後でこちらのファイルを開くと employeesForm.name = \u540d\u524d  のように文字化けしてしまいますが、問題なく処理されますので気にしなくて大丈夫です! ValidationMessages.properties # 必須項目チェック notblank_check = {0}は必須項目です。 # 字数制限チェック length_check = {0}は100字以内で入力してください。 length_check_age = 年齢は3桁以内で入力してください。 # 正規表現チェック pattern_check_birthday = 生年月日はyyyy/MM/ddまたはyyyy-MM-ddの形で入力してください。 pattern_check_eMail = メールアドレスを正しく入力してください。 Formで{messages=...}と名前をつけたものと先程の日本語化したフィールド名を紐づけるため、resorceフォルダ直下に「ValidationMessages.properties」という名前でプロパティファイルを作成します。 ⑦ pom.xml 下記は参考までに。 pom.xml <project> --省略-- <!-- validationライブラリ --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.2.4.Final</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> --省略-- </project> 以上でユーザー登録処理の完了です? 他にもここでは解説しきれなかった更新・削除処理も執筆しておりますので、よければご覧ください! また、なにか気になる点や間違っているとこなどがあれば遠慮なくご指摘ください...!! まだまだ勉強中ですが、本記事が何かの参考になればとても幸いです。 4, 参考にしたサイト •  【Java・SpringBoot】Spring JDBC でユーザー登録 •  SpringFrameworkでJdbcTemplateを使ってみる •  Spring Bootのバリデーションメッセージの変更
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Couchbase Lite機能解説:データベースの使い方

はじめに Couchbase Liteの利用に資するために、下記のドキュメントの内容(英語)を整理します。 初期化 APIを使用する最初のステップは、イニシャライザーを呼び出すことです。 初期化前に他のAPIメソッドが呼び出されると、例外が発生します。 例1.データベース初期化 // Initialize the Couchbase Lite system CouchbaseLite.init(); データベース作成/オープン Databaseクラスを使用して、新しいデータベースを作成したり、既存のデータベースをオープンすることができます。 データベース名とオプションでDatabaseConfigurationを渡します。 注意すべき点は次のとおりです。 データベースのオープン/作成は非同期プロセスです 指定されたデータベースが指定された場所またはデフォルトの場所に存在しない場合は、新しいデータベースが作成されます データベースは、ディレクトリを指定しない限り、デフォルトの場所に作成されます(参照:DatabaseConfigurationおよびDatabaseConfiguration.setDirectory()) ベストプラクティスは、データベースへのパスを常に明示的に指定することです。 「データベースファイルの検索」も参照してください。 例2.データベース作成/オープン DatabaseConfiguration config = new DatabaseConfiguration(); config.setDirectory(context.getFilesDir().getAbsolutePath()); Database database = new Database(DB_NAME, config); ここでは、データベースのディレクトリパスを指定しています。 データベースをクローズする データベースを閉じるには、Database.close()を使用します。 ただし、注意すべき点がいくつかあります。 データベースを閉じることは同期操作であり、すぐに有効になります。 開いていないデータベースを閉じることはできません。 データベースを開く(または作成する)ことは非同期であることに注意してください。したがって、オープン/作成を開始した直後にクローズを発行すると、そのプロセスが完了していない場合にエラーが発生する可能性があります。 データベースをクローズすると、アクティブなレプリケーション、リスナー、またはデータベースに接続されているライブクエリもすべてクローズされます。 レプリケーションを開始した直後にデータベースを閉じると、同期で例外が生成される可能性があります (例:IllegalStateException: Attempt to perform an operation on a closed database)。 オープンしているすべてのデータベースをクローズすることをアプリケーションワークフローに組み込むことをお勧めします。 例3.データベースを閉じる database.close() データベースの暗号化 データベースの暗号化はEnterprise Editionのみで利用できる機能です。 Java上のCouchbase Liteには、データベースを暗号化する機能が含まれています。これにより、モバイルアプリケーションは、データがデバイスに保存されているときに、保存されているデータを保護できます。データベースの暗号化に使用されるアルゴリズムは256ビットAESです。 暗号化を有効にするには、DatabaseConfiguration.encryptionKeyプロパティを選択した暗号化キーに設定する必要があります。データベースをオープンするたびに、この暗号化キーを指定します。 DatabaseConfiguration config = new DatabaseConfiguration(); config.setEncryptionKey(new EncryptionKey("PASSWORD")); Database database = new Database(DB_NAME, config); Couchbase Liteはキーを保持しません。キーを管理し、AppleのキーチェーンやAndroidのキーストアなどのプラットフォーム固有の安全なキーストアに保存するのはアプリケーションの責任です。 暗号化されたデータベースは、最初に暗号化に使用されたのと同じ言語SDK(Swift、C#、Java、Java(Android)、またはObjective-C)でのみ開くことができます。たとえば、データベースがSwift SDKで暗号化されてからエクスポートされた場合、データベースはSwift SDKでのみ読み取り可能になります。 データベースファイルの検索 デフォルトでは、JavaのCouchbase Liteデータベースは、カレントディレクトリの下の<databaseName>.cblite2というディレクトリに作成されます 。 この場所は、DatabaseConfigurationメソッドによって設定されます。データベースの場所を明示的に設定するには、常にこれを使用する必要があります。これを行う方法については、次の例を参照してください。 DatabaseConfiguration thisConfig = new DatabaseConfiguration(); thisConfig.setDirectory("yourDBpath"); Database thisDB = new Database("db", thisConfig); コマンドラインツール cblite は、Couchbase Lite2.xデータベースを検査およびクエリするためのコマンドラインツールです。 couchbaselabsのGitHubリポジトリからダウンロードしてビルドできます。このcbliteツールは、Couchbaseサポートポリシーではサポートされていないことに注意してください。 トラブルシューティング 診断情報の最初のソースとして、Couchbaseのコンソールログを使用する必要があります。デフォルトのログレベルの情報が不十分な場合は、データベースエラーに焦点を当てて、より詳細なメッセージを生成できます。例4を参照してください。 ログの使用の詳細については、「ログの使用」を参照してください。 例4.データベースのログレベルを上げる Database.log.getConsole().setDomain(LogDomain.DATABASE);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java 17新機能まとめ

Java 17が2021/9/14にリリースされました。 機能的に多くのプログラマに関係ありそうな変更はApple Siliconに対応したくらいですが、LTSであるということが大きいと思います。 詳細はこちら Oracle Releases Java 17 Java SE 17 Platform JSR 392 JDK 17 GA Release APIドキュメントはこちら Overview (Java SE 17) 変更点まとめはこちら https://docs.oracle.com/en/java/javase/17/migrate/significant-changes-jdk-release.html#GUID-339B2415-8BA8-438C-93AF-F9C746F7CE45 今回はLTSなので、前回LTSであるJava 11からの差分もまとめられています。 JEPの差分はこちら。 https://openjdk.java.net/projects/jdk/17/jeps-since-jdk-11 APIの差分はこちら。 https://docs.oracle.com/en/java/javase/17/docs/api/new-list.html MacやLinuxでのインストールにはSDKMAN!をお勧めします Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。(まだ用意されていないものもあります) Oracle JDK Adoptium Azul Zulu Liberica JDK Amazon Corretto 17 Microsoft Build of OpenJDK アップデートは10月に17.0.1が、翌年1月に17.0.2がリリースされることになります。 Oracle JDKも「Oracle No-Fee Terms and Conditions License」で無償での商用利用ができるようになっています。 次のLTSは2年後の21になる模様。 https://mreinhold.org/blog/forward-even-faster JEP 大きめの変更はJEPでまとまっています。 https://openjdk.java.net/projects/jdk/17/ 今回は14個のJEPが取り込まれました。すでにプレビューなどで出ていたものが3つあり、新たに取り込まれたものは11です。非推奨・削除が4つあるので、新機能としては7つですね。 JEP 306: Restore Always-Strict Floating-Point Semantics JEP 356: Enhanced Pseudo-Random Number Generators JEP 382: New macOS Rendering Pipeline JEP 391: macOS/AArch64 Port JEP 398: Deprecate the Applet API for Removal JEP 403: Strongly Encapsulate JDK Internals JEP 406: Pattern Matching for switch (Preview) JEP 407: Remove RMI Activation JEP 409: Sealed Classes JEP 410: Remove the Experimental AOT and JIT Compiler JEP 411: Deprecate the Security Manager for Removal JEP 412: Foreign Function & Memory API (Incubator) JEP 414: Vector API (Second Incubator) JEP 415: Context-Specific Deserialization Filters 言語機能 言語仕様にかかわる変更としては次のようなものがあります。 JEP 306: Restore Always-Strict Floating-Point Semantics JEP 406: Pattern Matching for switch (Preview) JEP 409: Sealed Classes JEP 306: Restore Always-Strict Floating-Point Semantics 常に厳密な浮動小数点演算をする。strictfpをつけるのと同じ動作になる。 もともと厳密な浮動小数点演算は負荷が高いからCPU任せをデフォルトにして厳密に演算したい場合はstrictfpをつけるというふうにJava 1.2でなったけど、もうSSE2で対応できて負荷もなくなったので常にstrictfpでやろう、ということらしい。 JEP 406: Pattern Matching for switch (Preview) Switch文・式でパターンマッチングが使えます。 Object o; System.out.println(switch (o) { case String s -> " %s ".formatted(s); case Integer i -> "%,d".formatted(i); default -> o.toString(); } ガード節もあります。 Object o; System.out.println(switch (o) { case String s && s.length() >= 5 -> s.toUpperCase(); case String s -> " %s ".formatted(s); case Integer i -> "%,d".formatted(i); default -> o.toString(); } switchにnullを渡すとjavacが落ちます。 public class def { public static void main(String[] args) { switch (null) { default: } } } 6月に報告されてそのままリリースされてますね。 https://bugs.openjdk.java.net/browse/JDK-8269113 JEP 409: Sealed Classes Sealedクラスは、継承できるクラスを限定する機能です。仕様の名前は「クラス」 となってますが、interfaceでも使えます。 クラス名のあとに permitsで継承クラスを指定していくという感じになります。 public abstract sealed class Shape permits Circle, Rectangle, Square {...} そうするとShapeクラスを継承できるのはCircle、Rectangle、Squareに限定されます。Circle、Rectangle、Squareは必ず定義する必要があります。 これらのクラスは、モジュール化しているのであれば同じモジュール内、モジュール化していないのであれば同じパッケージ内である必要があります。 また、permitsされるクラスが同一のソースだけにあるのであれば、permitsは省略できます。 permitsするクラスは定義のときにsealedかnon-sealedかfinalをつける必要があります。recordは暗黙的にfinalになるのでなにもつける必要はありません。 現状ではほとんど使いどころがないです。 例えば、Shapeの継承先は確定しているので、次のコードはコンパイル通ってほしいところですが、エラーになります。 String getName(Shape s) { if (s instanceof Circle) { return "円"; } else if (s instanceof Rectangle) { return "四角"; } else if (s instanceof Square) { return "正方形"; } } まだプレビューですがPattern matching for switchでswitch式/文でパターンマッチを使うときには、次のようにdefaultなしで書ける予定です。 String getName(Shape s) { switch (s) { case Circle c -> return "円"; case Rectangle r -> return "四角"; case Square q -> return "正方形"; } } もちろん、ここでswitch式を使うこともできますね。 String getName(Shape s) { return switch (s) { case Circle c -> "円"; case Rectangle r -> "四角"; case Square q -> "正方形"; } } ということで、現時点でのSealedクラスは今後の拡張のための下準備というところです。 背景にある考え方は、次の記事で言語設計者であるBrian Goetzが解説しています。 Java 注目の機能:Sealed クラス API APIの変更に関するJEPは次のようなものがあります。 JEP 290: Filter Incoming Serialization Data JEP 356: Enhanced Pseudo-Random Number Generators JEP 382: New macOS Rendering Pipeline JEP 398: Deprecate the Applet API for Removal JEP 403: Strongly Encapsulate JDK Internals JEP 407: Remove RMI Activation JEP 411: Deprecate the Security Manager for Removal JEP 412: Foreign Function & Memory API (Incubator) JEP 414: Vector API (Second Incubator) JEP 415: Context-Specific Deserialization Filters APIの変更はこちらにまとまっています。 http://cr.openjdk.java.net/~iris/se/17/build/latest/diffsFrom16%2b36/ JEP 356: Enhanced Pseudo-Random Number Generators 疑似乱数の改善 java.util.randomパッケージが作られて乱数発生まわりがまとめられています。 https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html JEP 382: New macOS Rendering Pipeline レンダリングにMetalフレームワークを使う JEP 398: Deprecate the Applet API for Removal Applet使わんやろってことでDeprecatedに JEP 403: Strongly Encapsulate JDK Internals デフォルトになっていた。強制になる。 JEP 396: Strongly Encapsulate JDK Internals by Default sun.misc.Unsafeなど以外の内部APIの隠蔽をデフォルトにします。 内部APIから標準APIへの移行がやりやすいようにします。 Java 9で内部APIを隠蔽する仕組みが導入されていてJava 16でデフォルト有効になりましたが、今回は--illegal-accessが削除されて強制になりました。 --add-opensで利用するクラスを明示する必要があります。 制限される内部APIの一覧はこちらです。 https://cr.openjdk.java.net/~mr/jigsaw/jdk8-packages-denied-by-default jshell> A.class.getClassLoader().loadClass("com.sun.beans.finder.ClassFinder").getMethod("findClass", String.class).invoke("test") | 例外java.lang.IllegalAccessException: class REPL.$JShell$15 cannot access class com.sun.beans.finder.ClassFinder (in module java.desktop) because module java.desktop does not export com.sun.beans.finder to unnamed module @77556fd | at Reflection.newIllegalAccessException (Reflection.java:385) | at AccessibleObject.checkAccess (AccessibleObject.java:687) | at Method.invoke (Method.java:559) | at (#5:1) JEP 407: Remove RMI Activation Java 1.2で導入されたRMI Activationですが、時代遅れってことでJava 15でDeprecatedになり、今回削除されました。 ※ RMI自体は消えません rmidというデーモンにRMIサービスを登録して、必要に応じてリモートオブジェクトを起動するという仕組みでした。Java 8からは必須ではなくなっています。rmidも削除されました。 JEP 411: Deprecate the Security Manager for Removal アプレットを動かすときのサンドボックスなど、リモートからやってきたコードを安全に動かす仕組みをもってたけど、サーバサイドでは使われてないしアプレットなくなったらいらんやろってことでdeprecated JEP 412: Foreign Function & Memory API (Incubator) 変更点 ネイティブライブラリの呼び出しを行う 外部メモリのアクセスにはForeign Memory Access APIを使う JNIの代替 MemorySegmentとMemoryAddressの明確な分離 MemoryAccessインタフェースの導入。VarHandle APIの利用を最小限にする 共有セグメントのサポート Cleanerつきセグメント ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。 ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。 Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。 JNIを使うとCコードを書く必要があり、性能もよくないです。 ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入されたわけです。そしてJava 15でセカンドインキュベータになっています。 次のようなコードになります。 import jdk.incubator.foreign.*; import java.nio.ByteOrder; VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder()); try (MemorySegment segment = MemorySegment.allocateNative(100)) { for (int i = 0 ; i < 25 ; i++) { intHandle.set(seg, i * 4, i); } } コンパイル、実行では--add-modules jdk.incubator.foreignを付けてモジュールを読み込む必要があります。 パッケージもモジュール名と同様、jdk.incubator.foreignになっています。 MemoryHandlesからwithOffsetとwithStrideが消えましたね。 jshell> import jdk.incubator.foreign.* jshell> MemoryHandles. asAddressVarHandle( asUnsigned( class collectCoordinates( dropCoordinates( filterCoordinates( filterValue( insertCoordinates( permuteCoordinates( varHandle( Java 15ではMemoryHandlesからVarHandleを得るメソッドが拡充してました。 jshell> MemoryHandles. asAddressVarHandle( asUnsigned( class collectCoordinates( dropCoordinates( filterCoordinates( filterValue( insertCoordinates( permuteCoordinates( varHandle( withOffset( withStride( Java 14では次の3つのメソッドしかありませんでした。 jshell> MemoryHandles. class varHandle( withOffset( withStride( Java 16ではMemoryAccessというユーティリティが用意されて、VarHandleを使わずにシンプルにかけるようになっています。 import jdk.incubator.foreign.*; try (MemorySegment segment = MemorySegment.allocateNative(100)) { for (int i = 0 ; i < 25 ; i++) { MemoryAccess.setIntAtOffset(i * 4, i); } } JEP 414: Vector API (Second Incubator) AVX命令のような、複数のデータに対する計算を同時に行う命令をJavaから利用できるようになります。 Project Panamaのひとつとして開発されていました。 Java 16でインキュベータとして導入されました。 使うためには実行時やコンパイル時に--add-modules jdk.incubator.vectorをつける必要があります。 import jdk.incubator.vector.*; static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256; void vectorComputation(float[] a, float[] b, float[] c) { for (int i = 0; i < a.length; i += SPECIES.length()) { // SPECIES.length() = 256bit / 32bit -> 8 VectorMask<Float> m = SPECIES.indexInRange(i, a.length); // 端数がマスクされる // a.lengthが11でiが8のとき最初の3つしか要素がないので [TTT.....] // FloatVector va, vb, vc; FloatVector va = FloatVector.fromArray(SPECIES, a, i, m); FloatVector vb = FloatVector.fromArray(SPECIES, b, i, m); FloatVector vc = va.mul(va). add(vb.mul(vb)). neg(); vc.intoArray(c, i, m); } } 利用できるのは次の6つの型です。それぞれに対応するVector型があって、これが基本になります。 型 bit幅 Vector byte 8 ByteVector short 16 ShortVector int 32 IntVector long 64 LongVector float 32 FloatVector double 64 DoubleVector ただ、利用するにはVectorSpeciesが必要です。利用したいVectorにSPECIES_*という定数が用意されているので、それを使います。*は一度に計算するbit数ですね。 jshell> FloatVector.SP SPECIES_128 SPECIES_256 SPECIES_512 SPECIES_64 SPECIES_MAX SPECIES_PREFERRED MAXではそのハードウェアで使える最大、PREFERREDは推奨ビット数だけど、同じになるんじゃないのかな。ここでは256bitが推奨されて、floatが8個同時に計算できるようになっていますね。 jshell> FloatVector.SPECIES_PREFERRED $11 ==> Species[float, 8, S_256_BIT] ハードウェアで使えるbit数は搭載CPUに依存しますが、普通のIntel/AMDであれば256、XEONとか つよつよCPUなら512かな。M1は128でした。ハードウェアでサポートされないbit数を使おうとするとソフトウェア処理になるので遅くなります。 実際のVectorはfrom*というメソッドで取得します。fromArray、fromByteArray、fromByteBufferが用意されています。インキュベータに入る前はfromValuesがあったのですが、なくなってますね。 Vectorを得られたら、用意されたメソッドで計算します。ひととおりの算術命令はあります。 jshell> va. abs() add( addIndex( bitSize() blend( broadcast( byteSize() compare( div( elementSize() elementType() eq( equals( fma( getClass() hashCode() intoArray( intoByteArray( intoByteBuffer( lane( lanewise( length() lt( max( min( mul( neg() notify() notifyAll() pow( rearrange( reduceLanes( reduceLanesToLong( reinterpretAsBytes() reinterpretShape( selectFrom( shape() slice( sqrt() sub( test( toArray() toDoubleArray() toIntArray() toLongArray() toShuffle() toString() unslice( viewAsFloatingLanes() viewAsIntegralLanes() wait( withLane( ところで、こういったメソッド呼び出しの内部でAVX命令などを呼び出すのでは遅くなるんではという気がしますが、実際にはJVM intrinsicsという仕組みでJITコンパイラがこれらのメソッド呼び出しをネイティブ関数呼び出しに置き換えます。 JEP 415: Context-Specific Deserialization Filters デシリアライズのフィルターをプログラミング可能にする。 Hex formetter / parser 16進数のフォーマット、パース C:\Users\naoki>jshell | JShellへようこそ -- バージョン17 | 概要については、次を入力してください: /help intro jshell> HexFormat.of().toHexDigits(12345678) $1 ==> "00bc614e" jshell> HexFormat.of().withUpperCase().toHexDigits(12345678) $2 ==> "00BC614E" jshell> HexFormat.of().fromHexDigits("00bc614e") $3 ==> 12345678 jshell> http://cr.openjdk.java.net/~rriggs/hex-formatter/java.base/java/util/Hex.html http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-August/068375.html http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-August/068256.html JVM JVMに関する変更はGraal削除の1件です。 JEP 410: Remove the Experimental AOT and JIT Compiler JEP 410: Remove the Experimental AOT and JIT Compiler Java 16ではOracle OpenJDKなどでビルドに含まれていなかったGraalですが、だれも文句いわなかったということで正式に削除されました。 https://bugs.openjdk.java.net/browse/JDK-8255616 ZGCの変更 JEPになってませんが、ZGCも改善されています。 https://malloc.se/blog/zgc-jdk17 OpenJDK OpenJDKに関する変更は、Apple Silicon対応の1件です。 JEP 391: macOS/AArch64 Port JEP 391: macOS/AArch64 Port M1などApple Siliconに対応しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む