728x90

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);
}

블로그 이미지

Link2Me

,