如何使 ListView 加载 parent 和由 ForeignKey 关联的子项?

How can I make a ListView load parent and childs related by ForeignKey?

现在,我有一个工作的 ListView,它成功加载了 ListView 中的每个“parent”object,检索 parent 属性并在每个元素中显示它们。由于我试图遵循 Android's Model-View-ViewModel architecture patterns,我正在使用 RoomDb、存储库 class 和 ViewModel class(viewmodel class 包含 parent 和children;我想知道这是否是反模式;但这可能是另一个问题的主题)。

无论如何,由于我必须查询检索包含 POJO 列表的 LiveData object,这将我可以在 ListView 上显示的数据限制为仅 parent 的字段,因为 POJO 只包含 parent table 的字段;并查询每个 parentID 以在某种循环中检索所有 child objects 出于某种原因听起来不太好。因此,在互联网上搜索此内容时,我发现(但我不记得在哪里)这种制作某种“关系实体”的方法同时包含 parent 和 children:

import androidx.room.Embedded;
import androidx.room.Relation;

import java.io.Serializable;
import java.util.List;

public class ParentWithChildren implements Serializable {
    @Embedded public Parent parent;
    @Relation(
            parentColumn = "id",
            entityColumn = "parent_id"
    )
    public List<Child> children;
}

然后,在我的 ParentDao 中,我添加了这个:

// XXX The old DAO searcher that already works for retrieving
//     just the parent POJOs
@Query("SELECT * FROM parent WHERE parent_field1 = :search OR parent_field2 = :search_no_accents")
LiveData<List<Parent>> searchParents(String search, String search_no_accents);

// XXX The new method I'd like to create
@Transaction
@Query("SELECT * FROM parent WHERE parent_field1 = :search OR parent_field2 = :search_no_accents")
LiveData<List<ParentWithChildren>> searchParentsWithChildren(String search, String search_no_accents);

并且,在我的存储库中 class:

public LiveData<List<ParentWithChildren>> searchParentsWithChildren(String search, String search_no_accents) {
    return parent_dao.searchParentsWithChildren(search, search_no_accents);
}

...这是我的 ParentViewModel 从存储库中检索列表的方法:

public LiveData<List<ParentWithChildren>> searchParentsWithChildren(String search) {
    // I remove accents from user search with and ad-hoc method
    String search_no_accents = removeAccents(search);
    return repository.searchParentsWithChildren(search, search_no_accents);
}

在我的 MainActivity 中,我有一个处理用户搜索的 SearchView:

SearchView search_view = findViewById(R.id.searchView);
search_view.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String query) {
        if (query.equals("")) {
            loadFullListFragment();
        } else {
            search_results = parent_viewmodel.searchParents(query);
            //search_with_children = parent_viewmodel.searchParentsWithChildren(query);

            observeSearchResults(search_results);
            //observeSearchResults(search_with_children);
        }
        return true;
    }
});

我的 observeSearchResults 方法,也在 MainActivity 中,根据结果列表的大小加载适当的片段:

private void observeSearchResults(LiveData<List<Parent>> parents_results) {
//private void observeSearchResults(LiveData<List<ParentWithChildren>> parents_with_children) {
    parents_results.observe(this, new Observer<List<Parent>>() {
    //parents_with_children.observe(this, new Observer<List<ParentWithChildren>>() {
        @Override
        //public void onChanged(List<ParentWithChildren> parents) {
        public void onChanged(List<Parent> parents) {
            if ( parents.size() > 1 ) {
                // ## List view of multiple results
                listFragment = ParentsListFragment.newInstance( (ArrayList)parents );
                transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragmentContainer, listFragment);
                transaction.addToBackStack(null);
                transaction.commit();
            }
            else if ( parents.size() > 0 ) {
                // TODO XXX This is kinda hacky
                loadParentProfileFragment(parents.get(0));
            } else {
                transaction = getSupportFragmentManager().beginTransaction();
                transaction.replace(R.id.fragmentContainer, insertFragment);
                transaction.addToBackStack(null);
                transaction.commit();
            }
        }
    });
}

嗯,你看到那 commented-out 行代码了吗? 这是我要介绍的新更改,以开始使用新的 ParentWithChildren 实体。 它们是 commented-out,因为它们正上方或下方的那些已经在努力在 ListView 中显示 parent 的 object。因此,当我尝试实施更改时,我 comment-out 旧的和“dis-comment-out”具有 ParentWithChildren 更改的那些。我必须更改这么多 classes 才能实现 一个更改 ...嗯...这里闻起来像 Shotgun Surgery...:-(

...好吧,长话短说,我尝试在加载 ListView 的片段中将之前的 List<Parent> 替换为 List<ParentWithChildren>,因此每个列表元素也将能够检索parent 相关 child object 并显示它们。但是,上次我尝试以这种方式实现 ParentWithChildren 实体时,它因以下异常而崩溃(这绝对让我大吃一惊):

java.lang.NoClassDefFoundError: Failed resolution of: com/mydomain/myapp/daos/ParentDao_Impl;

这个异常的Traceback从这个auto-generatedParentDao_Implement的某行开始,Traceback的下一行指向我的新Repository.searchParentsWithChildren方法。 我完全不知道是我做错了,还是怎么处理这个奇怪的故障。

我的 ArrayAdapter 以及我希望它变成的样子

这个片段显示了我当前工作的 ArrayAdapter,commented-out 行代码或多或少显示了我想要引入的更改:

public class ParentsArrayAdapter extends ArrayAdapter<Parent> {
//public class ParentsArrayAdapter extends ArrayAdapter<ParentWithChildren> {

    //public ParentsArrayAdapter(Context context, List<ParentWithChildren> parents) {
    public ParentsArrayAdapter(Context context, List<Parent> parents) {
        super(context, 0, parents);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup vgroup) {
        // Get the data item for this position
        Parent parent = getItem(position);
        //ParentWithChildren parent = getItem(position);
        // XXX TODO Load child attributes list

        // Check if an existing view is being reused, otherwise inflate the view
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext())
                   .inflate(R.layout.listview_parents, vgroup, false);
        }
        // Lookup view for data population
        TextView parentField1 = convertView.findViewById(R.id.list_parentField1);
        // Populate the data into the template view using the data object
        parentField1.setText(parent.getField1());
        //parentField1.setText(parent.parent.getField1());

        TextView parentField2 = convertView.findViewById(R.id.list_parentField2);
        parentField2.setText(parent.getField2());
        //pinyinText.setText(parent.parent.getField2());

        // TODO Load first 5-10 childs here (when ParentWithChildren works)
        TextView childrenCommaListField = convertView.findViewById(R.id.list_childsCommaList);
        /*
             ¿¿?? SebasSBM is confused
         */

        // Return the completed view to render on screen
        return convertView;
    }
}

在 PC 应用程序中,这就像做某种 sqlSELECT p.*, c.* FROM parent AS p INNER JOIN child as c ON p.id = c.parent_id WHERE bla bla bla 一样简单:但是对于 RoomDB,我感觉关系数据库的一切都复杂得多。

那么,如何让 ListView 在每个 parent 列表的元素中加载 children 的字段,使其看起来或多或少是这样的?感谢您的帮助:

sorry, I am terrible making examples sometimes

------------
parent1 ---> parent_field1 ---> child1, child2, child3
------------
parent2 ---> parent_field1 ---> child4, child5, child6

数据库的Table结构(或多或少)

Parent table

+------+--------+--------+-
|  ID  | field1 | field2 | [...]
+------+--------+--------+-
|      |        |        |
+------+--------+--------+-

Child table (relation One2Many by FK)

+------+-----------+--------+---------+-
|  ID  | parent_id | field1 | field2  | [...]
+------+-----------+--------+---------+-
|      |           |        |         |
+------+-----------+--------+---------+-

In a PC app, this would be as simple as doing some kind of sql SELECT p., c. FROM parent AS p INNER JOIN child as c ON p.id = c.parent_id WHERE bla bla bla: but with RoomDB I have the sensation everything with relational databases is way much more complicated.

您可以对房间执行此操作,只需拥有一个具有

的 POJO
@Embedded
Parent parent;
@Embedded
Child child;

如果有任何列名与您需要消除列歧义的列名相同,这会变得有点困难。你可以使用@Embedded(prefix = "a suitable prefix").

这是一个工作示例。

Parentclass/entity:-

@Entity
class Parent {
    public static final String PREFIX = "parent_";
    @PrimaryKey
    Long id=null;
    String name;

    Parent(){}
    @Ignore
    Parent(String name) {
        this.name = name;
    }
}

Childclass/entity:-

@Entity
class Child {
    public static final String PREFIX = "child_";
    @PrimaryKey
    Long id = null;
    long parent;
    String name;

    Child(){}
    @Ignore
    Child(long parent,String name) {
        this.parent = parent;
        this.name = name;
    }
}

加入 parent 与 child 的笛卡尔积的 POJO :-

class ParentChildCartesian {
    @Embedded(prefix = Parent.PREFIX)
    Parent parent;
    @Embedded(prefix = Child.PREFIX)
    Child child;
}

Dao 的(抽象 class 而不是接口,如此抽象....对于 dao 方法):-

@Dao
abstract class AllDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(Parent parent);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(Child child);

    @Query("SELECT parent.id AS " + Parent.PREFIX + "id, " +
            "parent.name AS " + Parent.PREFIX + "name, " +
            "child.id AS " + Child.PREFIX + "id," +
            "child.parent AS " + Child.PREFIX + "parent," +
            "child.name AS " + Child.PREFIX + "name " +
            " FROM parent JOIN child ON child.parent = parent.id"
    )
    abstract List<ParentChildCartesian> getParentChildCartesianList();
}

显然是一个合适的@Database class,这里是TheDatabase(不需要显示)。

最后,演示上面的使用:-

    db = TheDatabase.getInstance(this);
    dao = db.getAllDao();

    long p1 = dao.insert(new Parent("Parent1"));
    long p2 = dao.insert(new Parent("Parent2"));
    dao.insert(new Child(p1,"Child1"));
    dao.insert(new Child(p1,"Child2"));
    dao.insert(new Child(p1,"Child3"));
    dao.insert(new Child(p2,"Child4"));

    for(ParentChildCartesian pc: dao.getParentChildCartesianList()) {
        Log.d(
                "DBINFO",
                "Parent name is " + pc.parent.name + " id is" + pc.parent.id +
                        ". Child name is " + pc.child.name + " id is " + pc.child.id + " parent id is " + pc.child.parent);
    }

日志中的结果:-

D/DBINFO: Parent name is Parent1 id is1. Child name is Child1 id is 1 parent id is 1
D/DBINFO: Parent name is Parent1 id is1. Child name is Child2 id is 2 parent id is 1
D/DBINFO: Parent name is Parent1 id is1. Child name is Child3 id is 3 parent id is 1
D/DBINFO: Parent name is Parent2 id is2. Child name is Child4 id is 4 parent id is 2

但是,我认为您希望所有 children 的名字与 parent 排在一行中。这可以利用上面的 PJO,但是 GROUP 的数据允许使用 group_concat 聚合函数的不同查询,例如:-

@Query("SELECT parent.id AS " + Parent.PREFIX + "id, " +
        "parent.name AS " + Parent.PREFIX + "name, " +
        "child.id AS " + Child.PREFIX + "id," + /* note will be an arbitrary id */
        "child.parent AS " + Child.PREFIX + "parent," +
        "group_concat(child.name) AS " + Child.PREFIX + "name " +
        "FROM parent " +
        "JOIN child ON child.parent = parent.id " +
        "GROUP BY parent.id"
)
abstract List<ParentChildCartesian> getParentwithCSV();

与 :-

一起使用时
    for (ParentChildCartesian pc: dao.getParentwithCSV()) {
        Log.d(
                "DBINFO",
                "Parent is " + pc.parent.name+ " children are " + pc.child.name
        );
    }

日志包括:-

D/DBINFO: Parent is Parent1 children are Child1,Child2,Child3
D/DBINFO: Parent is Parent2 children are Child4

不用@Embedded,方便包含object.

的成员变量

所以考虑 POJO :-

class WithoutEmbedded {
    long parentId;
    String parentName;
    String childrenNameCSV;
    String childrenIdCSV;
}

您必须将成员变量名称与列名称相匹配,这样您就可以拥有,例如:-

@Query("SELECT parent.id AS parentId, " +
        "parent.name AS parentName, " +
        "group_concat(child.name) AS childrenNameCSV, " +
        "group_concat(child.id) AS childrenIdCSV " +
        "FROM parent " +
        "JOIN child ON child.parent = parent.id " +
        "GROUP BY parent.id"
)
abstract List<WithoutEmbedded> getParentsWithchildrenCSVandChildrenIdCSV();

使用:-

    for(WithoutEmbedded pc: dao.getParentsWithchildrenCSVandChildrenIdCSV()) {
        Log.d(
                "DBINFO",
                "Parent is " + pc.parentName + " children are " + pc.childrenNameCSV + " children ids are " + pc.childrenIdCSV
        );
    }

日志中的结果将是:-

D/DBINFO: Parent is Parent1 children are Child1,Child2,Child3 children ids are 1,2,3
D/DBINFO: Parent is Parent2 children are Child4 children ids are 4

简而言之,@Relation 通过对所有相关的 children 使用基础查询来实现相当于连接的功能,因此建议使用 @Transaction。

虽然@Embedded 期望查询结果具有与嵌入式classes 的成员变量名称相匹配的列,因此可以使用JOINS(包括过滤)。因为这一切都在一个查询中完成,所以不需要 @Transaction。

@Embedded 基本上是一种便利,因此可以通过手动包含成员变量来省略。 (同样不需要@Transaction)。

额外

重新评论。现在你想要什么更清楚了。简而言之,您的原始问题就是线索。您可以根据需要使用@Embedded 和@Realtion(如果我理解正确的话)。

例如来自一个工作示例:-

Mary 和 Fred 是 parents(由于几次测试运行而重复行)。

适配器应遵循 :-

class MyAdapter extends ArrayAdapter<ParentWithChildren> ....

其中 ParentWithChildren 是 @Embedded .... @Realation POJO 例如:-

class ParentWithChildren {
    @Embedded
    Parent parent;
    @Relation(
            entity = Child.class,
            parentColumn = Parent.COL_PARENT_ID,
            entityColumn = Child.COL_PARENT_MAP
    )
    List<Child> children;
}

Child 是(在示例的情况下):-

@Entity(tableName = Child.TABLE_NAME,
        foreignKeys = {
            @ForeignKey(
                    entity = Parent.class,
                    parentColumns = {Parent.COL_PARENT_ID},
                    childColumns = {Child.COL_PARENT_MAP},
                    onDelete = ForeignKey.CASCADE,
                    onUpdate = ForeignKey.CASCADE
            )
        }
)
class Child {
    public static final String TABLE_NAME = "child";
    public static final String COL_CHILD_ID = "childid";
    public static final String COL_PARENT_MAP = "parentmap";
    public static final String COL_CHILD_NAME = "childname";
    @PrimaryKey
    @ColumnInfo(name = COL_CHILD_ID)
    Long childId = null;
    @ColumnInfo(name = COL_PARENT_MAP)
    Long parentMap;
    @ColumnInfo(name = COL_CHILD_NAME, index = true)
    String childName;

    Child(){}
    @Ignore
    Child(String name, long parentMap) {
        this.childName = name;
        this.parentMap = parentMap;
    }
}

Parent:-

@Entity(tableName = Parent.TABLE_NAME)
class Parent {
    public static final String TABLE_NAME = "parent";
    public static final String COL_PARENT_ID = "parentid";
    public static final String COL_PARENT_NAME = "parentname";
    @PrimaryKey
    @ColumnInfo(name = COL_PARENT_ID)
    Long parentId=null;
    @ColumnInfo(name = COL_PARENT_NAME, index = true)
    String parentName;

    Parent(){}

    @Ignore
    Parent(String name) {
        this.parentName = name;
    }
}

@Dao class AllDao :-

@Dao
abstract class AllDao {
    @Insert
    abstract long insert(Parent parent);
    @Insert
    abstract long insert(Child child);

    @Transaction
    @Query("SELECT * FROM parent")
    abstract List<ParentWithChildren> getAllParentsWithChildren();
}

@Database TheDatabase 是:-

@Database(entities = {Parent.class,Child.class},version = TheDatabase.DATABASE_VERSION)
abstract class TheDatabase extends RoomDatabase {
    abstract AllDao getAllDao();

    public static final String DATABASE_NAME = "my.db";
    public static final int DATABASE_VERSION = 1;

    private static volatile TheDatabase instance = null;
    public static TheDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context,TheDatabase.class,DATABASE_NAME)
                    .allowMainThreadQueries()
                    .build();
        }
        return instance;
    }
}

适配器的布局 parentwithchildre.xml 最多 5 children :-

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/parent_name"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        >
    </TextView>
    <TextView
        android:id="@+id/child1"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        >
    </TextView>
    <TextView
        android:id="@+id/child2"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        >
    </TextView>
    <TextView
        android:id="@+id/child3"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        >
    </TextView>
    <TextView
        android:id="@+id/child4"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        >
    </TextView>
    <TextView
        android:id="@+id/child5"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        >
    </TextView>

</LinearLayout>

activity MainActity 是:-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDao dao;
    ListView lv1;
    MyAdapter adapter;
    List<ParentWithChildren> baseList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv1 = this.findViewById(R.id.lv1);
        db = TheDatabase.getInstance(this);
        dao = db.getAllDao();
        long p1 = dao.insert(new Parent("Mary"));
        long p2 = dao.insert(new Parent("Fred"));
        dao.insert(new Child("Tom",p1));
        dao.insert(new Child("Alice",p1));
        dao.insert(new Child("Jane",p1));
        dao.insert(new Child("Bob",p1));
        dao.insert(new Child("Susan",p2));
        dao.insert(new Child("Alan",p2));
        setOrRefreshAdapter();
        setOrRefreshAdapter();

    }

    private void setOrRefreshAdapter() {
        baseList = dao.getAllParentsWithChildren();
        if (adapter == null) {
            adapter = new MyAdapter(this,R.layout.parentwithchildren, baseList);
            lv1.setAdapter(adapter);
        } else {
            adapter.clear();
            adapter.addAll(baseList);
            adapter.notifyDataSetChanged();
        }
    }
}

最后也是大多数 MyAdapter :-

class MyAdapter extends ArrayAdapter<ParentWithChildren> {

    public MyAdapter(@NonNull Context context, int resource, @NonNull List<ParentWithChildren> objects) {
        super(context, resource, objects);
    }
    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        ParentWithChildren pwc = getItem(position);
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(
                    R.layout.parentwithchildren,parent,false
            );
            TextView parent_textview = convertView.findViewById(R.id.parent_name);
            parent_textview.setText(pwc.parent.parentName);
            TextView child1_textview = convertView.findViewById(R.id.child1);
            TextView child2_textview = convertView.findViewById(R.id.child2);
            TextView child3_textview = convertView.findViewById(R.id.child3);
            TextView child4_textview = convertView.findViewById(R.id.child4);
            TextView child5_textview = convertView.findViewById(R.id.child5);

           if(pwc.children.size() >= 1) {
               child1_textview.setText(pwc.children.get(0).childName);
           }
           if (pwc.children.size() >= 2) {
               child2_textview.setText(pwc.children.get(1).childName);
           }
           if (pwc.children.size() >=3) {
               child3_textview.setText(pwc.children.get(2).childName);
           }
           if (pwc.children.size() >= 4) {
               child4_textview.setText(pwc.children.get(3).childName);
           }
           if (pwc.children.size() >= 5) {
                    child5_textview.setText(pwc.children.get(4).childName);
           }
        }
        return convertView;
        //return super.getView(position, convertView, parent);
    }
}

因此,在 getView 中传递一个列表后,您会得到一个 ParentWithChild object,下面它被命名为 pwc,如下所示ParentWithChildren pwc = getItem(position);(即列表中第n个ParentWithChildren)

parent 的名字是 pwc.parent.parentName

children的名字是通过:-

获得的
 pwc.children.get(?).childName

在哪里?是 children 数的数字 0,但您会将值限制为观看次数 - 1.