Programming a one-to-many relationship

2019-02-01 13:27发布

问题:

So I am surprised that doing a search on google and stackoverflow doesn't return more results.

In OO programming (I'm using java), how do you correctly implement a one-to-many relationship?

I have a class Customer and class Job. My application is for a fictious company that completes jobs for customers. My current implementation is so that the Job class doesn't have anything to do with the Customer class, there is no reference to it at all. The Customer class uses a collection and methods to hold, retrieve and modify information about the Jobs that have been assigned by and/or completed for a customer.

The question is, what if I'd want to find out for which customer a particular Job has been done? I've only found this article that's relevant: http://www.ibm.com/developerworks/webservices/library/ws-tip-objrel3/index.html.

According to the implementation of the author, I would let the Job constructor take a Customer parameter, and store it so I can retrieve it. However, I see no guarantee at all that this model can be consistent. There are no restirctions to set the related customer for a job as a customer that the job was not for, and add jobs to customers that were done for someone else. Any help on this would be appreciated.

回答1:

There's no 100% surefire way to maintain the integrity.

The approach which is usually taken is to use one method to construct the relationship, and construct the other direction in that same method. But, as you say, this doesn't keep anyone from messing with it.

The next step would be to make some of the methods package-accessible, so that at least code which has nothing to do with yours can't break it:

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

In reality, you won't often model this relationship in your classes. Instead, you will look up all children for a parent in some kind of repository.



回答2:

Maybe you didn't expect a complex (and zero-code) answer, but there is no solution to build your bombproof API the way you intend it. And it's not because the paradigm (OO) or the platform (Java), but only because you made a wrong analysis. In a transactional world (every system that models real life problems and their evolution over time is transactional) This code will ever break at some point:

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

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

because at the time j1.someProperty is accessed, j1 and j2 could not even exist :)

TL;DR

The long answer to this is immutability, and it also introduces the concepts of life cycle and transactions. All other answers tell you how to do it, instead I want to outline why. A one-to-many relationship has two sides

  1. has many
  2. belongs to

Your system is consistent as long as if Customer A has Job B, the Job B belongs to Customer A. You can implement this in a number of ways, but this must happen in a transaction, ie a complex action made of simple ones, and the system must be unavailble until the transaction has finished execution. Does this seem too abstract and unrelated to your question? No, it isn't :) A transactional system ensures that clients can access system's objects only if all these objects are in a valid state, hence only if the whole system is consistent. From other answers you see the amount of processing needed to solve some problems, so that guarantee comes at a cost: performance. This is the simple explanation why Java (and other general purpose OO languages) can't solve your problem out of the box.

Of course, an OO language can be used to both model a transactional world and accessing it, but special care must be taken, some constraints must be imposed and a special programming style be required to client developers. Usually a transactional system offers two commands: search (aka query) and lock. The result of the query is immutable: it's a photo (ie a copy) of the system at the very specific moment it was taken, and modifying the photo has obviously no effect on the real world. How can one modify the system? Usually

  1. lock the system (or parts of it) if/when needed
  2. locate an object: returns a copy (a photo) of the real object which can be read and written locally
  3. modify the local copy
  4. commit the modified object, ie let the system update its state based on provided input
  5. discard any reference to (now useless) local objects: the system has changed changed, so the local copy isn't up to date.

(BTW, can you see how the concept of life cycle is applied to local and remote objects?)

You can go with Sets, final modifiers and so on, but until you introduce transactions and immutability, your design will have a flaw. Usually Java applications are backed by a database, which provides transactional functionalities, and often the DB is coupled with an ORM (such as Hibernate) to write object oriented code.



回答3:

You can ensure that there are no duplicates by using a Set implementation like HashSet instead of using other data-structure. And instead of adding Job to a customer, create an final inner class in Job class that has private constructor. That ensure that the wrapper inner class can only be created by a job object. Make you Job constructor take in jobID and customer as parameter. To maintain consistency -if customer is Null throw Exception as dummy jobs shouldn't be created .

In add method of Customer, check to see if the Job wrapped by JobUnit has the same customer ID as the its own id, if not throw Exception.

When replacing a customer in Job class remove the JobUnit using the method provided by Customer class and add itself to the new customer and change the customer reference to the newly passed customer. That way you can reason with your code better.

Here's what your customer class might look like.

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

}

And the Job Class:

public class Job {
Customer customer;
private Long id;

final JobUnit unit;

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

But one thing I'm curious about is why do you even need to add jobs to a customer object? If all you want to check is to see which customer has been assigned to which job, simply inspecting a Job will give you that information. Generally I try not to create circular references unless unavoidable. Also if replacing a customer from a job once its been created is not necessary, simply make the customer field Final in the Job class and remove method to set or replace it.

The restriction for assigning customer for a job should be maintained in database and the database entry should be used as a checking point. As for adding jobs to customer that were done for someone else, you can either check for customer reference in a job to ensure that the customer to which a job is being added is the same one it holds or even better-simply remove any reference in customer for Job and it will simplify things for you.



回答4:

If the Customer object owns the relationship then you can possibly do it this way:

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
}

...if the ownership is the other way around, just substitute customer for job.

The idea is to have the owner of the relationship maintain consistency. Bi-directional relationships generally imply that the consistency management sits in both entities.



回答5:

Make a proper setter-function that maintains consistency. For instance, whenever you create a job, you supply the customer in the constructor. The job constructor then adds itself to the customer's list of jobs. Or whenever you add a job to a customer, the add function has to check that the job's customer is the customer it's being added to. Or some combination of this and similar things to what suits your needs.



回答6:

Just implement some sort of collection in the object that has the other objects For example in customer you could say:

private List<Job> jobs; 

then by using getters and setters you can add values jobs to this list. This is basic OO stuff, I don't think you searched enough on the internet. there is a lot of info available on these subjects.

Btw, you can use all sort of collections (Sets, Lists, Maps)



回答7:

I know this is late but I think another way to this would be to look at the problem a bit differently. Since customer holds a collection of all jobs assigned by or completed for a customer, you could consider the job class to be a sub class of customer with extra information of having all the jobs completed by the customer. Then you would only have to maintain customer id in the main class and it would be inherited. This design would ensure that each job can be linked to a customer. Also if for a customer you want to find out how many jobs are present that too also would be got.



回答8:

I am sorry I know this is very late but I have come across a similar problem where I feel the best solution is to follow a inheritance model. Think of job as being jobs done/asisgned by a particular customer. So in that case the Customer would be a super class with the Job(Lets call is customer job) being a sub class since a Job cannot exists without a customer. A customer would also have a list of jobs primarily for ease of data fetching. Intutively this does not make sense since Job and Customer done seem to have any relation, however once you see that Job cannot exist without a customer, it just becomes an extension of customer.