【问题标题】:How can I make a ListView load parent and childs related by ForeignKey?如何使 ListView 加载与 ForeignKey 相关的父子对象?
【发布时间】:2021-10-25 13:06:43
【问题描述】:

现在,我有一个有效的 ListView,它成功地加载了 ListView 中的每个“父”对象,检索父属性并在每个元素中显示它们。由于我试图关注Android's Model-View-ViewModel architecture patterns,我正在使用 RoomDb、一个 Repository 类和一个 ViewModel 类(viewmodel 类包含父子类的逻辑;我想知道这是否是反模式;但这可能是一个主题另一个问题)。

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

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

而且,在我的 Repository 类中:

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

嗯,你看到那些被注释掉的代码行了吗?这是我要引入的新变化,以开始使用新的 ParentWithChildren 实体。 它们被注释掉了,因为它们正上方或下方的那些已经在 ListView 中显示父对象。因此,当我尝试实施更改时,我注释掉旧的那些和“dis-comment-out”那些有ParentWithChildren变化的。我必须更改这么多类来实现一个单一的更改...嗯...这里闻起来像Shotgun Surgery...:-(

...嗯,长话短说,我尝试在加载 ListView 的 Fragment 中将之前的 List&lt;Parent&gt; 替换为 List&lt;ParentWithChildren&gt;,因此每个列表元素也将能够检索与父相关的子对象并显示他们。但是,上次我尝试以这种方式实现 ParentWithChildren 实体时,它因异常而崩溃(这绝对让我大吃一惊):

java.lang.NoClassDefFoundError: 解析失败:com/mydomain/myapp/daos/ParentDao_Impl$5;

此异常的 Traceback 始于此自动生成的 ParentDao_Implement 中的某行,并且 Traceback 的下一行指向我的新 Repository.searchParentsWithChildren 方法。 我完全不知道我是否做错了,或者如何处理这个奇怪的失败。

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

这个 sn-p 显示了我当前工作的 ArrayAdapter,以及注释掉的代码行,或多或少地显示了我想要引入的更改:

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 在每个父列表的元素中加载子字段,所以它或多或少看起来像这样?任何帮助表示赞赏:

sorry, I am terrible making examples sometimes

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

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

Parent table

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

Child table (relation One2Many by FK)

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

【问题讨论】:

  • 我可能错了,但我知道这不会像这样工作。您需要为 ParentWithChildren 创建另一个实体(表)并通过 ParentWithChildrenDao 与他们合作。
  • 那么,如果我理解你的话,这意味着新的“交易-父-子”实体也需要自己的道,先生?
  • 我很迷茫你想要实现什么以及你的问题是什么:D 为了对你的知识做一个小小的澄清,这里解释了为什么 Room 不像普通的 SQL 数据库developer.android.com/training/data-storage/room/…跨度>
  • 简而言之,我的问题是我只知道如何获取从单个表中检索数据行的 POJO,但我之前从未尝试过使用这些“事务”实体之一。而且我担心让 ListView 的 ArrayAdapter 访问每个 Parent 1 by 1 的 DAO 以检索每个父母的孩子将是一个糟糕的电话......
  • 欧。现在我明白了。我从一开始就没有遇到过这种情况,因为 Room 不支持这种方法。但现在看来这是可能的。我想这篇文章会对你有所帮助。它不足以成为一个答案,所以只是作为评论享受它;)medium.com/androiddevelopers/…

标签: java android relational-database android-room


【解决方案1】:

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

你可以用房间来做这个,只要有一个 POJO 有

@Embedded
Parent parent;
@Embedded
Child child;

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

这是一个工作示例。

类/实体:-

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

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

Child 类/实体:-

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

父子连接的笛卡尔积的POJO:-

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

Dao 的(在抽象类而不是接口中,所以抽象 .... 对于 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 类,这里是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

但是,我认为您希望所有孩子的名字与父母在一行中。这可以利用上面的 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,它方便地包含了对象的成员变量。

所以考虑 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 相当于通过对所有相关子项使用基础查询来执行连接,因此建议使用 @Transaction。

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

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

附加

重新计算。现在你想要什么更清楚了。简而言之,您最初的问题是线索。您可以使用@Embedded 和@Realtion 来满足您的需求(如果我理解正确的话)。

例如从一个工作示例:-

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

适配器应该是这样的:-

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

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

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

父母:-

@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 类 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 个孩子:-

<?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>

活动 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 中传递一个 List 后,您会得到一个 ParentWithChild 对象,在它下面命名为 pwc,如 ParentWithChildren pwc = getItem(position);(即 List 中的第 n 个 ParentWithChildren)

父母的名字是 pwc.parent.parentName

孩子的名字是通过以下方式获得的:-

 pwc.children.get(?).childName

在哪里?是孩子数量的数字 0,但您会将值限制为视图数量 - 1。

【讨论】:

  • 我几分钟前才开始查看这个答案。那么,拥有一个带有@Embedded Parent parent;@Embedded ArrayList&lt;Child&gt; childs; 的 POJO 不是一种选择吗?现在我将尝试按照您的方式实现它,看看会发生什么......
  • 我最近编辑了我的问题,所以我想要完成的事情更容易理解。只需注意我的ParentDao 的sn-p 中的XXX cmets 即可查看更改。谢谢。
  • 我在ListViewArrayAdapter 中添加了代码 sn-p,它显示了我尝试完成的工作。它已经适用于在每个列表元素中加载每个父级,我正在尝试弄清楚如何用每个父级的子对象来提供这个 ArrayAdapter。
  • 感谢您的回答,我注意到我或多或少走在正确的道路上,除了我必须将新方法放在专用的 DAO 中。那么,这种“复合 DAO”肯定是抽象类而不是接口,对吧?正如您向我展示的那样,我实现了新的 DAO,并且效果非常好。非常感谢。但是,我不需要添加带有名称的 @ColumnInfos。
猜你喜欢
  • 2011-04-19
  • 2014-08-20
  • 1970-01-01
  • 1970-01-01
  • 2020-04-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多