Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:267)
at androidx.room.RoomDatabase.query(RoomDatabase.java:323)
at androidx.room.util.DBUtil.query(DBUtil.java:83)
RoomDatabase 를 사용하는 방법에 대해 <오준석의 생존코딩> Morden Android 유투브 강좌를 듣고 구글링을 해서 내용을 좀 더 알게된 걸 적어둔다.
Room은 구글에서 만든 공식 ORM(Object-relational mapping)이라고 할 수 있으며 여러가지 강력한 기능들을 지원하고 있다. Room 지속성 라이브러리는 SQLite를 완벽히 활용하면서 강력한 데이터베이스 액세스를 지원하는 추상화 계층을 SQLite에 제공한다.
보다 자세한 내용은 https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0 를 참조한다.
위 URL 마지막에 나오는 https://github.com/googlecodelabs/android-room-with-a-view 자료를 받아서 테스트 해본다.
Project build.gradle
buildscript { ext.kotlin_version = '1.3.71' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.6.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } }
allprojects { repositories { google() jcenter() } }
task clean(type: Delete) { delete rootProject.buildDir }
|
앱 build.gradle 추가 사항
Google I/O 2018에서는 AndroidX 패키지를 소개하였다. 하지만 아직 28.x까지는 기존 패키지를 사용할 수 있다.
dependencies { def lifecycle_version = "2.2.0"
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.room:room-runtime:2.2.5' annotationProcessor 'androidx.room:room-compiler:2.2.5'
// https://developer.android.com/jetpack/androidx/releases/lifecycle?hl=ko // Lifecycle components (ViewModel and LiveData) implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" }
|
activitity_main.xml 파일 만들기
https://www.youtube.com/watch?v=pG6OkJ3rSjg 동영상을 보는게 좋다.
Todo.java
import androidx.room.Entity; import androidx.room.PrimaryKey;
// @Entity 어노테이션이 지정된 클래스는 데이터베이스의 테이블을 정의하는데 사용된다. @Entity public class Todo { // 1. 엔티티 클래스 만들기 // 먼저 데이터 구조를 정의할 엔티티를 클래스 형태로 만들어 준다.
@PrimaryKey(autoGenerate = true) private int id; private String title;
public Todo(String title) { this.title = title; }
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
@Override public String toString() { return "Todo{" + "id=" + id + ", title='" + title + '\'' + '}'; } }
|
TodoDao.java
import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; import androidx.room.Update;
import java.util.List;
@Dao public interface TodoDao { // 2. DAO 인터페이스 만들기
@Query("SELECT * FROM Todo") LiveData<List<Todo>> getAll();
@Insert(onConflict = OnConflictStrategy.IGNORE) void insert(Todo todo);
@Update void update(Todo todo);
@Delete void delete(Todo todo);
@Query("DELETE FROM Todo") void deleteAll();
}
|
AppDatabase.java
import android.content.Context;
import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
@Database(entities = {Todo.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { // 3. 데이터베이스 클래스 만들기 // https://webcoding.tistory.com/ 참조하면 좋은 자료 많다.
public abstract TodoDao todoDao(); // Dao 인터페이스 private static volatile AppDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4; static final ExecutorService databaseWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
// 인스턴스를 생성하여 반환 public static AppDatabase getInstance(Context context){ if(INSTANCE == null){ synchronized (AppDatabase.class){ if(INSTANCE == null){ INSTANCE = Room.databaseBuilder(context.getApplicationContext(),AppDatabase.class,"Todo.db") //.allowMainThreadQueries() .build(); } } } return INSTANCE; } }
|
MainViewModel.java
import android.app.Application; import android.os.AsyncTask;
import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData;
import java.util.List;
public class MainViewModel extends AndroidViewModel { private AppDatabase db;
public MainViewModel(@NonNull Application application) { super(application); db = AppDatabase.getInstance(application); // 데이터베이스 접근 인스턴스 사용하기 }
public LiveData<List<Todo>> getAll(){ return db.todoDao().getAll(); }
public void insert(Todo todo){ new InsertAsyncTask(db.todoDao()).execute(todo); }
private static class InsertAsyncTask extends AsyncTask<Todo,Void, Void> { private TodoDao mTodoDao;
public InsertAsyncTask(TodoDao todoDao) { this.mTodoDao = todoDao; }
@Override protected Void doInBackground(Todo... todos) { mTodoDao.insert(todos[0]); return null; } } }
|
MainActivity.java
viewModel = ViewModelProviders.of(this).get(MainViewModel.class);
ViewModelProviders.of()로 초기화하던 방식이 2.2.0에서 deprecated 되었으므로 아래와 같이 수정한다.
import android.os.Bundle; import android.widget.EditText; import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelStore; import androidx.lifecycle.ViewModelStoreOwner;
public class MainActivity extends AppCompatActivity implements ViewModelStoreOwner { private EditText mTodoEditText; private TextView mResultTextView;
private ViewModelProvider.AndroidViewModelFactory viewModelFactory; private ViewModelStore viewModelStore = new ViewModelStore(); private MainViewModel viewModel;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
mTodoEditText = findViewById(R.id.todo_edit); mResultTextView = findViewById(R.id.result_text);
if(viewModelFactory == null){ viewModelFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()); } viewModel = new ViewModelProvider(this,viewModelFactory).get(MainViewModel.class);
// UI 갱신 viewModel.getAll().observe(this, todos -> { mResultTextView.setText(todos.toString()); });
// 버튼 클릭시 DB에 insert findViewById(R.id.add_btn).setOnClickListener(v -> { viewModel.insert(new Todo(mTodoEditText.getText().toString())); }); }
@Override protected void onDestroy() { super.onDestroy(); viewModelStore.clear(); }
@Override public ViewModelStore getViewModelStore(){ return viewModelStore; } }
|
<오준석의 생존코딩 모던 안드로이드> 유투브 강좌로 부족한 부분은 https://github.com/tazihad/Android---Room-ViewModel-LiveData-RecyclerView-MVVM---Example 에 나오는 자료를 받고, 관련 유투브 강좌 https://www.youtube.com/watch?v=ARpn-1FPNE4 를 보면 도움이 될 것이다.
유투브 강좌를 듣고 따라한 코딩 소스를 첨부한다.
notesqlite-1.zip
https://guides.codepath.com/android/Room-Guide 를 보면 Room 사용법에 대해 조금 더 이해하는데 도움된다.
notesqlite-2.zip
@Entity(tableName = "note_table", indices = @Index(value = {"title"}, unique = true)) public class Note {
@PrimaryKey(autoGenerate = true) private int id; private String title; // 중복 입력 체크 private String description; private int priority;
public Note(String title, String description, int priority) { this.title = title; this.description = description; this.priority = priority; } }
|
@Dao public interface NoteDao {
// 중복 값 입력시 새로운 값으로 변경됨. @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(Note note);
// 기존에 존재하는 제목 값으로 변경시 무시 @Update(onConflict = OnConflictStrategy.IGNORE) void update(Note note);
@Delete void delete(Note note);
@Query("DELETE FROM note_table") void deleteAllNotes();
@Query("SELECT * FROM note_table ORDER BY priority DESC") LiveData<List<Note>> getAllNotes();
}
|
중복 입력 체크 예시
출처 : https://stackoverflow.com/questions/46916388/android-room-inserts-duplicate-entities
@Entity(tableName = "cameras", indices = [Index(value = ["accountId","dvrId"], unique = true)])
public class CameraEntity {
@PrimaryKey(autoGenerate = true)
private int id;
private Integer accountId;
private Integer dvrId;
private String vendorId;
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<CameraEntity> values);
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
출처 : https://developer.android.com/training/data-storage/room#java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}