对使用房间数据库填充 AutoCompleteTextView 建议感到困惑

confused about Populating AutoCompleteTextView Suggestions with a Room Database

我是新手,正在尝试在 JAVA 中的 android 应用程序中实现 autoComplete 函数(不是 Kotlin,因为我还没有了解它)与本教程:

link

用户可以在自动完成的文本视图中搜索城市名称,当他们键入时,建议会根据他们键入的内容出现在下拉列表中,例如,如果他们键入“N”,那么建议应显示“纽约”、“新城堡”等

这些建议应该预先填充了一个包含数千个城市信息的现有数据库(数据已经在里面),我将这个 city_db.db 文件保存在我的资产文件夹中。

我有几个问题:

  1. 在写My DAO.java class的时候应该包括我的查询命令,我只包括查询部分,因为我只需要从数据库中读取,不需要写入,所以我写了:
package com.example.roomtest;
import androidx.room.Dao;
import androidx.room.Query;
@Dao
public interface DAO {
    //this class contains methods that accesses the database
    @Query("SELECT * city_name FROM city")
}

但它只显示红线并告诉我“此处不允许注释”

而且我不确定这是否是正确的查询命令,它不应该像

"SELECT * city_name FROM city where like "what the user typed%" "?
  1. 我不确定我是否需要更多 classes 来实现这个自动完成功能, 现在我有这些 classes:

1.MainActivity.java

2.Entity.java

3.Dao.java

4.Database.java

我需要更多 classes 来实现这个自动完成功能吗?我想我错过了什么。

这是我在所有 class 中的代码:


package com.example.roomtest;
    import androidx.appcompat.app.AppCompatActivity;
    import android.os.Bundle;
    import android.widget.AutoCompleteTextView;
    import android.widget.EditText;
    
    public class MainActivity extends AppCompatActivity {
        private AutoCompleteTextView at;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            at = findViewById(R.id.att);
        }
    }

package com.example.roomtest;
    import android.content.Context;
    import androidx.room.Room;
    import androidx.room.RoomDatabase;
    
    @androidx.room.Database(entities = Entity.class,version = 1)
    //must include the entity associated with database
    public abstract class Database extends RoomDatabase {
        //must be abstract
        public abstract DAO dao();
        //must declare this
        //check if a database instance is existing
        //if not, create one and return it
        private static volatile Database db;
        static Database getDatabase(final Context context){
            if(db==null){
                synchronized (Database.class){
                    if(db == null){
                        db =Room.databaseBuilder(context.getApplicationContext(),
                                Database.class, "Database")
                                .createFromAsset("city_db.db").build();
                    }
                }
            }
            return db;
        }
    }
package com.example.roomtest;
    
    import androidx.room.ColumnInfo;
    import androidx.room.PrimaryKey;
    
    
    @androidx.room.Entity(tableName = "city")
    public class Entity {
        //define data types in this class
        @PrimaryKey
        public int id;
        @ColumnInfo(name = "city_name")
        public String name;
    
        public String state;
        public String country;
        public String lon;
        public String lat;
    }
package com.example.roomtest;
    import androidx.room.Dao;
    import androidx.room.Query;
    @Dao
    public interface DAO {
        //this class contains methods that accesses the database
        @Query("SELECT * city_name FROM city")
    }

我的 gradle 依赖项:

dependencies {
    //room components
    def room_version = "2.3.0"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    testImplementation "androidx.room:room-testing:$room_version"

    implementation 'androidx.appcompat:appcompat:1.3.1'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

这里还有我的数据库的属性:

很抱歉这么长post,但我对这个简单功能所需的所有工作太困惑了,如果有人知道这方面的好教程,请与我分享,谢谢。

您需要在列名称之间使用逗号(* 表示所有列,因此不需要 *,city_name)。

SELECT * FROM city.... 相当于,在你的情况下,说 SELECT id,city_name,state,country,lon,lat FROM city ....

虽然 SELECT *,city_name .... 在说 SELECT id,city_name,state,country,lon,lat,city_name FROM city ....

虽然有效SQL为什么两次得到相同的值(修辞)?此外,Room 应该使用哪个(即使它们相同)city_name(rehtorical)? 这是模棱两可的,有可能导致错误(我认为 room 会处理这个并选择最后一个)

你想要:-

@Dao
public interface DAO {
    //this class contains methods that accesses the database
    @Query("SELECT * FROM city WHERE city_name LIKE :cityName")
    List<Entity> getCity(String cityName); //<<<< The method that is called from the code
}

这将 return 一个列表,即实体对象的列表

或者:-

@Dao
public interface DAO {
    //this class contains methods that accesses the database
    @Query("SELECT city_name FROM city WHERE city_name LIKE :cityName")
    List<String> getCityName(cityName);
}
  • 这是一个 return 列表,其中每个字符串都是 city_name 的
  • 你可以两者兼得

调用时,如果传递的字符串 (cityName) 是“New%”,那么您将得到 New York、Newcastle 等(即 % 是 1 个或多个字符的通配符 _ 是单个字符)

例子

使用你的代码,稍作改动,上面的代码编译成功 运行s OK。

上面建议的更改已经包含(两者)所以使用的 DAO class 是 :-

@Dao
public interface DAO {

    //this class contains methods that accesses the database
    @Query("SELECT * FROM city WHERE city_name LIKE :cityName")
    List<Entity> getCitiesByName(String cityName);

    @Query("SELECT city_name FROM city WHERE city_name LIKE :cityName")
    List<String> getCityNamesByName(String cityName);
}

我对 数据库 class 进行了一些更改,以 a) 允许在主线程上 运行ning(添加 .allowMainThreadQueries()) b) 不根据 :-

从资产文件创建(注释掉 .createFromAsset("city_db.db")
@androidx.room.Database(entities = Entity.class,version = 1)
//must include the entity associated with database
public abstract class Database extends RoomDatabase {
    //must be abstract
    public abstract DAO dao();
    //must declare this
    //check if a database instance is existing
    //if not, create one and return it
    private static volatile Database db;
    static Database getDatabase(final Context context){
        if(db==null){
            synchronized (Database.class){
                if(db == null){
                    db = Room.databaseBuilder(context.getApplicationContext(),
                            Database.class, "Database")
                            //.createFromAsset("city_db.db")
                            .allowMainThreadQueries()
                            .build();
                }
            }
        }
        return db;
    }
}

使用的 MainActivity 是 :-

public class MainActivity extends AppCompatActivity {

    Database db;
    DAO dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        db = Database.getDatabase(this);
        dao = db.dao();
        dao.getCitiesByName("Somewhere%");
        dao.getCityNamesByName("Nowhere%");
    }
}

因此,即使没有对结果进行任何处理(数据库中没有数据),上面的操作也会打开数据库(不使用 .createFromAsset 创建它)。

您可以在 Android Studio 中根据 App Inspection(又名数据库检查器)查看此内容:-

像你这样编码,除非资产文件的副本在按照建议修改时有效。

正在进行的问题

根据您的屏幕截图,您可能会在 .build().

方面遇到持续的问题

房间期望复制的数据库与从实体派生的模式完全匹配(对于在@Database 注释的实体参数中定义的实体)。

作为一个示例,屏幕截图显示城市名称位于名为 name 的列(字段)中,而在您的实体中则是等效的列名称,按照实体实体是 city_name。房间构建将失败。同样,coord.lon 和 coord.lat 是经度和纬度。

此外,列定义(列类型和约束,例如 NOT NULL)也必须匹配,否则 Room 构建将失败。

因此,您必须确保模式匹配。这可能需要使用一种可用的工具来相应地转换数据库(由于 Room 施加的限制,转换正在复制的数据库可能比尝试匹配现有数据库容易得多)。

起点是当您使用包含实体的 valid/usable @Database 编译时 Room 生成的 SQL。

例如在您的情况下,在 class 中生成的 java(从 Android Studio 中的 Android 视图可见)与具有 @ 的 class 命名相同数据库注释,但后缀为 _IMPL。

因此,由于您的@Database class 被命名为数据库,因此将会有一个 class Database_IMPL.

在 class 中会有一个方法 createAllTables 它有 SQL 用于所有 table(任何 SQL 可以忽略与 room_ 的交易)。

例如你的会是这样的:-

您很可能需要使用 SQLite 工具转换数据库,例如 DB Browser for SQLite、SQlite Studio、DBeaver、Navicat For SQlite 以使用模式。

粗略地说,使用从生成的java中获得的SQL创建table,然后复制数据。

转换本身会非常简单。首先创建 city_db.db 文件的副本。

打开其中一个(根据您的喜好,在您喜欢的 SQLite 工具中进行演示

  • 我使用 Navicat 并使用查询生成原始示例(只有几个国家))按照 :-

    CREATE TABLE IF NOT EXISTS city (id INTEGER PRIMARY KEY, name TEXT,state TEXT, country TEXT, `coord.lon` REAL, `coord.lat`);
    INSERT INTO city VALUES
        (3904809,'Santa Rita','','BO',-63.3499,-17.966),
        (3904890,'Santa Elenta','','BO',-64.7833,-20.5496)
    ;
    
    - don't use the above
    
    

然后使用下面的 SQL 注意这将删除原来的 table (不想要超大的资产文件)。 如果 运行 第二次,这将失败,因为原始列名已被有效覆盖,因此原始列名不存在。

DROP TABLE IF EXISTS room_city;

/*As copied from the App's generated java */
/* BUT table name changed to room_city from city */
CREATE TABLE IF NOT EXISTS `room_city` (`id` INTEGER NOT NULL, `city_name` TEXT, `state` TEXT, `country` TEXT, `lon` TEXT, `lat` TEXT, PRIMARY KEY(`id`));
INSERT INTO room_city SELECT id,name AS city_name, state, country, `coord.lon` AS lon, `coord.lat` AS lat FROM city;
/* To clean up so as to not include the original in the asset
    you don't want the asset hold twice the data that it needs to
    !!!!!NOTE!!!!! you should use a copy of the original city database
*/
DROP TABLE IF EXISTS city;
ALTER TABLE `room_city` RENAME TO `city`;
VACUUM;
/* Below not needed but good to check all is as expected */
SELECT * FROM city;
SELECT * FROm sqlite_master;
  • 请注意,这假设原始数据库与屏幕截图一致。

  • VACUUM 之后的第一个 SELECT 显示:-

  • 第二个显示架构:-

    • 它与 Room 期望的架构相匹配

应关闭数据库和连接(打开检查并再次关闭以仔细检查要复制的文件是否已保存)。

然后将文件复制到 assets 文件夹(您可能需要创建它)确保它被命名(可以重命名)city_db.db . :-

在使用上述演示的情况下,数据库 class 已更改为现在包含行 .createFromAsset("city_db.db") // <<<<< reinstated

MainActivity 已更改为:-

public class MainActivity extends AppCompatActivity {

    Database db;
    DAO dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        db = Database.getDatabase(this);
        dao = db.dao();

        for(Entity city: dao.getCitiesByName("%")) { //* <<<<<< changed to show all as only 2 cities in demo
            Log.d("CITYINFO-1","CityName is " + city.name + " state/county is " + city.state + " etc.....");
        }
        for(String cityname: dao.getCityNamesByName("%")) { //* <<<<<< changed to show all as only 2 cities in demo
            Log.d("CITYINFO-2","CityName is " + cityname);
        }
        dao.getCityNamesByName("New%");
    }
}

当 运行 日志显示时:-

2021-10-23 15:44:37.480 D/CITYINFO-1: CityName is Santa Rita state/county is  etc.....
2021-10-23 15:44:37.480 D/CITYINFO-1: CityName is Santa Elenta state/county is  etc.....
2021-10-23 15:44:37.482 D/CITYINFO-2: CityName is Santa Rita
2021-10-23 15:44:37.482 D/CITYINFO-2: CityName is Santa Elenta

所以数据库可以正常工作。

如果不转换会怎样?

再次根据屏幕截图,应用程序失败并显示:-

2021-10-23 15:50:53.826 6345-6345/a.a.so69683821cities E/AndroidRuntime: FATAL EXCEPTION: main
    Process: a.a.so69683821cities, PID: 6345
    java.lang.RuntimeException: Unable to start activity ComponentInfo{a.a.so69683821cities/a.a.so69683821cities.MainActivity}: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: city(a.a.so69683821cities.Entity).
     Expected:
    TableInfo{name='city', columns={country=Column{name='country', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, city_name=Column{name='city_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, lon=Column{name='lon', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, state=Column{name='state', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, lat=Column{name='lat', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
     Found:
    TableInfo{name='city', columns={country=Column{name='country', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, name=Column{name='name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, state=Column{name='state', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, coord.lat=Column{name='coord.lat', type='', affinity='1', notNull=false, primaryKeyPosition=0, defaultValue='null'}, coord.lon=Column{name='coord.lon', type='REAL', affinity='4', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)

其中 EXPECTED 是 Room 根据实体期望的架构 class 但是 Room FOUND 不匹配来自资产的架构。