问题 编程一对多关系


所以我很惊讶在谷歌和stackoverflow上搜索不会返回更多结果。

在OO编程中(我使用的是java),你如何正确地实现一对多关系?

我上课了 Customer 和班级 Job。我的申请是为一家为客户完成工作的虚构公司。我目前的实施是这样的 Job 上课没有任何关系 Customer 上课时,完全没有提及它。该 Customer class使用集合和方法来保存,检索和修改有关已为客户分配和/或完成的作业的信息。

问题是,如果我想知道哪个客户是特定的,该怎么办? Job 已经完成?我只发现这篇文章是相关的: http://www.ibm.com/developerworks/webservices/library/ws-tip-objrel3/index.html

根据作者的实施,我会让 Job 构造函数拿一个 Customer 参数,并存储它,以便我可以检索它。但是,我完全不能保证这个模型可以 一贯。没有为工作的相关客户设置工作作为该工作不适用的客户,并为其他人完成的客户添加工作。任何有关这方面的帮助将不胜感激。


8700
2018-04-10 09:52


起源



答案:


没有100%可靠的方法来维护完整性。

通常采用的方法是使用一种方法来构建关系,并在同一方法中构建另一个方向。但是,正如你所说,这并不能阻止任何人搞乱它。

下一步是使一些方法可以访问包,这样至少与你的无关的代码不能破坏它:

class Parent {

  private Collection<Child> children;

  //note the default accessibility modifiers
  void addChild(Child) {
    children.add(child);
  }

  void removeChild(Child) {
    children.remove(child);
  }
}

class Child {

   private Parent parent;
   public void setParent(Parent parent){
     if (this.parent != null)
       this.parent.removeChild(this);
     this.parent = parent;
     this.parent.addChild(this);
   }
}

实际上,您不会经常在课堂上模拟这种关系。相反,您将在某种存储库中查找所有子项以查找父项。


9
2018-04-10 11:02



谢谢你给出了一个非常好的答案。我会接受,但我会把问题保持开放一段时间,这样可能会有更多的答案。 - MarioDS
在Parent中没有拼写错误,它有两个addChild()方法 - powder366
@ powder366好抓。纠正。 - Joeri Hendrickx
@JoeriHendrickx存储库中的对象是什么样的?我想它会像 ParentChild{Parent p, Collection<Child> c} 和 Repository{ Collection<ParentChild> pc; returnParents(Child c){...}; returnChildren(Parent p){...} }。 - Slazer
@Slazer就像Repository {Collection <Children> getAllForParent(Parent parent)} - Joeri Hendrickx


答案:


没有100%可靠的方法来维护完整性。

通常采用的方法是使用一种方法来构建关系,并在同一方法中构建另一个方向。但是,正如你所说,这并不能阻止任何人搞乱它。

下一步是使一些方法可以访问包,这样至少与你的无关的代码不能破坏它:

class Parent {

  private Collection<Child> children;

  //note the default accessibility modifiers
  void addChild(Child) {
    children.add(child);
  }

  void removeChild(Child) {
    children.remove(child);
  }
}

class Child {

   private Parent parent;
   public void setParent(Parent parent){
     if (this.parent != null)
       this.parent.removeChild(this);
     this.parent = parent;
     this.parent.addChild(this);
   }
}

实际上,您不会经常在课堂上模拟这种关系。相反,您将在某种存储库中查找所有子项以查找父项。


9
2018-04-10 11:02



谢谢你给出了一个非常好的答案。我会接受,但我会把问题保持开放一段时间,这样可能会有更多的答案。 - MarioDS
在Parent中没有拼写错误,它有两个addChild()方法 - powder366
@ powder366好抓。纠正。 - Joeri Hendrickx
@JoeriHendrickx存储库中的对象是什么样的?我想它会像 ParentChild{Parent p, Collection<Child> c} 和 Repository{ Collection<ParentChild> pc; returnParents(Child c){...}; returnChildren(Parent p){...} }。 - Slazer
@Slazer就像Repository {Collection <Children> getAllForParent(Parent parent)} - Joeri Hendrickx


也许你没想到复杂(和零代码)的答案,但是 没有解决方案来按照您的意图构建您的防弹API。并不是因为范例(OO)或平台(Java),而是因为你做了错误的分析。在交易世界中(每个系统都能模拟现实生活中的问题及其随时间的演变) 是交易的)这段代码 永远不会破裂 在某一点:

// create
Job j1 = ...
Job j2 = ...
...
// modify
j1.doThis();
...

// access
j2.setSomeProperty(j1.someProperty);

因为当时 j1.someProperty 访问, j1 和 j2 甚至不存在:)

TL; DR

对此的答案很长 不变性,它还介绍了概念 生命周期 和 交易。所有其他答案告诉你 怎么做 它,而不是我想要概述 为什么。一对多关系有两个方面

  1. 有很多
  2. 属于

只要客户,您的系统就是一致的 A 有工作 B, 工作 B属于客户 A。您可以通过多种方式实现此目的,但这必须在a中实现 交易,即由简单的复杂动作组成,系统必须是 unavailble 直到事务完成执行。这看起来太抽象而且与你的问题无关吗?不,它不是:)事务系统确保客户端只有在所有这些对象都在的情况下才能访问系统的对象 有效的国家因此,只有整个系统是一致的。从其他答案中,您可以看到需要解决的处理量 一些 问题,所以保证是有代价的: 性能。这就是为什么Java(和其他通用OO语言)无法解决您的问题的简单解释 盒子外面

当然,OO语言可用于模拟a 交易世界 并且访问它,但必须特别小心,必须施加一些约束,并且客户端开发人员需要特殊的编程风格。通常,事务系统提供两个命令: 搜索 (又名查询)和 。查询的结果是 一成不变:它是系统拍摄时的一张照片(即副本),修改照片显然对现实世界没有任何影响。如何修改系统?平时

  1. 如果/需要,锁定系统(或部分系统)
  2. 找到一个对象:返回一个 复制 (照片)可以在本地读取和写入的真实对象
  3. 修改本地副本
  4. 提交修改后的对象,即让系统根据提供的输入更新其状态
  5. 丢弃对(现在无用的)本地对象的任何引用:系统已更改已更改,因此本地副本不是最新的。

(顺便说一句,你能看到生命周期的概念如何应用于本地和远程对象吗?)

你可以去 SetS, final 修饰符等,但在你引入交易和不变性之前,你的设计会有一个缺陷。通常,Java应用程序由数据库提供支持,数据库提供事务功能,并且DB通常与ORM(例如Hibernate)耦合以编写面向对象的代码。


4
2018-04-10 16:28



我完全读了你的答案,我理解你所说的大部分内容。这很有意思,我希望我的学校能像你现在所做的那样向我们解释更多的事情。事实上,通常应用程序确实有数据库,但我们只是刚刚在学校的数据库中引入,他们为我们加载了大量工作,我没有时间深入研究它们/目前正在试验它们。感谢您在这里的贡献。 - MarioDS
我只想指出设计阶段的重要性:您可以在给定一组资源(时间,$$$,知识)的情况下以不同方式对系统建模,但每个解决方案 将有其局限性 而且你必须明确它们,因为 即使是最简单的也可能是最好的,但你必须仔细概述要求,限制和范围:) - Raffaele


您可以使用a确保没有重复项  实施就像 HashSet的 而不是使用其他数据结构。 而不是添加 工作 对于客户,在Job类中创建一个具有私有构造函数的最终内部类。这确保了包装器内部类只能由作业对象创建。让你的Job构造函数接受 JOBID 和客户作为参数。为了保持一致性 - 如果客户是Null抛出异常,则不应创建虚拟作业。

 客户的方法,检查是否 工作 包裹着 JobUnit如果没有抛出异常,则具有与其自己的id相同的客户ID。

在Job类中替换客户时删除 JobUnit 使用Customer类提供的方法并将其自身添加到新客户并将客户引用更改为新传递的客户。 这样你可以更好地推理你的代码。

这是您的客户类可能是什么样子。

public class Customer {

    Set<JobUnit> jobs=new HashSet<JobUnit>();    
    private Long id;
    public Customer(Long id){        
        this.id = id;
    }

    public boolean add(JobUnit unit) throws Exception{
       if(!unit.get().getCustomer().id.equals(id))
           throw new Exception(" cannot assign job to this customer");
        return jobs.add(unit);
    }

     public boolean remove(JobUnit unit){
        return jobs.remove(unit);
    }

    public Long getId() {
        return id;
    }

}

工作班:

public class Job {
Customer customer;
private Long id;

最终JobUnit单位;

public Job(Long id,Customer customer) throws Exception{
    if(customer==null)
        throw new Exception("Customer cannot be null");
    this.customer = customer; 
   unit= new JobUnit(this);       
    this.customer.add(unit);
}

public void replace(Customer c) throws Exception{      
    this.customer.remove(unit);
    c.add(unit);
    this.customer=c;
}

public Customer getCustomer(){
    return customer;
}

/**
 * @return the id
 */
public Long getId() {
    return id;
}

public final class JobUnit{
    private final Job j;


    private JobUnit(Job j){
        this.j = j;

    }
    public Job get(){
        return j;
    }
 }
}

但有一点我很好奇,为什么你甚至需要为客户对象添加工作? 如果要检查的是要查看哪个客户已分配到哪个作业,只需检查作业即可获得该信息。一般来说,除非不可避免,否则我尽量不创建循环引用。 此外,如果不需要在创建工作时替换客户,只需将客户字段设置为最终 工作 class和remove方法  要么 更换 它。

应该在数据库中维护为作业分配客户的限制,并且应该将数据库条目用作检查点。 至于为客户添加为其他人完成的工作,您可以检查工作中的客户参考,以确保添加工作的客户与其拥有的工作相同,甚至更好 - 只需删除客户对Job的任何引用,它将简化您的工作


2
2018-04-10 14:16



谢谢你把时间花在这上面。这一切都有道理,但实施有点矫枉过正。我已经决定让客户领域在工作级别最终,并通过这样做,其余的变得相当容易。我会用你的答案作进一步的参考。谢谢。 - MarioDS


如果Customer对象拥有该关系,那么您可以这样做:

Job job = new Job();
job.setStuff(...);
customer.addJob(Job job) {
    this.jobs.add(job);
    job.setCustomer(this); //set/overwrite the customer for this job
}

//in the job class
public void setCustomer(Customer c) {
    if (this.customer==null) {
        this.customer = c;
    } // for the else{} you could throw a runtime exception
}

...如果所有权是相反的方式,只需替代客户的工作。

这个想法是让关系的所有者保持一致性。双向关系通常意味着一致性管理位于两个实体中。


1
2018-04-10 10:09



这就是我的想法,但它不是一致性的保证。还有一种设置客户的公共方法,可以在添加作业后进行更改...当然作为程序员,您必须以这种方式打破一致性。但必须有办法真正保证它吗?编辑:使该领域最终可能是一个解决方案...... - MarioDS
...或者你可以在job.setCustomer中明确说明(参见编辑的答案) - Ryan Fernandes


做一个适当的setter功能,保持一致性。例如,无论何时创建作业,都要在构造函数中提供客户。然后,作业构造函数将自己添加到客户的作业列表中。 或者,无论何时向客户添加作业,添加功能都必须检查作业的客户是否是要添加到的客户。 或者这个和类似的东西的组合,以满足您的需求。


0
2018-04-10 10:06



如果我想在列表中添加新作业,将无法工作......我将获得重复。喜欢 customer.addJob(new Job(this, a, b, c)); 将导致作业被添加两次。而你的解决方案需要这样的公共方法,所以... - MarioDS
然后不要这样称呼,只做新的工作(这,...) - Matsemann
我担心的是,当你不自己打破它时,保证一致性。当然,你在各方面都是正确的,但我想以这样的方式创建我的课程 没有人 以编程方式能够破坏一致性,而不仅仅是因为它们忽略了公共方法。在程序员可能想要使用它们的各种方式中,方法和构造函数都需要100%可用。 - MarioDS
我不能回复你对另一个答案的评论,因为我是新来的,所以在这里:如果你有一个公共职能来改变客户,这个功能必须通过从旧客户中删除自己并添加来保持一致性本身对新的。 - Matsemann
感谢您的帮助并了解您要解释的内容,但我不确定您是否了解该解决方案可能出现的问题。我将进一步研究它,看看我得到了什么。 - MarioDS


只需在包含其他对象的对象中实现某种集合 例如,客户可以说:

private List<Job> jobs; 

然后通过使用getter和setter,您可以将值作业添加到此列表中。 这是基本的OO东西,我认为你没有在互联网上搜索得足够多。有很多关于这些主题的信息。

顺便说一句,你可以使用所有类型的集合(集合,列表,地图)


-1
2018-04-10 10:27



这不能回答我的问题,你只需重复我已有的内容。你看过我的问题吗? - MarioDS
我很快就回应了,因为我错过了问题的本质。我的坏,真诚的道歉:) - steelshark
没问题。尽管如此,投票不是我的,因为你的答案不正确或无关紧要。 - MarioDS


我知道这已经很晚了,但我认为另一种方法是稍微改变一下这个问题。由于客户持有由客户分配或完成的所有工作的集合,因此您可以将工作类视为客户的子类,其中包含客户完成所有工作的额外信息。然后,您只需要在主类中维护客户ID,它就会被继承。此设计将确保每个作业都可以链接到客户。此外,如果对于客户,您想要了解有多少工作也可以获得。


-1
2017-10-18 20:48





对不起,我知道这已经很晚了,但我遇到了类似的问题,我认为最好的解决方案是遵循继承模型。将工作视为由特定客户完成/工作的工作。因此,在这种情况下,客户将是一个超级类,其中Job(让我们称之为客户工作)是一个子类,因为如果没有客户,Job就不能存在。客户还可以拥有一份工作列表,主要是为了便于数据获取。直觉上这是没有意义的,因为工作和客户完成似乎有任何关系,但是一旦你看到没有客户就不能存在工作,它只是成为客户的延伸。


-1
2017-11-08 10:00



迟到的答案不是问题,但这是一个古怪的想法。所以,如果我打电话 toString() 在这个什么是显示?这违反了单一责任原则。无论如何,您完全忽略了可能不一致的集合。 - candied_orange
好的,首先在调用toString()时你可以修改当前值和超类toString()而不是集合。该集合仅用于存储多个子节点,它不需要存在于toString()中。关于单一责任,那就满足了,因为customerJob类只有与customerJob相关的操作而不是其他任何操作。这里的要点是,因为customerJob不能在没有客户的情况下存在,因为customerJob是客户的延伸,所以我没有看到任何组合。请明确错误,而不仅仅是低估这一点。 - prashant
你对继承是什么有误解。没有食物,水或地球,我就不能存在。我不是其中任何一个的延伸。但哲学上违反最小惊讶的原则除外。如果作业继承自客户,则作业实例中的客户实例具有1对1的关系。对不起,但这没有任何意义。 - candied_orange
好的,当你说没有食物和水时你不能存在,你就是在谈论生活。但我所说的是,如果Tiger扩展Animal并且没有动物那么就没有老虎就是这样可以让客户和客户的工作顺利进行(即没有工作就没有客户工作)。你所说的我同意的第二部分,但是在这种情况下,如果我们建立一个组合关系,那么我们会错过我认为应该捕获的继承部分,所以如果我们保留继承并使用集合保持多对一关系那么IMO我们可以充分利用两个世界 - prashant
继承将确保实体包含所有需要存在的数据,并且集合将建立非常需要的多对一关系。该类仍然只有一个责任,并且对于它的所有操作,它将使用inehritance模型而不是多对一关系。然而,当用作DTO时,它可以存储子列表,从而不需要额外的结构来存储子列表 - prashant